From 718e95850dda96a16b20063caf00c2d2971a9b02 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Fri, 8 Nov 2024 15:49:13 -0500
Subject: [PATCH 001/198] wip
---
src/constants.js | 2 +-
src/tools/creatMonitoringGoalsCLI.js | 7 +
src/tools/createMonitoringGoals.js | 101 ++++++++
src/tools/createMonitoringGoals.test.js | 310 ++++++++++++++++++++++++
4 files changed, 419 insertions(+), 1 deletion(-)
create mode 100644 src/tools/creatMonitoringGoalsCLI.js
create mode 100644 src/tools/createMonitoringGoals.js
create mode 100644 src/tools/createMonitoringGoals.test.js
diff --git a/src/constants.js b/src/constants.js
index b99de39882..9e80e20b5b 100644
--- a/src/constants.js
+++ b/src/constants.js
@@ -246,7 +246,7 @@ const MAINTENANCE_CATEGORY = {
IMPORT: 'IMPORT',
};
-const GOAL_CREATED_VIA = ['imported', 'activityReport', 'rtr', 'merge', 'admin'];
+const GOAL_CREATED_VIA = ['imported', 'activityReport', 'rtr', 'merge', 'admin', 'monitoring'];
const CURRENT_GOAL_SIMILARITY_VERSION = 4;
diff --git a/src/tools/creatMonitoringGoalsCLI.js b/src/tools/creatMonitoringGoalsCLI.js
new file mode 100644
index 0000000000..94228c6255
--- /dev/null
+++ b/src/tools/creatMonitoringGoalsCLI.js
@@ -0,0 +1,7 @@
+import createMonitoringGoals from './createMonitoringGoals';
+import { auditLogger } from '../logger';
+
+createMonitoringGoals().catch((e) => {
+ auditLogger.error(e);
+ process.exit(1);
+});
diff --git a/src/tools/createMonitoringGoals.js b/src/tools/createMonitoringGoals.js
new file mode 100644
index 0000000000..ad44894100
--- /dev/null
+++ b/src/tools/createMonitoringGoals.js
@@ -0,0 +1,101 @@
+import {
+ sequelize,
+ GoalTemplate,
+} from '../models';
+import { auditLogger } from '../logger';
+
+const createMonitoringGoals = async () => {
+ console.log('\n\n\n----- start of job');
+ const cutOffDate = '2023-12-01';
+ const monitoringGoalTemplateId = 18172;
+
+ // Verify that the monitoring goal template exists.
+ const monitoringGoalTemplate = await GoalTemplate.findOne({
+ where: {
+ id: monitoringGoalTemplateId,
+ },
+ });
+ console.log('\n\n\n------monitoringGoalTemplate', monitoringGoalTemplate);
+
+ // If the monitoring goal template does not exist, throw an error.
+ if (!monitoringGoalTemplate) {
+ auditLogger.error(`Monitoring Goal template with ID ${monitoringGoalTemplateId} not found`);
+ return;
+ }
+
+ // Create monitoring goals for grants that need them.
+ await sequelize.query(`
+ WITH
+ grants_needing_goal AS (
+ SELECT
+ grta."activeGrantId" "grantId"
+ FROM "Grants" gr
+ JOIN "GrantRelationshipToActive" grta
+ ON gr.id = grta."grantId"
+ AND grta."activeGrantId" IS NOT NULL
+ JOIN "MonitoringReviewGrantees" mrg
+ ON gr.number = mrg."grantNumber"
+ JOIN "MonitoringReviews" mr
+ ON mrg."reviewId" = mr."reviewId"
+ JOIN "MonitoringReviewStatuses" mrs
+ ON mr."statusId" = mrs."statusId"
+ JOIN "MonitoringFindingHistories" mfh
+ ON mr."reviewId" = mfh."reviewId"
+ JOIN "MonitoringFindings" mf
+ ON mfh."findingId" = mf."findingId"
+ JOIN "MonitoringFindingStatuses" mfs
+ ON mf."statusId" = mfs."statusId"
+ JOIN "MonitoringFindingGrants" mfg
+ ON mf."findingId" = mfg."findingId"
+ AND mrg."granteeId" = mfg."granteeId"
+ LEFT JOIN "Goals" g
+ ON (grta."grantId" = g."grantId"
+ OR grta."activeGrantId" = g."grantId")
+ AND g."goalTemplateId" = ${monitoringGoalTemplateId} -- NEEDS TO BE CHANGED TO THE MONITORING GOAL
+ WHERE gr.status = 'Active'
+ AND mrs."name" = 'Complete'
+ AND mfs."name" = 'Active'
+ AND mr."reportDeliveryDate" BETWEEN '${cutOffDate}' AND NOW()
+ AND mr."reviewType" IN (
+ 'AIAN-DEF',
+ 'RAN',
+ 'Follow-up',
+ 'FA-1', 'FA1-FR',
+ 'FA-2', 'FA2-CR',
+ 'Special'
+ )
+ AND g.id IS NULL
+ GROUP BY 1
+ ),
+ new_goals AS (
+ SELECT
+ gt."templateName" "name",
+ 'Not started' "status",
+ NULL "timeframe",
+ FALSE "isFromSmartSheetTtaPlan",
+ NOW() "createdAt",
+ NOW() "updatedAt",
+ NULL "endDate",
+ gt.id "goalTemplateId",
+ gng."grantId" "grantId",
+ FALSE "onApprovedAR",
+ 'monitoring' "createdVIA",
+ FALSE "isRttapa",
+ FALSE "onAR",
+ NULL "rtrOrder",
+ 'Federal monitoring issues, including CLASS and RANs' "source",
+ NULL "deletedAt",
+ NULL "mapsToParrentGoalId"
+ FROM "GoalTemplates" gt
+ CROSS JOIN grants_needing_goal gng
+ WHERE gt.id = 18172 -- NEEDS TO BE CHANGED TO THE MONITORING GOAL
+ )
+ INSERT INTO "Goals"
+ ("name", "status", "timeframe", "isFromSmartSheetTtaPlan", "createdAt", "updatedAt", "endDate", "goalTemplateId", "grantId", "onApprovedAR", "createdVIA", "isRttapa", "onAR", "rtrOrder", "source", "deletedAt", "mapsToParrentGoalId")
+ SELECT
+ "name", "status", "timeframe", "isFromSmartSheetTtaPlan", "createdAt", "updatedAt", "endDate", "goalTemplateId", "grantId", "onApprovedAR", "createdVIA", "isRttapa", "onAR", "rtrOrder", "source", "deletedAt", "mapsToParrentGoalId"
+ FROM new_goals;
+ `);
+};
+
+export default createMonitoringGoals;
diff --git a/src/tools/createMonitoringGoals.test.js b/src/tools/createMonitoringGoals.test.js
new file mode 100644
index 0000000000..f696b7c0ca
--- /dev/null
+++ b/src/tools/createMonitoringGoals.test.js
@@ -0,0 +1,310 @@
+import faker from '@faker-js/faker';
+import createMonitoringGoals from './createMonitoringGoals';
+import {
+ sequelize,
+ GoalTemplate,
+ Recipient,
+ Grant,
+ MonitoringReviewGrantee,
+ MonitoringReview,
+ MonitoringReviewStatus,
+ MonitoringFindingHistory,
+ MonitoringFinding,
+ MonitoringFindingStatus,
+ MonitoringFindingGrant,
+ Goal,
+} from '../models';
+import { auditLogger } from '../logger';
+
+jest.mock('../logger');
+
+describe('createMonitoringGoals', () => {
+ let recipient;
+ let allGrants;
+ let grantThatNeedsMonitoringGoal;
+ let grantThatAlreadyHasMonitoringGoal;
+ let grantThatFallsBeforeStartDate;
+ let grantThatFallsAfterCutOffDate;
+ let grantThatIsInactive;
+ let grantThatsMonitoringReviewStatusIsNotComplete;
+ let grantThatsMonitoringFindingStatusIsNotActive;
+ let grantThatsMonitoringReviewReviewTypeIsNotAllowed;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ beforeAll(async () => {
+ try {
+ // Recipient.
+ recipient = await Recipient.create({
+ id: faker.datatype.number({ min: 64000 }),
+ name: faker.random.alphaNumeric(6),
+ });
+
+ // Grants.
+ const grants = await Grant.bulkCreate([
+ {
+ id: faker.datatype.number({ min: 9999 }),
+ number: faker.datatype.string(),
+ recipientId: recipient.id,
+ regionId: 1,
+ startDate: new Date(),
+ endDate: new Date(),
+ status: 'Active',
+ },
+ {
+ id: faker.datatype.number({ min: 9999 }),
+ number: faker.datatype.string(),
+ recipientId: recipient.id,
+ regionId: 1,
+ startDate: new Date(),
+ endDate: new Date(),
+ status: 'Active',
+ },
+ {
+ id: faker.datatype.number({ min: 9999 }),
+ number: faker.datatype.string(),
+ recipientId: recipient.id,
+ regionId: 1,
+ startDate: new Date('2021-01-01'),
+ endDate: new Date('2021-12-31'),
+ status: 'Active',
+ },
+ {
+ id: faker.datatype.number({ min: 9999 }),
+ number: faker.datatype.string(),
+ recipientId: recipient.id,
+ regionId: 1,
+ startDate: new Date('2023-12-02'),
+ endDate: new Date('2024-12-01'),
+ status: 'Active',
+ },
+ {
+ id: faker.datatype.number({ min: 9999 }),
+ number: faker.datatype.string(),
+ recipientId: recipient.id,
+ regionId: 1,
+ startDate: new Date(),
+ endDate: new Date(),
+ status: 'Inactive',
+ },
+ {
+ id: faker.datatype.number({ min: 9999 }),
+ number: faker.datatype.string(),
+ recipientId: recipient.id,
+ regionId: 1,
+ startDate: new Date(),
+ endDate: new Date(),
+ status: 'Active',
+ },
+ {
+ id: faker.datatype.number({ min: 9999 }),
+ number: faker.datatype.string(),
+ recipientId: recipient.id,
+ regionId: 1,
+ startDate: new Date(),
+ endDate: new Date(),
+ status: 'Active',
+ },
+ {
+ id: faker.datatype.number({ min: 9999 }),
+ number: faker.datatype.string(),
+ recipientId: recipient.id,
+ regionId: 1,
+ startDate: new Date(),
+ endDate: new Date(),
+ status: 'Active',
+ },
+ ]);
+
+ [
+ grantThatNeedsMonitoringGoal,
+ grantThatAlreadyHasMonitoringGoal,
+ grantThatFallsBeforeStartDate,
+ grantThatFallsAfterCutOffDate,
+ grantThatIsInactive,
+ grantThatsMonitoringReviewStatusIsNotComplete,
+ grantThatsMonitoringFindingStatusIsNotActive,
+ grantThatsMonitoringReviewReviewTypeIsNotAllowed,
+ ] = grants;
+
+ // Set allGrants to the grants that were created.
+ allGrants = grants;
+
+ // MonitoringReviewGrantee.
+ await MonitoringReviewGrantee.bulkCreate([
+ {
+ id: faker.datatype.number({ min: 9999 }),
+ grantNumber: grantThatNeedsMonitoringGoal.number,
+ reviewId: faker.datatype.string(),
+ granteeId: recipient.id,
+ createTime: new Date(),
+ updateTime: new Date(),
+ updateBy: 'Support Team',
+ },
+ {
+ id: faker.datatype.number({ min: 9999 }),
+ grantNumber: grantThatAlreadyHasMonitoringGoal.number,
+ reviewId: faker.datatype.string(),
+ granteeId: recipient.id,
+ createTime: new Date(),
+ updateTime: new Date(),
+ updateBy: 'Support Team',
+ },
+ {
+ id: faker.datatype.number({ min: 9999 }),
+ grantNumber: grantThatFallsBeforeStartDate.number,
+ reviewId: faker.datatype.string(),
+ granteeId: recipient.id,
+ createTime: new Date(),
+ updateTime: new Date(),
+ updateBy: 'Support Team',
+ },
+ {
+ id: faker.datatype.number({ min: 9999 }),
+ grantNumber: grantThatFallsAfterCutOffDate.number,
+ reviewId: faker.datatype.string(),
+ granteeId: recipient.id,
+ createTime: new Date(),
+ updateTime: new Date(),
+ updateBy: 'Support Team',
+ },
+ {
+ id: faker.datatype.number({ min: 9999 }),
+ grantNumber: grantThatIsInactive.number,
+ reviewId: faker.datatype.string(),
+ granteeId: recipient.id,
+ createTime: new Date(),
+ updateTime: new Date(),
+ updateBy: 'Support Team',
+ },
+ {
+ id: faker.datatype.number({ min: 9999 }),
+ grantNumber: grantThatsMonitoringReviewStatusIsNotComplete.number,
+ reviewId: faker.datatype.string(),
+ granteeId: recipient.id,
+ createTime: new Date(),
+ updateTime: new Date(),
+ updateBy: 'Support Team',
+ },
+ {
+ id: faker.datatype.number({ min: 9999 }),
+ grantNumber: grantThatsMonitoringFindingStatusIsNotActive.number,
+ reviewId: faker.datatype.string(),
+ granteeId: recipient.id,
+ createTime: new Date(),
+ updateTime: new Date(),
+ updateBy: 'Support Team',
+ },
+ {
+ id: faker.datatype.number({ min: 9999 }),
+ grantNumber: grantThatsMonitoringReviewReviewTypeIsNotAllowed.number,
+ reviewId: faker.datatype.string(),
+ granteeId: recipient.id,
+ createTime: new Date(),
+ updateTime: new Date(),
+ updateBy: 'Support Team',
+ },
+ ]);
+
+ // MonitoringReview.
+ /*
+ await MonitoringReview.bulkCreate([
+ {
+ id: faker.datatype.string(),
+ statusId: 1,
+ reportDeliveryDate: new Date(),
+ reviewType: 'AIAN-DEF',
+ },
+ {
+ id: faker.datatype.string(),
+ statusId: 1,
+ reportDeliveryDate: new Date(),
+ reviewType: 'AIAN-DEF',
+ },
+ {
+ id: faker.datatype.string(),
+ statusId: 1,
+ reportDeliveryDate: new Date(),
+ reviewType: 'AIAN-DEF',
+ },
+ {
+ id: faker.datatype.string(),
+ statusId: 1,
+ reportDeliveryDate: new Date(),
+ reviewType: 'AIAN-DEF',
+ },
+ {
+ id: faker.datatype.string(),
+ statusId: 2,
+ reportDeliveryDate: new Date(),
+ reviewType: 'AIAN-DEF',
+ },
+ {
+ id: faker.datatype.string(),
+ statusId: 1,
+ reportDeliveryDate: new Date(),
+ reviewType: 'AIAN-DEF',
+ },
+ {
+ id: faker.datatype.string(),
+ statusId: 1,
+ reportDeliveryDate: new Date(),
+ reviewType: 'AIAN-DEF',
+ },
+ {
+ id: faker.datatype.string(),
+ statusId: 1,
+ reportDeliveryDate: new Date(),
+ reviewType: 'AIAN-DEF',
+ },
+ ]);
+ */
+ } catch (error) {
+ console.log('\n\n\n============create error', error);
+ }
+ });
+
+ afterAll(async () => {
+ try {
+ // Delete MonitoringReviewGrantee.
+ await MonitoringReviewGrantee.destroy({
+ where: {
+ grantNumber: allGrants.map((grant) => grant.number),
+ },
+ });
+
+ // Delete Grants.
+ await Grant.destroy({
+ where: {
+ number: allGrants.map((grant) => grant.number),
+ },
+ });
+
+ // Delete Recipient.
+ await Recipient.destroy({
+ where: {
+ id: recipient.id,
+ },
+ });
+
+ // Close the connection to the database.
+ await sequelize.close();
+ } catch (error) {
+ console.log('\n\n\n============delete error', error);
+ }
+ });
+
+ it('logs an error is the monitoring goal template doesn\'t exist', async () => {
+ // Mock the GoalTemplate.findOne method to return null.
+ GoalTemplate.findOne = jest.fn().mockResolvedValueOnce(null);
+ jest.spyOn(auditLogger, 'error');
+ await createMonitoringGoals();
+ expect(auditLogger.error).toHaveBeenCalledWith('Monitoring Goal template with ID 18172 not found');
+ });
+
+ it('creates monitoring goals for grants that need them', async () => {
+ expect(true).toBe(true);
+ });
+});
From 8dfd48edd0ec9b9d683f3c4c227c58f4b7c1228f Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Sat, 9 Nov 2024 13:42:45 -0500
Subject: [PATCH 002/198] create some data to test all the various cases for
the monitorign CRON
---
docker-compose.yml | 20 -
src/tools/createMonitoringGoals.test.js | 972 +++++++++++++++++++++---
2 files changed, 850 insertions(+), 142 deletions(-)
diff --git a/docker-compose.yml b/docker-compose.yml
index 1931e66c4e..0f9ca32645 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,8 +1,6 @@
services:
api-docs:
image: redocly/redoc
- profiles:
- - full_stack
ports:
- "5003:80"
volumes:
@@ -13,8 +11,6 @@ services:
image: postgres:15.6
container_name: postgres_docker
env_file: .env
- profiles:
- - minimal_required
ports:
- "5432:5432"
volumes:
@@ -22,8 +18,6 @@ services:
shm_size: 1g
minio:
image: minio/minio:RELEASE.2024-01-01T16-36-33Z
- profiles:
- - full_stack
env_file: .env
ports:
- "9000:9000"
@@ -33,16 +27,12 @@ services:
command: server /data --console-address ":9001"
aws-cli:
image: amazon/aws-cli
- profiles:
- - full_stack
env_file: .env
command: ["--endpoint-url", "http://minio:9000", "s3api", "create-bucket", "--bucket", "$S3_BUCKET"]
depends_on:
- minio
clamav-rest:
image: ajilaag/clamav-rest
- profiles:
- - full_stack
ports:
- "9443:9443"
environment:
@@ -50,8 +40,6 @@ services:
similarity_api:
build:
context: ./similarity_api
- profiles:
- - minimal_required
ports:
- "9100:8080"
env_file: .env
@@ -61,24 +49,18 @@ services:
- "./similarity_api/src:/app:rw"
redis:
image: redis:5.0.6-alpine
- profiles:
- - minimal_required
command: ['redis-server', '--requirepass', '$REDIS_PASS']
env_file: .env
ports:
- "6379:6379"
mailcatcher:
image: schickling/mailcatcher
- profiles:
- - full_stack
ports:
- "1025:1025"
- "1080:1080"
testingonly:
build:
context: .
- profiles:
- - full_stack
ports:
- "9999:9999"
depends_on:
@@ -91,8 +73,6 @@ services:
- NODE_ENV=development
sftp:
image: jmcombs/sftp:alpine
- profiles:
- - full_stack
volumes:
- ./test-sftp:/home/tta_ro/ProdTTAHome
- ./test-sftp/sshd_config:/etc/ssh/sshd_config
diff --git a/src/tools/createMonitoringGoals.test.js b/src/tools/createMonitoringGoals.test.js
index f696b7c0ca..7d6592a803 100644
--- a/src/tools/createMonitoringGoals.test.js
+++ b/src/tools/createMonitoringGoals.test.js
@@ -1,4 +1,6 @@
+/* eslint-disable max-len */
import faker from '@faker-js/faker';
+import { v4 as uuidv4 } from 'uuid';
import createMonitoringGoals from './createMonitoringGoals';
import {
sequelize,
@@ -12,8 +14,10 @@ import {
MonitoringFinding,
MonitoringFindingStatus,
MonitoringFindingGrant,
+ GrantReplacement,
Goal,
} from '../models';
+import { captureSnapshot, rollbackToSnapshot } from '../lib/programmaticTransaction';
import { auditLogger } from '../logger';
jest.mock('../logger');
@@ -21,22 +25,42 @@ jest.mock('../logger');
describe('createMonitoringGoals', () => {
let recipient;
let allGrants;
- let grantThatNeedsMonitoringGoal;
- let grantThatAlreadyHasMonitoringGoal;
- let grantThatFallsBeforeStartDate;
- let grantThatFallsAfterCutOffDate;
- let grantThatIsInactive;
- let grantThatsMonitoringReviewStatusIsNotComplete;
- let grantThatsMonitoringFindingStatusIsNotActive;
- let grantThatsMonitoringReviewReviewTypeIsNotAllowed;
+ const startingReportDeliveryDate = new Date('2023-12-01');
+
+ const goalTemplateId = 18172;
+ let goalTemplate;
+
+ let grantThatNeedsMonitoringGoal1;
+ let grantThatAlreadyHasMonitoringGoal2;
+ let grantThatFallsBeforeStartDate3;
+ let grantThatFallsAfterCutOffDate4;
+ let grantThatIsInactive5;
+ let grantThatsMonitoringReviewStatusIsNotComplete6;
+ let grantThatsMonitoringFindingStatusIsNotActive7;
+ let grantThatsMonitoringReviewReviewTypeIsNotAllowed8;
+ let inactiveGrantThatHasBeenReplacedByActiveGrant9;
+
+ const grantThatNeedsMonitoringGoalNumber1 = faker.datatype.string(4);
+ const grantThatAlreadyHasMonitoringGoalNumber2 = faker.datatype.string(4);
+ const grantThatFallsBeforeStartDateNumber3 = faker.datatype.string(4);
+ const grantThatFallsAfterCutOffDateNumber4 = faker.datatype.string(4);
+ const grantThatIsInactiveNumber5 = faker.datatype.string(4);
+ const grantThatsMonitoringReviewStatusIsNotCompleteNumber6 = faker.datatype.string(4);
+ const grantThatsMonitoringFindingStatusIsNotActiveNumber7 = faker.datatype.string(4);
+ const grantThatsMonitoringReviewReviewTypeIsNotAllowedNumber8 = faker.datatype.string(4);
+ const inactiveGrantThatHasBeenReplacedByActiveGrantNumber9 = faker.datatype.string(4);
+
+ let snapShot;
beforeEach(() => {
jest.clearAllMocks();
});
beforeAll(async () => {
try {
- // Recipient.
+ // Create a snapshot of the database so we can rollback after the tests.
+ snapShot = await captureSnapshot();
+ // Recipient.
recipient = await Recipient.create({
id: faker.datatype.number({ min: 64000 }),
name: faker.random.alphaNumeric(6),
@@ -45,8 +69,9 @@ describe('createMonitoringGoals', () => {
// Grants.
const grants = await Grant.bulkCreate([
{
+ // 1
id: faker.datatype.number({ min: 9999 }),
- number: faker.datatype.string(),
+ number: grantThatNeedsMonitoringGoalNumber1,
recipientId: recipient.id,
regionId: 1,
startDate: new Date(),
@@ -54,8 +79,9 @@ describe('createMonitoringGoals', () => {
status: 'Active',
},
{
+ // 2
id: faker.datatype.number({ min: 9999 }),
- number: faker.datatype.string(),
+ number: grantThatAlreadyHasMonitoringGoalNumber2,
recipientId: recipient.id,
regionId: 1,
startDate: new Date(),
@@ -63,8 +89,9 @@ describe('createMonitoringGoals', () => {
status: 'Active',
},
{
+ // 3
id: faker.datatype.number({ min: 9999 }),
- number: faker.datatype.string(),
+ number: grantThatFallsBeforeStartDateNumber3,
recipientId: recipient.id,
regionId: 1,
startDate: new Date('2021-01-01'),
@@ -72,8 +99,9 @@ describe('createMonitoringGoals', () => {
status: 'Active',
},
{
+ // 4
id: faker.datatype.number({ min: 9999 }),
- number: faker.datatype.string(),
+ number: grantThatFallsAfterCutOffDateNumber4,
recipientId: recipient.id,
regionId: 1,
startDate: new Date('2023-12-02'),
@@ -81,8 +109,9 @@ describe('createMonitoringGoals', () => {
status: 'Active',
},
{
+ // 5
id: faker.datatype.number({ min: 9999 }),
- number: faker.datatype.string(),
+ number: grantThatIsInactiveNumber5,
recipientId: recipient.id,
regionId: 1,
startDate: new Date(),
@@ -90,8 +119,9 @@ describe('createMonitoringGoals', () => {
status: 'Inactive',
},
{
+ // 6
id: faker.datatype.number({ min: 9999 }),
- number: faker.datatype.string(),
+ number: grantThatsMonitoringReviewStatusIsNotCompleteNumber6,
recipientId: recipient.id,
regionId: 1,
startDate: new Date(),
@@ -99,8 +129,9 @@ describe('createMonitoringGoals', () => {
status: 'Active',
},
{
+ // 7
id: faker.datatype.number({ min: 9999 }),
- number: faker.datatype.string(),
+ number: grantThatsMonitoringFindingStatusIsNotActiveNumber7,
recipientId: recipient.id,
regionId: 1,
startDate: new Date(),
@@ -108,8 +139,19 @@ describe('createMonitoringGoals', () => {
status: 'Active',
},
{
+ // 8
id: faker.datatype.number({ min: 9999 }),
- number: faker.datatype.string(),
+ number: grantThatsMonitoringReviewReviewTypeIsNotAllowedNumber8,
+ recipientId: recipient.id,
+ regionId: 1,
+ startDate: new Date(),
+ endDate: new Date(),
+ status: 'Active',
+ },
+ {
+ // 9
+ id: faker.datatype.number({ min: 9999 }),
+ number: inactiveGrantThatHasBeenReplacedByActiveGrantNumber9,
recipientId: recipient.id,
regionId: 1,
startDate: new Date(),
@@ -119,176 +161,862 @@ describe('createMonitoringGoals', () => {
]);
[
- grantThatNeedsMonitoringGoal,
- grantThatAlreadyHasMonitoringGoal,
- grantThatFallsBeforeStartDate,
- grantThatFallsAfterCutOffDate,
- grantThatIsInactive,
- grantThatsMonitoringReviewStatusIsNotComplete,
- grantThatsMonitoringFindingStatusIsNotActive,
- grantThatsMonitoringReviewReviewTypeIsNotAllowed,
+ grantThatNeedsMonitoringGoal1,
+ grantThatAlreadyHasMonitoringGoal2,
+ grantThatFallsBeforeStartDate3,
+ grantThatFallsAfterCutOffDate4,
+ grantThatIsInactive5,
+ grantThatsMonitoringReviewStatusIsNotComplete6,
+ grantThatsMonitoringFindingStatusIsNotActive7,
+ grantThatsMonitoringReviewReviewTypeIsNotAllowed8,
+ inactiveGrantThatHasBeenReplacedByActiveGrant9,
] = grants;
+ // Create an inactive grant that has 'cdi' true that points to inactiveGrantThatHasBeenReplacedByActiveGrant9.
+ const cdiGrant = await Grant.create({
+ id: faker.datatype.number({ min: 9999 }),
+ number: faker.datatype.string(4),
+ recipientId: recipient.id,
+ regionId: 1,
+ startDate: new Date(Date.now() - 24 * 60 * 60 * 1000), // Set to yesterday's date
+ endDate: new Date(Date.now() - 24 * 60 * 60 * 1000), // Set to yesterday's date
+ status: 'Inactive',
+ cdi: true,
+ });
+
+ // Insert into GrantReplacement to show that cdiGrant was replaced by inactiveGrantThatHasBeenReplacedByActiveGrant9.
+ await GrantReplacement.create({
+ replacedGrantId: cdiGrant.id,
+ replacingGrantId: inactiveGrantThatHasBeenReplacedByActiveGrant9.id,
+ grantReplacementTypeId: faker.datatype.number({ min: 1, max: 10 }),
+ replacementDate: new Date(),
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ });
+
// Set allGrants to the grants that were created.
allGrants = grants;
+ // granteeId GUID.
+ const grantThatNeedsMonitoringGoalNumberGranteeId1 = uuidv4();
+ const grantThatAlreadyHasMonitoringGoalNumberGranteeId2 = uuidv4();
+ const grantThatFallsBeforeStartDateNumberGranteeId3 = uuidv4();
+ const grantThatFallsAfterCutOffDateNumberGranteeId4 = uuidv4();
+ const grantThatIsInactiveNumberGranteeId5 = uuidv4();
+ const grantThatsMonitoringReviewStatusIsNotCompleteNumberGranteeId6 = uuidv4();
+ const grantThatsMonitoringFindingStatusIsNotActiveNumberGranteeId7 = uuidv4();
+ const grantThatsMonitoringReviewReviewTypeIsNotAllowedNumberGranteeId8 = uuidv4();
+ const inactiveGrantThatHasBeenReplacedByActiveGrantNumberGranteeId9 = uuidv4();
+
+ // reviewId GUID.
+ const grantThatNeedsMonitoringGoalNumberReviewId1 = uuidv4();
+ const grantThatAlreadyHasMonitoringGoalNumberReviewId2 = uuidv4();
+ const grantThatFallsBeforeStartDateNumberReviewId3 = uuidv4();
+ const grantThatFallsAfterCutOffDateNumberReviewId4 = uuidv4();
+ const grantThatIsInactiveNumberReviewId5 = uuidv4();
+ const grantThatsMonitoringReviewStatusIsNotCompleteNumberReviewId6 = uuidv4();
+ const grantThatsMonitoringFindingStatusIsNotActiveNumberReviewId7 = uuidv4();
+ const grantThatsMonitoringReviewReviewTypeIsNotAllowedNumberReviewId8 = uuidv4();
+ const inactiveGrantThatHasBeenReplacedByActiveGrantNumberReviewId9 = uuidv4();
+
// MonitoringReviewGrantee.
await MonitoringReviewGrantee.bulkCreate([
{
+ // 1
id: faker.datatype.number({ min: 9999 }),
- grantNumber: grantThatNeedsMonitoringGoal.number,
- reviewId: faker.datatype.string(),
- granteeId: recipient.id,
+ grantNumber: grantThatNeedsMonitoringGoalNumber1,
+ reviewId: grantThatNeedsMonitoringGoalNumberReviewId1,
+ granteeId: grantThatNeedsMonitoringGoalNumberGranteeId1,
createTime: new Date(),
updateTime: new Date(),
updateBy: 'Support Team',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
},
{
+ // 2
id: faker.datatype.number({ min: 9999 }),
- grantNumber: grantThatAlreadyHasMonitoringGoal.number,
- reviewId: faker.datatype.string(),
- granteeId: recipient.id,
+ grantNumber: grantThatAlreadyHasMonitoringGoalNumber2,
+ reviewId: grantThatAlreadyHasMonitoringGoalNumberReviewId2,
+ granteeId: grantThatAlreadyHasMonitoringGoalNumberGranteeId2,
createTime: new Date(),
updateTime: new Date(),
updateBy: 'Support Team',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
},
{
+ // 3
id: faker.datatype.number({ min: 9999 }),
- grantNumber: grantThatFallsBeforeStartDate.number,
- reviewId: faker.datatype.string(),
- granteeId: recipient.id,
+ grantNumber: grantThatFallsBeforeStartDateNumber3,
+ reviewId: grantThatFallsBeforeStartDateNumberReviewId3,
+ granteeId: grantThatFallsBeforeStartDateNumberGranteeId3,
createTime: new Date(),
updateTime: new Date(),
updateBy: 'Support Team',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
},
{
+ // 4
id: faker.datatype.number({ min: 9999 }),
- grantNumber: grantThatFallsAfterCutOffDate.number,
- reviewId: faker.datatype.string(),
- granteeId: recipient.id,
+ grantNumber: grantThatFallsAfterCutOffDateNumber4,
+ reviewId: grantThatFallsAfterCutOffDateNumberReviewId4,
+ granteeId: grantThatFallsAfterCutOffDateNumberGranteeId4,
createTime: new Date(),
updateTime: new Date(),
updateBy: 'Support Team',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
},
{
+ // 5
id: faker.datatype.number({ min: 9999 }),
- grantNumber: grantThatIsInactive.number,
- reviewId: faker.datatype.string(),
- granteeId: recipient.id,
+ grantNumber: grantThatIsInactiveNumber5,
+ reviewId: grantThatIsInactiveNumberReviewId5,
+ granteeId: grantThatIsInactiveNumberGranteeId5,
createTime: new Date(),
updateTime: new Date(),
updateBy: 'Support Team',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
},
{
+ // 6
id: faker.datatype.number({ min: 9999 }),
- grantNumber: grantThatsMonitoringReviewStatusIsNotComplete.number,
- reviewId: faker.datatype.string(),
- granteeId: recipient.id,
+ grantNumber: grantThatsMonitoringReviewStatusIsNotCompleteNumber6,
+ reviewId: grantThatsMonitoringReviewStatusIsNotCompleteNumberReviewId6,
+ granteeId: grantThatsMonitoringReviewStatusIsNotCompleteNumberGranteeId6,
createTime: new Date(),
updateTime: new Date(),
updateBy: 'Support Team',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
},
{
+ // 7
id: faker.datatype.number({ min: 9999 }),
- grantNumber: grantThatsMonitoringFindingStatusIsNotActive.number,
- reviewId: faker.datatype.string(),
- granteeId: recipient.id,
+ grantNumber: grantThatsMonitoringFindingStatusIsNotActiveNumber7,
+ reviewId: grantThatsMonitoringFindingStatusIsNotActiveNumberReviewId7,
+ granteeId: grantThatsMonitoringFindingStatusIsNotActiveNumberGranteeId7,
createTime: new Date(),
updateTime: new Date(),
updateBy: 'Support Team',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
},
{
+ // 8
id: faker.datatype.number({ min: 9999 }),
- grantNumber: grantThatsMonitoringReviewReviewTypeIsNotAllowed.number,
- reviewId: faker.datatype.string(),
- granteeId: recipient.id,
+ grantNumber: grantThatsMonitoringReviewReviewTypeIsNotAllowedNumber8,
+ reviewId: grantThatsMonitoringReviewReviewTypeIsNotAllowedNumberReviewId8,
+ granteeId: grantThatsMonitoringReviewReviewTypeIsNotAllowedNumberGranteeId8,
createTime: new Date(),
updateTime: new Date(),
updateBy: 'Support Team',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
},
- ]);
+ {
+ // 9
+ id: faker.datatype.number({ min: 9999 }),
+ grantNumber: inactiveGrantThatHasBeenReplacedByActiveGrant9.number,
+ reviewId: inactiveGrantThatHasBeenReplacedByActiveGrantNumberReviewId9,
+ granteeId: inactiveGrantThatHasBeenReplacedByActiveGrantNumberGranteeId9,
+ createTime: new Date(),
+ updateTime: new Date(),
+ updateBy: 'Support Team',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ ], { individualHooks: true });
+
+ // Create 9 statusId variables made up of 6 random numbers and set each one to its corresponding statusId below in MonitoringReview.bulkCreate.
+ const statusId1 = faker.datatype.number({ min: 1, max: 9 });
+ const statusId2 = faker.datatype.number({ min: 1, max: 9 });
+ const statusId3 = faker.datatype.number({ min: 1, max: 9 });
+ const statusId4 = faker.datatype.number({ min: 1, max: 9 });
+ const statusId5 = faker.datatype.number({ min: 1, max: 9 });
+ const statusId6 = faker.datatype.number({ min: 1, max: 9 });
+ const statusId7 = faker.datatype.number({ min: 1, max: 9 });
+ const statusId8 = faker.datatype.number({ min: 1, max: 9 });
+ const statusId9 = faker.datatype.number({ min: 1, max: 9 });
// MonitoringReview.
- /*
- await MonitoringReview.bulkCreate([
- {
- id: faker.datatype.string(),
- statusId: 1,
- reportDeliveryDate: new Date(),
- reviewType: 'AIAN-DEF',
- },
- {
- id: faker.datatype.string(),
- statusId: 1,
- reportDeliveryDate: new Date(),
- reviewType: 'AIAN-DEF',
- },
- {
- id: faker.datatype.string(),
- statusId: 1,
- reportDeliveryDate: new Date(),
- reviewType: 'AIAN-DEF',
- },
- {
- id: faker.datatype.string(),
- statusId: 1,
- reportDeliveryDate: new Date(),
- reviewType: 'AIAN-DEF',
- },
- {
- id: faker.datatype.string(),
- statusId: 2,
- reportDeliveryDate: new Date(),
- reviewType: 'AIAN-DEF',
- },
- {
- id: faker.datatype.string(),
- statusId: 1,
- reportDeliveryDate: new Date(),
- reviewType: 'AIAN-DEF',
- },
- {
- id: faker.datatype.string(),
- statusId: 1,
- reportDeliveryDate: new Date(),
- reviewType: 'AIAN-DEF',
- },
- {
- id: faker.datatype.string(),
- statusId: 1,
- reportDeliveryDate: new Date(),
- reviewType: 'AIAN-DEF',
- },
- ]);
- */
- } catch (error) {
- console.log('\n\n\n============create error', error);
- }
- });
- afterAll(async () => {
- try {
- // Delete MonitoringReviewGrantee.
- await MonitoringReviewGrantee.destroy({
- where: {
- grantNumber: allGrants.map((grant) => grant.number),
+ // Allowed review types:
+ /*
+ 'AIAN-DEF',
+ 'RAN',
+ 'Follow-up',
+ 'FA-1', 'FA1-FR',
+ 'FA-2', 'FA2-CR',
+ 'Special'
+ */
+ await MonitoringReview.bulkCreate([
+ {
+ // 1
+ reviewId: grantThatNeedsMonitoringGoalNumberReviewId1,
+ contentId: faker.datatype.uuid(),
+ statusId: statusId1,
+ name: faker.random.words(3),
+ startDate: new Date(),
+ endDate: new Date(),
+ reviewType: 'AIAN-DEF',
+ reportDeliveryDate: new Date(),
+ reportAttachmentId: faker.datatype.uuid(),
+ outcome: faker.random.words(5),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
},
- });
+ {
+ // 2
+ reviewId: grantThatAlreadyHasMonitoringGoalNumberReviewId2,
+ contentId: faker.datatype.uuid(),
+ statusId: statusId2,
+ name: faker.random.words(3),
+ startDate: new Date(),
+ endDate: new Date(),
+ reviewType: 'Follow-up',
+ reportDeliveryDate: new Date(),
+ reportAttachmentId: faker.datatype.uuid(),
+ outcome: faker.random.words(5),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 3
+ reviewId: grantThatFallsBeforeStartDateNumberReviewId3,
+ contentId: faker.datatype.uuid(),
+ statusId: statusId3,
+ name: faker.random.words(3),
+ startDate: new Date(),
+ endDate: new Date(),
+ reviewType: 'FA-1',
+ // This should test the case of the reportDeliveryDate being before the official start date.
+ reportDeliveryDate: new Date(startingReportDeliveryDate.getTime() - 24 * 60 * 60 * 1000),
+ reportAttachmentId: faker.datatype.uuid(),
+ outcome: faker.random.words(5),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 4
+ reviewId: grantThatFallsAfterCutOffDateNumberReviewId4,
+ contentId: faker.datatype.uuid(),
+ statusId: statusId4,
+ name: faker.random.words(3),
+ startDate: new Date(),
+ endDate: new Date(),
+ reviewType: 'FA1-FR',
+ // This should test the case of the reportDeliveryDate being after the cut off date.
+ reportDeliveryDate: new Date(Date.now() + 24 * 60 * 60 * 1000), // Set to tomorrow's date
+ reportAttachmentId: faker.datatype.uuid(),
+ outcome: faker.random.words(5),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 5
+ reviewId: grantThatIsInactiveNumberReviewId5,
+ contentId: faker.datatype.uuid(),
+ statusId: statusId5,
+ name: faker.random.words(3),
+ startDate: new Date(),
+ endDate: new Date(),
+ reviewType: 'FA1-FR',
+ reportDeliveryDate: new Date(),
+ reportAttachmentId: faker.datatype.uuid(),
+ outcome: faker.random.words(5),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 6
+ reviewId: grantThatsMonitoringReviewStatusIsNotCompleteNumberReviewId6,
+ contentId: faker.datatype.uuid(),
+ statusId: statusId6,
+ name: faker.random.words(3),
+ startDate: new Date(),
+ endDate: new Date(),
+ reviewType: 'FA-2',
+ reportDeliveryDate: new Date(),
+ reportAttachmentId: faker.datatype.uuid(),
+ outcome: faker.random.words(5),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 7
+ reviewId: grantThatsMonitoringFindingStatusIsNotActiveNumberReviewId7,
+ contentId: faker.datatype.uuid(),
+ statusId: statusId7,
+ name: faker.random.words(3),
+ startDate: new Date(),
+ endDate: new Date(),
+ reviewType: 'FA2-CR',
+ reportDeliveryDate: new Date(),
+ reportAttachmentId: faker.datatype.uuid(),
+ outcome: faker.random.words(5),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 8 - This test exclusion of invalid review types.
+ reviewId: grantThatsMonitoringReviewReviewTypeIsNotAllowedNumberReviewId8,
+ contentId: faker.datatype.uuid(),
+ statusId: statusId8,
+ name: faker.random.words(3),
+ startDate: new Date(),
+ endDate: new Date(),
+ reviewType: 'Invalid Review Type',
+ reportDeliveryDate: new Date(),
+ reportAttachmentId: faker.datatype.uuid(),
+ outcome: faker.random.words(5),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 9
+ reviewId: inactiveGrantThatHasBeenReplacedByActiveGrantNumberReviewId9,
+ contentId: faker.datatype.uuid(),
+ statusId: statusId9,
+ name: faker.random.words(3),
+ startDate: new Date(),
+ endDate: new Date(),
+ reviewType: 'Special',
+ reportDeliveryDate: new Date(),
+ reportAttachmentId: faker.datatype.uuid(),
+ outcome: faker.random.words(5),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ ], { individualHooks: true });
- // Delete Grants.
- await Grant.destroy({
- where: {
- number: allGrants.map((grant) => grant.number),
+ // MonitoringReviewStatus.
+ await MonitoringReviewStatus.bulkCreate([
+ {
+ // 1
+ statusId: statusId1,
+ name: 'Complete',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
},
- });
+ {
+ // 2
+ statusId: statusId2,
+ name: 'Complete',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 3
+ statusId: statusId3,
+ name: 'Complete',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 4
+ statusId: statusId4,
+ name: 'Complete',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 5
+ statusId: statusId5,
+ name: 'Complete',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 6 - This test exclusion of invalid review status.
+ statusId: statusId6,
+ name: 'Not Complete',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 7
+ statusId: statusId7,
+ name: 'Complete',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 8
+ statusId: statusId8,
+ name: 'Complete',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 9
+ statusId: statusId9,
+ name: 'Complete',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ ], { individualHooks: true });
+
+ // MonitoringFindingHistory.
+ const findingId1 = uuidv4();
+ const findingId2 = uuidv4();
+ const findingId3 = uuidv4();
+ const findingId4 = uuidv4();
+ const findingId5 = uuidv4();
+ const findingId6 = uuidv4();
+ const findingId7 = uuidv4();
+ const findingId8 = uuidv4();
+ const findingId9 = uuidv4();
+ await MonitoringFindingHistory.bulkCreate([
+ {
+ // 1
+ reviewId: grantThatNeedsMonitoringGoalNumberReviewId1,
+ findingHistoryId: uuidv4(),
+ findingId: findingId1,
+ statusId: statusId1,
+ narrative: faker.random.words(10),
+ ordinal: faker.datatype.number({ min: 1, max: 10 }),
+ determination: faker.random.words(5),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ sourceDeletedAt: null,
+ },
+ {
+ // 2
+ reviewId: grantThatAlreadyHasMonitoringGoalNumberReviewId2,
+ findingHistoryId: uuidv4(),
+ findingId: findingId2,
+ statusId: statusId2,
+ narrative: faker.random.words(10),
+ ordinal: faker.datatype.number({ min: 1, max: 10 }),
+ determination: faker.random.words(5),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ sourceDeletedAt: null,
+ },
+ {
+ // 3
+ reviewId: grantThatFallsBeforeStartDateNumberReviewId3,
+ findingHistoryId: uuidv4(),
+ findingId: findingId3,
+ statusId: statusId3,
+ narrative: faker.random.words(10),
+ ordinal: faker.datatype.number({ min: 1, max: 10 }),
+ determination: faker.random.words(5),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ sourceDeletedAt: null,
+ },
+ {
+ // 4
+ reviewId: grantThatFallsAfterCutOffDateNumberReviewId4,
+ findingHistoryId: uuidv4(),
+ findingId: findingId4,
+ statusId: statusId4,
+ narrative: faker.random.words(10),
+ ordinal: faker.datatype.number({ min: 1, max: 10 }),
+ determination: faker.random.words(5),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ sourceDeletedAt: null,
+ },
+ {
+ // 5
+ reviewId: grantThatIsInactiveNumberReviewId5,
+ findingHistoryId: uuidv4(),
+ findingId: findingId5,
+ statusId: statusId5,
+ narrative: faker.random.words(10),
+ ordinal: faker.datatype.number({ min: 1, max: 10 }),
+ determination: faker.random.words(5),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ sourceDeletedAt: null,
+ },
+ {
+ // 6
+ reviewId: grantThatsMonitoringReviewStatusIsNotCompleteNumberReviewId6,
+ findingHistoryId: uuidv4(),
+ findingId: findingId6,
+ statusId: statusId6,
+ narrative: faker.random.words(10),
+ ordinal: faker.datatype.number({ min: 1, max: 10 }),
+ determination: faker.random.words(5),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ sourceDeletedAt: null,
+ },
+ {
+ // 7
+ reviewId: grantThatsMonitoringFindingStatusIsNotActiveNumberReviewId7,
+ findingHistoryId: uuidv4(),
+ findingId: findingId7,
+ statusId: statusId7,
+ narrative: faker.random.words(10),
+ ordinal: faker.datatype.number({ min: 1, max: 10 }),
+ determination: faker.random.words(5),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ sourceDeletedAt: null,
+ },
+ {
+ // 8
+ reviewId: grantThatsMonitoringReviewReviewTypeIsNotAllowedNumberReviewId8,
+ findingHistoryId: uuidv4(),
+ findingId: findingId8,
+ statusId: statusId8,
+ narrative: faker.random.words(10),
+ ordinal: faker.datatype.number({ min: 1, max: 10 }),
+ determination: faker.random.words(5),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ sourceDeletedAt: null,
+ },
+ {
+ // 9
+ reviewId: inactiveGrantThatHasBeenReplacedByActiveGrantNumberReviewId9,
+ findingHistoryId: uuidv4(),
+ findingId: findingId9,
+ statusId: statusId9,
+ narrative: faker.random.words(10),
+ ordinal: faker.datatype.number({ min: 1, max: 10 }),
+ determination: faker.random.words(5),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ sourceDeletedAt: null,
+ },
+ ], { individualHooks: true });
+
+ // MonitoringFinding.
+ await MonitoringFinding.bulkCreate([
+ {
+ // 1
+ findingId: findingId1,
+ statusId: statusId1,
+ findingType: faker.random.word(),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 2
+ findingId: findingId2,
+ statusId: statusId2,
+ findingType: faker.random.word(),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 3
+ findingId: findingId3,
+ statusId: statusId3,
+ findingType: faker.random.word(),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 4
+ findingId: findingId4,
+ statusId: statusId4,
+ findingType: faker.random.word(),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 5
+ findingId: findingId5,
+ statusId: statusId5,
+ findingType: faker.random.word(),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 6
+ findingId: findingId6,
+ statusId: statusId6,
+ findingType: faker.random.word(),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 7
+ findingId: findingId7,
+ statusId: statusId7,
+ findingType: faker.random.word(),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 8
+ findingId: findingId8,
+ statusId: statusId8,
+ findingType: faker.random.word(),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 9
+ findingId: findingId9,
+ statusId: statusId9,
+ findingType: faker.random.word(),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ ], { individualHooks: true });
+
+ // MonitoringFindingStatus.
+ await MonitoringFindingStatus.bulkCreate([
+ {
+ // 1
+ statusId: statusId1,
+ name: 'Active',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 2
+ statusId: statusId2,
+ name: 'Active',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 3
+ statusId: statusId3,
+ name: 'Active',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 4
+ statusId: statusId4,
+ name: 'Active',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 5
+ statusId: statusId5,
+ name: 'Active',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 6
+ statusId: statusId6,
+ name: 'Active',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 7 - This test exclusion of inactive status.
+ statusId: statusId7,
+ name: 'Not Active',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 8
+ statusId: statusId8,
+ name: 'Active',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 9
+ statusId: statusId9,
+ name: 'Active',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ ], { individualHooks: true });
- // Delete Recipient.
- await Recipient.destroy({
- where: {
- id: recipient.id,
+ // MonitoringFindingGrant.
+ await MonitoringFindingGrant.bulkCreate([
+ {
+ // 1
+ findingId: findingId1,
+ granteeId: grantThatNeedsMonitoringGoalNumberGranteeId1,
+ statusId: statusId1,
+ findingType: faker.random.word(),
+ source: faker.random.word(),
+ correctionDeadLine: new Date(),
+ reportedDate: new Date(),
+ closedDate: null,
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ sourceDeletedAt: null,
+ },
+ {
+ // 2
+ findingId: findingId2,
+ granteeId: grantThatAlreadyHasMonitoringGoalNumberGranteeId2,
+ statusId: statusId2,
+ findingType: faker.random.word(),
+ source: faker.random.word(),
+ correctionDeadLine: new Date(),
+ reportedDate: new Date(),
+ closedDate: null,
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ sourceDeletedAt: null,
+ },
+ {
+ // 3
+ findingId: findingId3,
+ granteeId: grantThatFallsBeforeStartDateNumberGranteeId3,
+ statusId: statusId3,
+ findingType: faker.random.word(),
+ source: faker.random.word(),
+ correctionDeadLine: new Date(),
+ reportedDate: new Date(),
+ closedDate: null,
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ sourceDeletedAt: null,
+ },
+ {
+ // 4
+ findingId: findingId4,
+ granteeId: grantThatFallsAfterCutOffDateNumberGranteeId4,
+ statusId: statusId4,
+ findingType: faker.random.word(),
+ source: faker.random.word(),
+ correctionDeadLine: new Date(),
+ reportedDate: new Date(),
+ closedDate: null,
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ sourceDeletedAt: null,
+ },
+ {
+ // 5
+ findingId: findingId5,
+ granteeId: grantThatIsInactiveNumberGranteeId5,
+ statusId: statusId5,
+ findingType: faker.random.word(),
+ source: faker.random.word(),
+ correctionDeadLine: new Date(),
+ reportedDate: new Date(),
+ closedDate: null,
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ sourceDeletedAt: null,
+ },
+ {
+ // 6
+ findingId: findingId6,
+ granteeId: grantThatsMonitoringReviewStatusIsNotCompleteNumberGranteeId6,
+ statusId: statusId6,
+ findingType: faker.random.word(),
+ source: faker.random.word(),
+ correctionDeadLine: new Date(),
+ reportedDate: new Date(),
+ closedDate: null,
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ sourceDeletedAt: null,
+ },
+ {
+ // 7
+ findingId: findingId7,
+ granteeId: grantThatsMonitoringFindingStatusIsNotActiveNumberGranteeId7,
+ statusId: statusId7,
+ findingType: faker.random.word(),
+ source: faker.random.word(),
+ correctionDeadLine: new Date(),
+ reportedDate: new Date(),
+ closedDate: null,
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ sourceDeletedAt: null,
+ },
+ {
+ // 8
+ findingId: findingId8,
+ granteeId: grantThatsMonitoringReviewReviewTypeIsNotAllowedNumberGranteeId8,
+ statusId: statusId8,
+ findingType: faker.random.word(),
+ source: faker.random.word(),
+ correctionDeadLine: new Date(),
+ reportedDate: new Date(),
+ closedDate: null,
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ sourceDeletedAt: null,
},
+ {
+ // 9
+ findingId: findingId9,
+ granteeId: inactiveGrantThatHasBeenReplacedByActiveGrantNumberGranteeId9,
+ statusId: statusId9,
+ findingType: faker.random.word(),
+ source: faker.random.word(),
+ correctionDeadLine: new Date(),
+ reportedDate: new Date(),
+ closedDate: null,
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ sourceDeletedAt: null,
+ },
+ ], { individualHooks: true });
+
+ // Retrieve the goal template.
+ goalTemplate = await GoalTemplate.findOne({ where: { id: goalTemplateId } });
+
+ // Create a goal for grantThatAlreadyHasMonitoringGoal2.
+ await Goal.create({
+ id: faker.datatype.number({ min: 9999 }),
+ grantId: grantThatAlreadyHasMonitoringGoal2.id,
+ goalTemplateId: goalTemplate.id,
+ status: 'Active',
});
+ } catch (error) {
+ console.log('\n\n\n============create error', error);
+ }
+ });
+ afterAll(async () => {
+ try {
+ await rollbackToSnapshot(snapShot);
// Close the connection to the database.
await sequelize.close();
} catch (error) {
From 9b0a3f94d3399991cfe4107904d2b4c66e989db5 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Sun, 10 Nov 2024 16:25:09 -0500
Subject: [PATCH 003/198] add enum, fix sql, complete tests
---
.../20241110022648-AddMonitoringEnum.js | 19 +
src/tools/createMonitoringGoals.js | 29 +-
src/tools/createMonitoringGoals.test.js | 1860 +++++++++--------
3 files changed, 983 insertions(+), 925 deletions(-)
create mode 100644 src/migrations/20241110022648-AddMonitoringEnum.js
diff --git a/src/migrations/20241110022648-AddMonitoringEnum.js b/src/migrations/20241110022648-AddMonitoringEnum.js
new file mode 100644
index 0000000000..c1131af3a7
--- /dev/null
+++ b/src/migrations/20241110022648-AddMonitoringEnum.js
@@ -0,0 +1,19 @@
+const { GOAL_CREATED_VIA } = require('../constants');
+const { prepMigration } = require('../lib/migration');
+
+/** @type {import('sequelize-cli').Migration} */
+module.exports = {
+ async up(queryInterface) {
+ await queryInterface.sequelize.transaction(async (transaction) => {
+ const sessionSig = __filename;
+ await prepMigration(queryInterface, transaction, sessionSig);
+ return Promise.all(Object.values(GOAL_CREATED_VIA).map((action) => queryInterface.sequelize.query(`
+ ALTER TYPE "enum_Goals_createdVia" ADD VALUE IF NOT EXISTS '${action}';
+ `)));
+ });
+ },
+
+ async down() {
+ // no rollbacks
+ },
+};
diff --git a/src/tools/createMonitoringGoals.js b/src/tools/createMonitoringGoals.js
index ad44894100..07b53b599c 100644
--- a/src/tools/createMonitoringGoals.js
+++ b/src/tools/createMonitoringGoals.js
@@ -2,24 +2,23 @@ import {
sequelize,
GoalTemplate,
} from '../models';
+
import { auditLogger } from '../logger';
const createMonitoringGoals = async () => {
- console.log('\n\n\n----- start of job');
const cutOffDate = '2023-12-01';
- const monitoringGoalTemplateId = 18172;
+ const monitoringTemplateName = '(Monitoring) The recipient will develop and implement a QIP/CAP to address monitoring findings.';
// Verify that the monitoring goal template exists.
const monitoringGoalTemplate = await GoalTemplate.findOne({
where: {
- id: monitoringGoalTemplateId,
+ templateName: monitoringTemplateName,
},
});
- console.log('\n\n\n------monitoringGoalTemplate', monitoringGoalTemplate);
// If the monitoring goal template does not exist, throw an error.
if (!monitoringGoalTemplate) {
- auditLogger.error(`Monitoring Goal template with ID ${monitoringGoalTemplateId} not found`);
+ auditLogger.error('Monitoring Goal template not found');
return;
}
@@ -51,7 +50,7 @@ const createMonitoringGoals = async () => {
LEFT JOIN "Goals" g
ON (grta."grantId" = g."grantId"
OR grta."activeGrantId" = g."grantId")
- AND g."goalTemplateId" = ${monitoringGoalTemplateId} -- NEEDS TO BE CHANGED TO THE MONITORING GOAL
+ AND g."goalTemplateId" = ${monitoringGoalTemplate.id}
WHERE gr.status = 'Active'
AND mrs."name" = 'Complete'
AND mfs."name" = 'Active'
@@ -72,28 +71,24 @@ const createMonitoringGoals = async () => {
gt."templateName" "name",
'Not started' "status",
NULL "timeframe",
- FALSE "isFromSmartSheetTtaPlan",
+ FALSE "isFromSmartsheetTtaPlan",
NOW() "createdAt",
NOW() "updatedAt",
- NULL "endDate",
gt.id "goalTemplateId",
gng."grantId" "grantId",
FALSE "onApprovedAR",
- 'monitoring' "createdVIA",
- FALSE "isRttapa",
+ 'monitoring'::"enum_Goals_createdVia" "createdVia",
+ 'Yes'::"enum_Goals_isRttapa" "isRttapa",
FALSE "onAR",
- NULL "rtrOrder",
- 'Federal monitoring issues, including CLASS and RANs' "source",
- NULL "deletedAt",
- NULL "mapsToParrentGoalId"
+ 'Federal monitoring issues, including CLASS and RANs'::"enum_Goals_source" "source"
FROM "GoalTemplates" gt
CROSS JOIN grants_needing_goal gng
- WHERE gt.id = 18172 -- NEEDS TO BE CHANGED TO THE MONITORING GOAL
+ WHERE gt.id = ${monitoringGoalTemplate.id}
)
INSERT INTO "Goals"
- ("name", "status", "timeframe", "isFromSmartSheetTtaPlan", "createdAt", "updatedAt", "endDate", "goalTemplateId", "grantId", "onApprovedAR", "createdVIA", "isRttapa", "onAR", "rtrOrder", "source", "deletedAt", "mapsToParrentGoalId")
+ ("name", "status", "timeframe", "isFromSmartsheetTtaPlan", "createdAt", "updatedAt", "goalTemplateId", "grantId", "onApprovedAR", "createdVia", "isRttapa", "onAR", "source")
SELECT
- "name", "status", "timeframe", "isFromSmartSheetTtaPlan", "createdAt", "updatedAt", "endDate", "goalTemplateId", "grantId", "onApprovedAR", "createdVIA", "isRttapa", "onAR", "rtrOrder", "source", "deletedAt", "mapsToParrentGoalId"
+ "name", "status", "timeframe", "isFromSmartsheetTtaPlan", "createdAt", "updatedAt", "goalTemplateId", "grantId", "onApprovedAR", "createdVia", "isRttapa", "onAR", "source"
FROM new_goals;
`);
};
diff --git a/src/tools/createMonitoringGoals.test.js b/src/tools/createMonitoringGoals.test.js
index 7d6592a803..ba4a569781 100644
--- a/src/tools/createMonitoringGoals.test.js
+++ b/src/tools/createMonitoringGoals.test.js
@@ -14,7 +14,6 @@ import {
MonitoringFinding,
MonitoringFindingStatus,
MonitoringFindingGrant,
- GrantReplacement,
Goal,
} from '../models';
import { captureSnapshot, rollbackToSnapshot } from '../lib/programmaticTransaction';
@@ -24,11 +23,10 @@ jest.mock('../logger');
describe('createMonitoringGoals', () => {
let recipient;
- let allGrants;
-
const startingReportDeliveryDate = new Date('2023-12-01');
- const goalTemplateId = 18172;
+ // const goalTemplateId = 18172;
+ const goalTemplateName = '(Monitoring) The recipient will develop and implement a QIP/CAP to address monitoring findings.';
let goalTemplate;
let grantThatNeedsMonitoringGoal1;
@@ -52,300 +50,296 @@ describe('createMonitoringGoals', () => {
const inactiveGrantThatHasBeenReplacedByActiveGrantNumber9 = faker.datatype.string(4);
let snapShot;
- beforeEach(() => {
- jest.clearAllMocks();
- });
beforeAll(async () => {
- try {
- // Create a snapshot of the database so we can rollback after the tests.
- snapShot = await captureSnapshot();
- // Recipient.
- recipient = await Recipient.create({
- id: faker.datatype.number({ min: 64000 }),
- name: faker.random.alphaNumeric(6),
- });
-
- // Grants.
- const grants = await Grant.bulkCreate([
- {
- // 1
- id: faker.datatype.number({ min: 9999 }),
- number: grantThatNeedsMonitoringGoalNumber1,
- recipientId: recipient.id,
- regionId: 1,
- startDate: new Date(),
- endDate: new Date(),
- status: 'Active',
- },
- {
- // 2
- id: faker.datatype.number({ min: 9999 }),
- number: grantThatAlreadyHasMonitoringGoalNumber2,
- recipientId: recipient.id,
- regionId: 1,
- startDate: new Date(),
- endDate: new Date(),
- status: 'Active',
- },
- {
- // 3
- id: faker.datatype.number({ min: 9999 }),
- number: grantThatFallsBeforeStartDateNumber3,
- recipientId: recipient.id,
- regionId: 1,
- startDate: new Date('2021-01-01'),
- endDate: new Date('2021-12-31'),
- status: 'Active',
- },
- {
- // 4
- id: faker.datatype.number({ min: 9999 }),
- number: grantThatFallsAfterCutOffDateNumber4,
- recipientId: recipient.id,
- regionId: 1,
- startDate: new Date('2023-12-02'),
- endDate: new Date('2024-12-01'),
- status: 'Active',
- },
- {
- // 5
- id: faker.datatype.number({ min: 9999 }),
- number: grantThatIsInactiveNumber5,
- recipientId: recipient.id,
- regionId: 1,
- startDate: new Date(),
- endDate: new Date(),
- status: 'Inactive',
- },
- {
- // 6
- id: faker.datatype.number({ min: 9999 }),
- number: grantThatsMonitoringReviewStatusIsNotCompleteNumber6,
- recipientId: recipient.id,
- regionId: 1,
- startDate: new Date(),
- endDate: new Date(),
- status: 'Active',
- },
- {
- // 7
- id: faker.datatype.number({ min: 9999 }),
- number: grantThatsMonitoringFindingStatusIsNotActiveNumber7,
- recipientId: recipient.id,
- regionId: 1,
- startDate: new Date(),
- endDate: new Date(),
- status: 'Active',
- },
- {
- // 8
- id: faker.datatype.number({ min: 9999 }),
- number: grantThatsMonitoringReviewReviewTypeIsNotAllowedNumber8,
- recipientId: recipient.id,
- regionId: 1,
- startDate: new Date(),
- endDate: new Date(),
- status: 'Active',
- },
- {
- // 9
- id: faker.datatype.number({ min: 9999 }),
- number: inactiveGrantThatHasBeenReplacedByActiveGrantNumber9,
- recipientId: recipient.id,
- regionId: 1,
- startDate: new Date(),
- endDate: new Date(),
- status: 'Active',
- },
- ]);
+ // Create a snapshot of the database so we can rollback after the tests.
+ snapShot = await captureSnapshot();
+ // Recipient.
+ recipient = await Recipient.create({
+ id: faker.datatype.number({ min: 64000 }),
+ name: faker.random.alphaNumeric(6),
+ });
- [
- grantThatNeedsMonitoringGoal1,
- grantThatAlreadyHasMonitoringGoal2,
- grantThatFallsBeforeStartDate3,
- grantThatFallsAfterCutOffDate4,
- grantThatIsInactive5,
- grantThatsMonitoringReviewStatusIsNotComplete6,
- grantThatsMonitoringFindingStatusIsNotActive7,
- grantThatsMonitoringReviewReviewTypeIsNotAllowed8,
- inactiveGrantThatHasBeenReplacedByActiveGrant9,
- ] = grants;
-
- // Create an inactive grant that has 'cdi' true that points to inactiveGrantThatHasBeenReplacedByActiveGrant9.
- const cdiGrant = await Grant.create({
+ // Grants.
+ const grants = await Grant.bulkCreate([
+ {
+ // 1
+ id: faker.datatype.number({ min: 9999 }),
+ number: grantThatNeedsMonitoringGoalNumber1,
+ recipientId: recipient.id,
+ regionId: 1,
+ startDate: new Date(),
+ endDate: new Date(),
+ status: 'Active',
+ },
+ {
+ // 2
+ id: faker.datatype.number({ min: 9999 }),
+ number: grantThatAlreadyHasMonitoringGoalNumber2,
+ recipientId: recipient.id,
+ regionId: 1,
+ startDate: new Date(),
+ endDate: new Date(),
+ status: 'Active',
+ },
+ {
+ // 3
+ id: faker.datatype.number({ min: 9999 }),
+ number: grantThatFallsBeforeStartDateNumber3,
+ recipientId: recipient.id,
+ regionId: 1,
+ startDate: new Date('2021-01-01'),
+ endDate: new Date('2021-12-31'),
+ status: 'Active',
+ },
+ {
+ // 4
+ id: faker.datatype.number({ min: 9999 }),
+ number: grantThatFallsAfterCutOffDateNumber4,
+ recipientId: recipient.id,
+ regionId: 1,
+ startDate: new Date('2023-12-02'),
+ endDate: new Date('2024-12-01'),
+ status: 'Active',
+ },
+ {
+ // 5
id: faker.datatype.number({ min: 9999 }),
- number: faker.datatype.string(4),
+ number: grantThatIsInactiveNumber5,
recipientId: recipient.id,
regionId: 1,
- startDate: new Date(Date.now() - 24 * 60 * 60 * 1000), // Set to yesterday's date
- endDate: new Date(Date.now() - 24 * 60 * 60 * 1000), // Set to yesterday's date
+ startDate: new Date(),
+ endDate: new Date(),
status: 'Inactive',
- cdi: true,
- });
+ },
+ {
+ // 6
+ id: faker.datatype.number({ min: 9999 }),
+ number: grantThatsMonitoringReviewStatusIsNotCompleteNumber6,
+ recipientId: recipient.id,
+ regionId: 1,
+ startDate: new Date(),
+ endDate: new Date(),
+ status: 'Active',
+ },
+ {
+ // 7
+ id: faker.datatype.number({ min: 9999 }),
+ number: grantThatsMonitoringFindingStatusIsNotActiveNumber7,
+ recipientId: recipient.id,
+ regionId: 1,
+ startDate: new Date(),
+ endDate: new Date(),
+ status: 'Active',
+ },
+ {
+ // 8
+ id: faker.datatype.number({ min: 9999 }),
+ number: grantThatsMonitoringReviewReviewTypeIsNotAllowedNumber8,
+ recipientId: recipient.id,
+ regionId: 1,
+ startDate: new Date(),
+ endDate: new Date(),
+ status: 'Active',
+ },
+ {
+ // 9
+ id: faker.datatype.number({ min: 9999 }),
+ number: inactiveGrantThatHasBeenReplacedByActiveGrantNumber9,
+ recipientId: recipient.id,
+ regionId: 1,
+ startDate: new Date(),
+ endDate: new Date(),
+ status: 'Active',
+ },
+ ]);
- // Insert into GrantReplacement to show that cdiGrant was replaced by inactiveGrantThatHasBeenReplacedByActiveGrant9.
- await GrantReplacement.create({
- replacedGrantId: cdiGrant.id,
- replacingGrantId: inactiveGrantThatHasBeenReplacedByActiveGrant9.id,
- grantReplacementTypeId: faker.datatype.number({ min: 1, max: 10 }),
- replacementDate: new Date(),
- createdAt: new Date(),
- updatedAt: new Date(),
- });
+ [
+ grantThatNeedsMonitoringGoal1,
+ grantThatAlreadyHasMonitoringGoal2,
+ grantThatFallsBeforeStartDate3,
+ grantThatFallsAfterCutOffDate4,
+ grantThatIsInactive5,
+ grantThatsMonitoringReviewStatusIsNotComplete6,
+ grantThatsMonitoringFindingStatusIsNotActive7,
+ grantThatsMonitoringReviewReviewTypeIsNotAllowed8,
+ inactiveGrantThatHasBeenReplacedByActiveGrant9,
+ ] = grants;
- // Set allGrants to the grants that were created.
- allGrants = grants;
+ // Create an inactive grant that has 'cdi' true that points to inactiveGrantThatHasBeenReplacedByActiveGrant9.
+ const cdiGrant = await Grant.create({
+ id: faker.datatype.number({ min: 9999 }),
+ number: faker.datatype.string(4),
+ recipientId: recipient.id,
+ regionId: 1,
+ startDate: new Date(Date.now() - 24 * 60 * 60 * 1000), // Set to yesterday's date
+ endDate: new Date(Date.now() - 24 * 60 * 60 * 1000), // Set to yesterday's date
+ status: 'Inactive',
+ cdi: true,
+ });
- // granteeId GUID.
- const grantThatNeedsMonitoringGoalNumberGranteeId1 = uuidv4();
- const grantThatAlreadyHasMonitoringGoalNumberGranteeId2 = uuidv4();
- const grantThatFallsBeforeStartDateNumberGranteeId3 = uuidv4();
- const grantThatFallsAfterCutOffDateNumberGranteeId4 = uuidv4();
- const grantThatIsInactiveNumberGranteeId5 = uuidv4();
- const grantThatsMonitoringReviewStatusIsNotCompleteNumberGranteeId6 = uuidv4();
- const grantThatsMonitoringFindingStatusIsNotActiveNumberGranteeId7 = uuidv4();
- const grantThatsMonitoringReviewReviewTypeIsNotAllowedNumberGranteeId8 = uuidv4();
- const inactiveGrantThatHasBeenReplacedByActiveGrantNumberGranteeId9 = uuidv4();
+ // Create a grant replacement type.
+ await sequelize.query(`
+ INSERT INTO "GrantReplacementTypes" ("name", "createdAt", "updatedAt", "deletedAt", "mapsTo")
+ VALUES ('Replace CDI Grant', NOW(), NOW(), NULL, NULL)
+ `);
- // reviewId GUID.
- const grantThatNeedsMonitoringGoalNumberReviewId1 = uuidv4();
- const grantThatAlreadyHasMonitoringGoalNumberReviewId2 = uuidv4();
- const grantThatFallsBeforeStartDateNumberReviewId3 = uuidv4();
- const grantThatFallsAfterCutOffDateNumberReviewId4 = uuidv4();
- const grantThatIsInactiveNumberReviewId5 = uuidv4();
- const grantThatsMonitoringReviewStatusIsNotCompleteNumberReviewId6 = uuidv4();
- const grantThatsMonitoringFindingStatusIsNotActiveNumberReviewId7 = uuidv4();
- const grantThatsMonitoringReviewReviewTypeIsNotAllowedNumberReviewId8 = uuidv4();
- const inactiveGrantThatHasBeenReplacedByActiveGrantNumberReviewId9 = uuidv4();
+ // Get the Grant Replacement Type where the name = 'Replace CDI Grant'.
+ const grantReplacementType = await sequelize.query(`
+ SELECT id FROM "GrantReplacementTypes" WHERE name = 'Replace CDI Grant'
+ `, { type: sequelize.QueryTypes.SELECT });
- // MonitoringReviewGrantee.
- await MonitoringReviewGrantee.bulkCreate([
- {
- // 1
- id: faker.datatype.number({ min: 9999 }),
- grantNumber: grantThatNeedsMonitoringGoalNumber1,
- reviewId: grantThatNeedsMonitoringGoalNumberReviewId1,
- granteeId: grantThatNeedsMonitoringGoalNumberGranteeId1,
- createTime: new Date(),
- updateTime: new Date(),
- updateBy: 'Support Team',
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- {
- // 2
- id: faker.datatype.number({ min: 9999 }),
- grantNumber: grantThatAlreadyHasMonitoringGoalNumber2,
- reviewId: grantThatAlreadyHasMonitoringGoalNumberReviewId2,
- granteeId: grantThatAlreadyHasMonitoringGoalNumberGranteeId2,
- createTime: new Date(),
- updateTime: new Date(),
- updateBy: 'Support Team',
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- {
- // 3
- id: faker.datatype.number({ min: 9999 }),
- grantNumber: grantThatFallsBeforeStartDateNumber3,
- reviewId: grantThatFallsBeforeStartDateNumberReviewId3,
- granteeId: grantThatFallsBeforeStartDateNumberGranteeId3,
- createTime: new Date(),
- updateTime: new Date(),
- updateBy: 'Support Team',
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- {
- // 4
- id: faker.datatype.number({ min: 9999 }),
- grantNumber: grantThatFallsAfterCutOffDateNumber4,
- reviewId: grantThatFallsAfterCutOffDateNumberReviewId4,
- granteeId: grantThatFallsAfterCutOffDateNumberGranteeId4,
- createTime: new Date(),
- updateTime: new Date(),
- updateBy: 'Support Team',
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- {
- // 5
- id: faker.datatype.number({ min: 9999 }),
- grantNumber: grantThatIsInactiveNumber5,
- reviewId: grantThatIsInactiveNumberReviewId5,
- granteeId: grantThatIsInactiveNumberGranteeId5,
- createTime: new Date(),
- updateTime: new Date(),
- updateBy: 'Support Team',
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- {
- // 6
- id: faker.datatype.number({ min: 9999 }),
- grantNumber: grantThatsMonitoringReviewStatusIsNotCompleteNumber6,
- reviewId: grantThatsMonitoringReviewStatusIsNotCompleteNumberReviewId6,
- granteeId: grantThatsMonitoringReviewStatusIsNotCompleteNumberGranteeId6,
- createTime: new Date(),
- updateTime: new Date(),
- updateBy: 'Support Team',
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- {
- // 7
- id: faker.datatype.number({ min: 9999 }),
- grantNumber: grantThatsMonitoringFindingStatusIsNotActiveNumber7,
- reviewId: grantThatsMonitoringFindingStatusIsNotActiveNumberReviewId7,
- granteeId: grantThatsMonitoringFindingStatusIsNotActiveNumberGranteeId7,
- createTime: new Date(),
- updateTime: new Date(),
- updateBy: 'Support Team',
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- {
- // 8
- id: faker.datatype.number({ min: 9999 }),
- grantNumber: grantThatsMonitoringReviewReviewTypeIsNotAllowedNumber8,
- reviewId: grantThatsMonitoringReviewReviewTypeIsNotAllowedNumberReviewId8,
- granteeId: grantThatsMonitoringReviewReviewTypeIsNotAllowedNumberGranteeId8,
- createTime: new Date(),
- updateTime: new Date(),
- updateBy: 'Support Team',
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- {
- // 9
- id: faker.datatype.number({ min: 9999 }),
- grantNumber: inactiveGrantThatHasBeenReplacedByActiveGrant9.number,
- reviewId: inactiveGrantThatHasBeenReplacedByActiveGrantNumberReviewId9,
- granteeId: inactiveGrantThatHasBeenReplacedByActiveGrantNumberGranteeId9,
- createTime: new Date(),
- updateTime: new Date(),
- updateBy: 'Support Team',
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- ], { individualHooks: true });
+ // Create a fake grant replacement.
+ await sequelize.query(`
+ INSERT INTO "GrantReplacements" ("replacedGrantId", "replacingGrantId", "grantReplacementTypeId", "replacementDate", "createdAt", "updatedAt")
+ VALUES (${cdiGrant.id}, ${inactiveGrantThatHasBeenReplacedByActiveGrant9.id}, ${grantReplacementType[0].id}, NOW(), NOW(), NOW())
+ `);
- // Create 9 statusId variables made up of 6 random numbers and set each one to its corresponding statusId below in MonitoringReview.bulkCreate.
- const statusId1 = faker.datatype.number({ min: 1, max: 9 });
- const statusId2 = faker.datatype.number({ min: 1, max: 9 });
- const statusId3 = faker.datatype.number({ min: 1, max: 9 });
- const statusId4 = faker.datatype.number({ min: 1, max: 9 });
- const statusId5 = faker.datatype.number({ min: 1, max: 9 });
- const statusId6 = faker.datatype.number({ min: 1, max: 9 });
- const statusId7 = faker.datatype.number({ min: 1, max: 9 });
- const statusId8 = faker.datatype.number({ min: 1, max: 9 });
- const statusId9 = faker.datatype.number({ min: 1, max: 9 });
+ // granteeId GUID.
+ const grantThatNeedsMonitoringGoalNumberGranteeId1 = uuidv4();
+ const grantThatAlreadyHasMonitoringGoalNumberGranteeId2 = uuidv4();
+ const grantThatFallsBeforeStartDateNumberGranteeId3 = uuidv4();
+ const grantThatFallsAfterCutOffDateNumberGranteeId4 = uuidv4();
+ const grantThatIsInactiveNumberGranteeId5 = uuidv4();
+ const grantThatsMonitoringReviewStatusIsNotCompleteNumberGranteeId6 = uuidv4();
+ const grantThatsMonitoringFindingStatusIsNotActiveNumberGranteeId7 = uuidv4();
+ const grantThatsMonitoringReviewReviewTypeIsNotAllowedNumberGranteeId8 = uuidv4();
+ const inactiveGrantThatHasBeenReplacedByActiveGrantNumberGranteeId9 = uuidv4();
- // MonitoringReview.
+ // reviewId GUID.
+ const grantThatNeedsMonitoringGoalNumberReviewId1 = uuidv4();
+ const grantThatAlreadyHasMonitoringGoalNumberReviewId2 = uuidv4();
+ const grantThatFallsBeforeStartDateNumberReviewId3 = uuidv4();
+ const grantThatFallsAfterCutOffDateNumberReviewId4 = uuidv4();
+ const grantThatIsInactiveNumberReviewId5 = uuidv4();
+ const grantThatsMonitoringReviewStatusIsNotCompleteNumberReviewId6 = uuidv4();
+ const grantThatsMonitoringFindingStatusIsNotActiveNumberReviewId7 = uuidv4();
+ const grantThatsMonitoringReviewReviewTypeIsNotAllowedNumberReviewId8 = uuidv4();
+ const inactiveGrantThatHasBeenReplacedByActiveGrantNumberReviewId9 = uuidv4();
- // Allowed review types:
- /*
+ // MonitoringReviewGrantee.
+ await MonitoringReviewGrantee.bulkCreate([
+ {
+ // 1
+ id: faker.datatype.number({ min: 9999 }),
+ grantNumber: grantThatNeedsMonitoringGoalNumber1,
+ reviewId: grantThatNeedsMonitoringGoalNumberReviewId1,
+ granteeId: grantThatNeedsMonitoringGoalNumberGranteeId1,
+ createTime: new Date(),
+ updateTime: new Date(),
+ updateBy: 'Support Team',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 2
+ id: faker.datatype.number({ min: 9999 }),
+ grantNumber: grantThatAlreadyHasMonitoringGoalNumber2,
+ reviewId: grantThatAlreadyHasMonitoringGoalNumberReviewId2,
+ granteeId: grantThatAlreadyHasMonitoringGoalNumberGranteeId2,
+ createTime: new Date(),
+ updateTime: new Date(),
+ updateBy: 'Support Team',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 3
+ id: faker.datatype.number({ min: 9999 }),
+ grantNumber: grantThatFallsBeforeStartDateNumber3,
+ reviewId: grantThatFallsBeforeStartDateNumberReviewId3,
+ granteeId: grantThatFallsBeforeStartDateNumberGranteeId3,
+ createTime: new Date(),
+ updateTime: new Date(),
+ updateBy: 'Support Team',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 4
+ id: faker.datatype.number({ min: 9999 }),
+ grantNumber: grantThatFallsAfterCutOffDateNumber4,
+ reviewId: grantThatFallsAfterCutOffDateNumberReviewId4,
+ granteeId: grantThatFallsAfterCutOffDateNumberGranteeId4,
+ createTime: new Date(),
+ updateTime: new Date(),
+ updateBy: 'Support Team',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 5
+ id: faker.datatype.number({ min: 9999 }),
+ grantNumber: grantThatIsInactiveNumber5,
+ reviewId: grantThatIsInactiveNumberReviewId5,
+ granteeId: grantThatIsInactiveNumberGranteeId5,
+ createTime: new Date(),
+ updateTime: new Date(),
+ updateBy: 'Support Team',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 6
+ id: faker.datatype.number({ min: 9999 }),
+ grantNumber: grantThatsMonitoringReviewStatusIsNotCompleteNumber6,
+ reviewId: grantThatsMonitoringReviewStatusIsNotCompleteNumberReviewId6,
+ granteeId: grantThatsMonitoringReviewStatusIsNotCompleteNumberGranteeId6,
+ createTime: new Date(),
+ updateTime: new Date(),
+ updateBy: 'Support Team',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 7
+ id: faker.datatype.number({ min: 9999 }),
+ grantNumber: grantThatsMonitoringFindingStatusIsNotActiveNumber7,
+ reviewId: grantThatsMonitoringFindingStatusIsNotActiveNumberReviewId7,
+ granteeId: grantThatsMonitoringFindingStatusIsNotActiveNumberGranteeId7,
+ createTime: new Date(),
+ updateTime: new Date(),
+ updateBy: 'Support Team',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 8
+ id: faker.datatype.number({ min: 9999 }),
+ grantNumber: grantThatsMonitoringReviewReviewTypeIsNotAllowedNumber8,
+ reviewId: grantThatsMonitoringReviewReviewTypeIsNotAllowedNumberReviewId8,
+ granteeId: grantThatsMonitoringReviewReviewTypeIsNotAllowedNumberGranteeId8,
+ createTime: new Date(),
+ updateTime: new Date(),
+ updateBy: 'Support Team',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 9
+ id: faker.datatype.number({ min: 9999 }),
+ grantNumber: inactiveGrantThatHasBeenReplacedByActiveGrantNumber9,
+ reviewId: inactiveGrantThatHasBeenReplacedByActiveGrantNumberReviewId9,
+ granteeId: inactiveGrantThatHasBeenReplacedByActiveGrantNumberGranteeId9,
+ createTime: new Date(),
+ updateTime: new Date(),
+ updateBy: 'Support Team',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ ], { individualHooks: true });
+
+ // Create 9 statusId variables made up of 6 random numbers and set each one to its corresponding statusId below in MonitoringReview.bulkCreate.
+ const statusIds = new Set();
+ while (statusIds.size < 9) {
+ statusIds.add(faker.datatype.number({ min: 1, max: 9 }));
+ }
+ const [statusId1, statusId2, statusId3, statusId4, statusId5, statusId6, statusId7, statusId8, statusId9] = [...statusIds];
+
+ // MonitoringReview.
+
+ // Allowed review types:
+ /*
'AIAN-DEF',
'RAN',
'Follow-up',
@@ -353,686 +347,736 @@ describe('createMonitoringGoals', () => {
'FA-2', 'FA2-CR',
'Special'
*/
- await MonitoringReview.bulkCreate([
- {
+ await MonitoringReview.bulkCreate([
+ {
// 1
- reviewId: grantThatNeedsMonitoringGoalNumberReviewId1,
- contentId: faker.datatype.uuid(),
- statusId: statusId1,
- name: faker.random.words(3),
- startDate: new Date(),
- endDate: new Date(),
- reviewType: 'AIAN-DEF',
- reportDeliveryDate: new Date(),
- reportAttachmentId: faker.datatype.uuid(),
- outcome: faker.random.words(5),
- hash: faker.datatype.uuid(),
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- {
+ reviewId: grantThatNeedsMonitoringGoalNumberReviewId1,
+ contentId: faker.datatype.uuid(),
+ statusId: statusId1,
+ name: faker.random.words(3),
+ startDate: new Date(),
+ endDate: new Date(),
+ reviewType: 'AIAN-DEF',
+ reportDeliveryDate: new Date(),
+ reportAttachmentId: faker.datatype.uuid(),
+ outcome: faker.random.words(5),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
// 2
- reviewId: grantThatAlreadyHasMonitoringGoalNumberReviewId2,
- contentId: faker.datatype.uuid(),
- statusId: statusId2,
- name: faker.random.words(3),
- startDate: new Date(),
- endDate: new Date(),
- reviewType: 'Follow-up',
- reportDeliveryDate: new Date(),
- reportAttachmentId: faker.datatype.uuid(),
- outcome: faker.random.words(5),
- hash: faker.datatype.uuid(),
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- {
+ reviewId: grantThatAlreadyHasMonitoringGoalNumberReviewId2,
+ contentId: faker.datatype.uuid(),
+ statusId: statusId2,
+ name: faker.random.words(3),
+ startDate: new Date(),
+ endDate: new Date(),
+ reviewType: 'Follow-up',
+ reportDeliveryDate: new Date(),
+ reportAttachmentId: faker.datatype.uuid(),
+ outcome: faker.random.words(5),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
// 3
- reviewId: grantThatFallsBeforeStartDateNumberReviewId3,
- contentId: faker.datatype.uuid(),
- statusId: statusId3,
- name: faker.random.words(3),
- startDate: new Date(),
- endDate: new Date(),
- reviewType: 'FA-1',
- // This should test the case of the reportDeliveryDate being before the official start date.
- reportDeliveryDate: new Date(startingReportDeliveryDate.getTime() - 24 * 60 * 60 * 1000),
- reportAttachmentId: faker.datatype.uuid(),
- outcome: faker.random.words(5),
- hash: faker.datatype.uuid(),
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- {
+ reviewId: grantThatFallsBeforeStartDateNumberReviewId3,
+ contentId: faker.datatype.uuid(),
+ statusId: statusId3,
+ name: faker.random.words(3),
+ startDate: new Date(),
+ endDate: new Date(),
+ reviewType: 'FA-1',
+ // This should test the case of the reportDeliveryDate being before the official start date.
+ reportDeliveryDate: new Date(startingReportDeliveryDate.getTime() - 24 * 60 * 60 * 1000),
+ reportAttachmentId: faker.datatype.uuid(),
+ outcome: faker.random.words(5),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
// 4
- reviewId: grantThatFallsAfterCutOffDateNumberReviewId4,
- contentId: faker.datatype.uuid(),
- statusId: statusId4,
- name: faker.random.words(3),
- startDate: new Date(),
- endDate: new Date(),
- reviewType: 'FA1-FR',
- // This should test the case of the reportDeliveryDate being after the cut off date.
- reportDeliveryDate: new Date(Date.now() + 24 * 60 * 60 * 1000), // Set to tomorrow's date
- reportAttachmentId: faker.datatype.uuid(),
- outcome: faker.random.words(5),
- hash: faker.datatype.uuid(),
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- {
+ reviewId: grantThatFallsAfterCutOffDateNumberReviewId4,
+ contentId: faker.datatype.uuid(),
+ statusId: statusId4,
+ name: faker.random.words(3),
+ startDate: new Date(),
+ endDate: new Date(),
+ reviewType: 'FA1-FR',
+ // This should test the case of the reportDeliveryDate being after the cut off date.
+ reportDeliveryDate: new Date(Date.now() + 24 * 60 * 60 * 1000), // Set to tomorrow's date
+ reportAttachmentId: faker.datatype.uuid(),
+ outcome: faker.random.words(5),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
// 5
- reviewId: grantThatIsInactiveNumberReviewId5,
- contentId: faker.datatype.uuid(),
- statusId: statusId5,
- name: faker.random.words(3),
- startDate: new Date(),
- endDate: new Date(),
- reviewType: 'FA1-FR',
- reportDeliveryDate: new Date(),
- reportAttachmentId: faker.datatype.uuid(),
- outcome: faker.random.words(5),
- hash: faker.datatype.uuid(),
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- {
+ reviewId: grantThatIsInactiveNumberReviewId5,
+ contentId: faker.datatype.uuid(),
+ statusId: statusId5,
+ name: faker.random.words(3),
+ startDate: new Date(),
+ endDate: new Date(),
+ reviewType: 'FA1-FR',
+ reportDeliveryDate: new Date(),
+ reportAttachmentId: faker.datatype.uuid(),
+ outcome: faker.random.words(5),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
// 6
- reviewId: grantThatsMonitoringReviewStatusIsNotCompleteNumberReviewId6,
- contentId: faker.datatype.uuid(),
- statusId: statusId6,
- name: faker.random.words(3),
- startDate: new Date(),
- endDate: new Date(),
- reviewType: 'FA-2',
- reportDeliveryDate: new Date(),
- reportAttachmentId: faker.datatype.uuid(),
- outcome: faker.random.words(5),
- hash: faker.datatype.uuid(),
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- {
+ reviewId: grantThatsMonitoringReviewStatusIsNotCompleteNumberReviewId6,
+ contentId: faker.datatype.uuid(),
+ statusId: statusId6,
+ name: faker.random.words(3),
+ startDate: new Date(),
+ endDate: new Date(),
+ reviewType: 'FA-2',
+ reportDeliveryDate: new Date(),
+ reportAttachmentId: faker.datatype.uuid(),
+ outcome: faker.random.words(5),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
// 7
- reviewId: grantThatsMonitoringFindingStatusIsNotActiveNumberReviewId7,
- contentId: faker.datatype.uuid(),
- statusId: statusId7,
- name: faker.random.words(3),
- startDate: new Date(),
- endDate: new Date(),
- reviewType: 'FA2-CR',
- reportDeliveryDate: new Date(),
- reportAttachmentId: faker.datatype.uuid(),
- outcome: faker.random.words(5),
- hash: faker.datatype.uuid(),
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- {
+ reviewId: grantThatsMonitoringFindingStatusIsNotActiveNumberReviewId7,
+ contentId: faker.datatype.uuid(),
+ statusId: statusId7,
+ name: faker.random.words(3),
+ startDate: new Date(),
+ endDate: new Date(),
+ reviewType: 'FA2-CR',
+ reportDeliveryDate: new Date(),
+ reportAttachmentId: faker.datatype.uuid(),
+ outcome: faker.random.words(5),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
// 8 - This test exclusion of invalid review types.
- reviewId: grantThatsMonitoringReviewReviewTypeIsNotAllowedNumberReviewId8,
- contentId: faker.datatype.uuid(),
- statusId: statusId8,
- name: faker.random.words(3),
- startDate: new Date(),
- endDate: new Date(),
- reviewType: 'Invalid Review Type',
- reportDeliveryDate: new Date(),
- reportAttachmentId: faker.datatype.uuid(),
- outcome: faker.random.words(5),
- hash: faker.datatype.uuid(),
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- {
+ reviewId: grantThatsMonitoringReviewReviewTypeIsNotAllowedNumberReviewId8,
+ contentId: faker.datatype.uuid(),
+ statusId: statusId8,
+ name: faker.random.words(3),
+ startDate: new Date(),
+ endDate: new Date(),
+ reviewType: 'Invalid Review Type',
+ reportDeliveryDate: new Date(),
+ reportAttachmentId: faker.datatype.uuid(),
+ outcome: faker.random.words(5),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
// 9
- reviewId: inactiveGrantThatHasBeenReplacedByActiveGrantNumberReviewId9,
- contentId: faker.datatype.uuid(),
- statusId: statusId9,
- name: faker.random.words(3),
- startDate: new Date(),
- endDate: new Date(),
- reviewType: 'Special',
- reportDeliveryDate: new Date(),
- reportAttachmentId: faker.datatype.uuid(),
- outcome: faker.random.words(5),
- hash: faker.datatype.uuid(),
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- ], { individualHooks: true });
+ reviewId: inactiveGrantThatHasBeenReplacedByActiveGrantNumberReviewId9,
+ contentId: faker.datatype.uuid(),
+ statusId: statusId9,
+ name: faker.random.words(3),
+ startDate: new Date(),
+ endDate: new Date(),
+ reviewType: 'Special',
+ reportDeliveryDate: new Date(),
+ reportAttachmentId: faker.datatype.uuid(),
+ outcome: faker.random.words(5),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ ], { individualHooks: true });
- // MonitoringReviewStatus.
- await MonitoringReviewStatus.bulkCreate([
- {
- // 1
- statusId: statusId1,
- name: 'Complete',
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- {
- // 2
- statusId: statusId2,
- name: 'Complete',
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- {
- // 3
- statusId: statusId3,
- name: 'Complete',
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- {
- // 4
- statusId: statusId4,
- name: 'Complete',
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- {
- // 5
- statusId: statusId5,
- name: 'Complete',
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- {
- // 6 - This test exclusion of invalid review status.
- statusId: statusId6,
- name: 'Not Complete',
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- {
- // 7
- statusId: statusId7,
- name: 'Complete',
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- {
- // 8
- statusId: statusId8,
- name: 'Complete',
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- {
- // 9
- statusId: statusId9,
- name: 'Complete',
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- ], { individualHooks: true });
+ // MonitoringReviewStatus.
+ await MonitoringReviewStatus.bulkCreate([
+ {
+ // 1
+ statusId: statusId1,
+ name: 'Complete',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 2
+ statusId: statusId2,
+ name: 'Complete',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 3
+ statusId: statusId3,
+ name: 'Complete',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 4
+ statusId: statusId4,
+ name: 'Complete',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 5
+ statusId: statusId5,
+ name: 'Complete',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 6 - This test exclusion of invalid review status.
+ statusId: statusId6,
+ name: 'Not Complete',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 7
+ statusId: statusId7,
+ name: 'Complete',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 8
+ statusId: statusId8,
+ name: 'Complete',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 9
+ statusId: statusId9,
+ name: 'Complete',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ ], { individualHooks: true });
- // MonitoringFindingHistory.
- const findingId1 = uuidv4();
- const findingId2 = uuidv4();
- const findingId3 = uuidv4();
- const findingId4 = uuidv4();
- const findingId5 = uuidv4();
- const findingId6 = uuidv4();
- const findingId7 = uuidv4();
- const findingId8 = uuidv4();
- const findingId9 = uuidv4();
- await MonitoringFindingHistory.bulkCreate([
- {
- // 1
- reviewId: grantThatNeedsMonitoringGoalNumberReviewId1,
- findingHistoryId: uuidv4(),
- findingId: findingId1,
- statusId: statusId1,
- narrative: faker.random.words(10),
- ordinal: faker.datatype.number({ min: 1, max: 10 }),
- determination: faker.random.words(5),
- hash: faker.datatype.uuid(),
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- sourceDeletedAt: null,
- },
- {
- // 2
- reviewId: grantThatAlreadyHasMonitoringGoalNumberReviewId2,
- findingHistoryId: uuidv4(),
- findingId: findingId2,
- statusId: statusId2,
- narrative: faker.random.words(10),
- ordinal: faker.datatype.number({ min: 1, max: 10 }),
- determination: faker.random.words(5),
- hash: faker.datatype.uuid(),
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- sourceDeletedAt: null,
- },
- {
- // 3
- reviewId: grantThatFallsBeforeStartDateNumberReviewId3,
- findingHistoryId: uuidv4(),
- findingId: findingId3,
- statusId: statusId3,
- narrative: faker.random.words(10),
- ordinal: faker.datatype.number({ min: 1, max: 10 }),
- determination: faker.random.words(5),
- hash: faker.datatype.uuid(),
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- sourceDeletedAt: null,
- },
- {
- // 4
- reviewId: grantThatFallsAfterCutOffDateNumberReviewId4,
- findingHistoryId: uuidv4(),
- findingId: findingId4,
- statusId: statusId4,
- narrative: faker.random.words(10),
- ordinal: faker.datatype.number({ min: 1, max: 10 }),
- determination: faker.random.words(5),
- hash: faker.datatype.uuid(),
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- sourceDeletedAt: null,
- },
- {
- // 5
- reviewId: grantThatIsInactiveNumberReviewId5,
- findingHistoryId: uuidv4(),
- findingId: findingId5,
- statusId: statusId5,
- narrative: faker.random.words(10),
- ordinal: faker.datatype.number({ min: 1, max: 10 }),
- determination: faker.random.words(5),
- hash: faker.datatype.uuid(),
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- sourceDeletedAt: null,
- },
- {
- // 6
- reviewId: grantThatsMonitoringReviewStatusIsNotCompleteNumberReviewId6,
- findingHistoryId: uuidv4(),
- findingId: findingId6,
- statusId: statusId6,
- narrative: faker.random.words(10),
- ordinal: faker.datatype.number({ min: 1, max: 10 }),
- determination: faker.random.words(5),
- hash: faker.datatype.uuid(),
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- sourceDeletedAt: null,
- },
- {
- // 7
- reviewId: grantThatsMonitoringFindingStatusIsNotActiveNumberReviewId7,
- findingHistoryId: uuidv4(),
- findingId: findingId7,
- statusId: statusId7,
- narrative: faker.random.words(10),
- ordinal: faker.datatype.number({ min: 1, max: 10 }),
- determination: faker.random.words(5),
- hash: faker.datatype.uuid(),
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- sourceDeletedAt: null,
- },
- {
- // 8
- reviewId: grantThatsMonitoringReviewReviewTypeIsNotAllowedNumberReviewId8,
- findingHistoryId: uuidv4(),
- findingId: findingId8,
- statusId: statusId8,
- narrative: faker.random.words(10),
- ordinal: faker.datatype.number({ min: 1, max: 10 }),
- determination: faker.random.words(5),
- hash: faker.datatype.uuid(),
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- sourceDeletedAt: null,
- },
- {
- // 9
- reviewId: inactiveGrantThatHasBeenReplacedByActiveGrantNumberReviewId9,
- findingHistoryId: uuidv4(),
- findingId: findingId9,
- statusId: statusId9,
- narrative: faker.random.words(10),
- ordinal: faker.datatype.number({ min: 1, max: 10 }),
- determination: faker.random.words(5),
- hash: faker.datatype.uuid(),
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- sourceDeletedAt: null,
- },
- ], { individualHooks: true });
+ // MonitoringFindingHistory.
+ const findingId1 = uuidv4();
+ const findingId2 = uuidv4();
+ const findingId3 = uuidv4();
+ const findingId4 = uuidv4();
+ const findingId5 = uuidv4();
+ const findingId6 = uuidv4();
+ const findingId7 = uuidv4();
+ const findingId8 = uuidv4();
+ const findingId9 = uuidv4();
+ await MonitoringFindingHistory.bulkCreate([
+ {
+ // 1
+ reviewId: grantThatNeedsMonitoringGoalNumberReviewId1,
+ findingHistoryId: uuidv4(),
+ findingId: findingId1,
+ statusId: statusId1,
+ narrative: faker.random.words(10),
+ ordinal: faker.datatype.number({ min: 1, max: 10 }),
+ determination: faker.random.words(5),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ sourceDeletedAt: null,
+ },
+ {
+ // 2
+ reviewId: grantThatAlreadyHasMonitoringGoalNumberReviewId2,
+ findingHistoryId: uuidv4(),
+ findingId: findingId2,
+ statusId: statusId2,
+ narrative: faker.random.words(10),
+ ordinal: faker.datatype.number({ min: 1, max: 10 }),
+ determination: faker.random.words(5),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ sourceDeletedAt: null,
+ },
+ {
+ // 3
+ reviewId: grantThatFallsBeforeStartDateNumberReviewId3,
+ findingHistoryId: uuidv4(),
+ findingId: findingId3,
+ statusId: statusId3,
+ narrative: faker.random.words(10),
+ ordinal: faker.datatype.number({ min: 1, max: 10 }),
+ determination: faker.random.words(5),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ sourceDeletedAt: null,
+ },
+ {
+ // 4
+ reviewId: grantThatFallsAfterCutOffDateNumberReviewId4,
+ findingHistoryId: uuidv4(),
+ findingId: findingId4,
+ statusId: statusId4,
+ narrative: faker.random.words(10),
+ ordinal: faker.datatype.number({ min: 1, max: 10 }),
+ determination: faker.random.words(5),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ sourceDeletedAt: null,
+ },
+ {
+ // 5
+ reviewId: grantThatIsInactiveNumberReviewId5,
+ findingHistoryId: uuidv4(),
+ findingId: findingId5,
+ statusId: statusId5,
+ narrative: faker.random.words(10),
+ ordinal: faker.datatype.number({ min: 1, max: 10 }),
+ determination: faker.random.words(5),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ sourceDeletedAt: null,
+ },
+ {
+ // 6
+ reviewId: grantThatsMonitoringReviewStatusIsNotCompleteNumberReviewId6,
+ findingHistoryId: uuidv4(),
+ findingId: findingId6,
+ statusId: statusId6,
+ narrative: faker.random.words(10),
+ ordinal: faker.datatype.number({ min: 1, max: 10 }),
+ determination: faker.random.words(5),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ sourceDeletedAt: null,
+ },
+ {
+ // 7
+ reviewId: grantThatsMonitoringFindingStatusIsNotActiveNumberReviewId7,
+ findingHistoryId: uuidv4(),
+ findingId: findingId7,
+ statusId: statusId7,
+ narrative: faker.random.words(10),
+ ordinal: faker.datatype.number({ min: 1, max: 10 }),
+ determination: faker.random.words(5),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ sourceDeletedAt: null,
+ },
+ {
+ // 8
+ reviewId: grantThatsMonitoringReviewReviewTypeIsNotAllowedNumberReviewId8,
+ findingHistoryId: uuidv4(),
+ findingId: findingId8,
+ statusId: statusId8,
+ narrative: faker.random.words(10),
+ ordinal: faker.datatype.number({ min: 1, max: 10 }),
+ determination: faker.random.words(5),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ sourceDeletedAt: null,
+ },
+ {
+ // 9
+ reviewId: inactiveGrantThatHasBeenReplacedByActiveGrantNumberReviewId9,
+ findingHistoryId: uuidv4(),
+ findingId: findingId9,
+ statusId: statusId9,
+ narrative: faker.random.words(10),
+ ordinal: faker.datatype.number({ min: 1, max: 10 }),
+ determination: faker.random.words(5),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ sourceDeletedAt: null,
+ },
+ ], { individualHooks: true });
- // MonitoringFinding.
- await MonitoringFinding.bulkCreate([
- {
+ // MonitoringFinding.
+ await MonitoringFinding.bulkCreate([
+ {
// 1
- findingId: findingId1,
- statusId: statusId1,
- findingType: faker.random.word(),
- hash: faker.datatype.uuid(),
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- {
+ findingId: findingId1,
+ statusId: statusId1,
+ findingType: faker.random.word(),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
// 2
- findingId: findingId2,
- statusId: statusId2,
- findingType: faker.random.word(),
- hash: faker.datatype.uuid(),
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- {
+ findingId: findingId2,
+ statusId: statusId2,
+ findingType: faker.random.word(),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
// 3
- findingId: findingId3,
- statusId: statusId3,
- findingType: faker.random.word(),
- hash: faker.datatype.uuid(),
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- {
+ findingId: findingId3,
+ statusId: statusId3,
+ findingType: faker.random.word(),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
// 4
- findingId: findingId4,
- statusId: statusId4,
- findingType: faker.random.word(),
- hash: faker.datatype.uuid(),
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- {
+ findingId: findingId4,
+ statusId: statusId4,
+ findingType: faker.random.word(),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
// 5
- findingId: findingId5,
- statusId: statusId5,
- findingType: faker.random.word(),
- hash: faker.datatype.uuid(),
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- {
+ findingId: findingId5,
+ statusId: statusId5,
+ findingType: faker.random.word(),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
// 6
- findingId: findingId6,
- statusId: statusId6,
- findingType: faker.random.word(),
- hash: faker.datatype.uuid(),
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- {
+ findingId: findingId6,
+ statusId: statusId6,
+ findingType: faker.random.word(),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
// 7
- findingId: findingId7,
- statusId: statusId7,
- findingType: faker.random.word(),
- hash: faker.datatype.uuid(),
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- {
+ findingId: findingId7,
+ statusId: statusId7,
+ findingType: faker.random.word(),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
// 8
- findingId: findingId8,
- statusId: statusId8,
- findingType: faker.random.word(),
- hash: faker.datatype.uuid(),
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- {
+ findingId: findingId8,
+ statusId: statusId8,
+ findingType: faker.random.word(),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
// 9
- findingId: findingId9,
- statusId: statusId9,
- findingType: faker.random.word(),
- hash: faker.datatype.uuid(),
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- ], { individualHooks: true });
+ findingId: findingId9,
+ statusId: statusId9,
+ findingType: faker.random.word(),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ ], { individualHooks: true });
- // MonitoringFindingStatus.
- await MonitoringFindingStatus.bulkCreate([
- {
+ // MonitoringFindingStatus.
+ await MonitoringFindingStatus.bulkCreate([
+ {
// 1
- statusId: statusId1,
- name: 'Active',
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- {
+ statusId: statusId1,
+ name: 'Active',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
// 2
- statusId: statusId2,
- name: 'Active',
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- {
+ statusId: statusId2,
+ name: 'Active',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
// 3
- statusId: statusId3,
- name: 'Active',
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- {
+ statusId: statusId3,
+ name: 'Active',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
// 4
- statusId: statusId4,
- name: 'Active',
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- {
+ statusId: statusId4,
+ name: 'Active',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
// 5
- statusId: statusId5,
- name: 'Active',
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- {
+ statusId: statusId5,
+ name: 'Active',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
// 6
- statusId: statusId6,
- name: 'Active',
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- {
+ statusId: statusId6,
+ name: 'Active',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
// 7 - This test exclusion of inactive status.
- statusId: statusId7,
- name: 'Not Active',
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- {
+ statusId: statusId7,
+ name: 'Not Active',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
// 8
- statusId: statusId8,
- name: 'Active',
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- {
+ statusId: statusId8,
+ name: 'Active',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
// 9
- statusId: statusId9,
- name: 'Active',
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- ], { individualHooks: true });
+ statusId: statusId9,
+ name: 'Active',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ ], { individualHooks: true });
- // MonitoringFindingGrant.
- await MonitoringFindingGrant.bulkCreate([
- {
+ // MonitoringFindingGrant.
+ await MonitoringFindingGrant.bulkCreate([
+ {
// 1
- findingId: findingId1,
- granteeId: grantThatNeedsMonitoringGoalNumberGranteeId1,
- statusId: statusId1,
- findingType: faker.random.word(),
- source: faker.random.word(),
- correctionDeadLine: new Date(),
- reportedDate: new Date(),
- closedDate: null,
- hash: faker.datatype.uuid(),
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- sourceDeletedAt: null,
- },
- {
+ findingId: findingId1,
+ granteeId: grantThatNeedsMonitoringGoalNumberGranteeId1,
+ statusId: statusId1,
+ findingType: faker.random.word(),
+ source: faker.random.word(),
+ correctionDeadLine: new Date(),
+ reportedDate: new Date(),
+ closedDate: null,
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ sourceDeletedAt: null,
+ },
+ {
// 2
- findingId: findingId2,
- granteeId: grantThatAlreadyHasMonitoringGoalNumberGranteeId2,
- statusId: statusId2,
- findingType: faker.random.word(),
- source: faker.random.word(),
- correctionDeadLine: new Date(),
- reportedDate: new Date(),
- closedDate: null,
- hash: faker.datatype.uuid(),
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- sourceDeletedAt: null,
- },
- {
+ findingId: findingId2,
+ granteeId: grantThatAlreadyHasMonitoringGoalNumberGranteeId2,
+ statusId: statusId2,
+ findingType: faker.random.word(),
+ source: faker.random.word(),
+ correctionDeadLine: new Date(),
+ reportedDate: new Date(),
+ closedDate: null,
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ sourceDeletedAt: null,
+ },
+ {
// 3
- findingId: findingId3,
- granteeId: grantThatFallsBeforeStartDateNumberGranteeId3,
- statusId: statusId3,
- findingType: faker.random.word(),
- source: faker.random.word(),
- correctionDeadLine: new Date(),
- reportedDate: new Date(),
- closedDate: null,
- hash: faker.datatype.uuid(),
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- sourceDeletedAt: null,
- },
- {
+ findingId: findingId3,
+ granteeId: grantThatFallsBeforeStartDateNumberGranteeId3,
+ statusId: statusId3,
+ findingType: faker.random.word(),
+ source: faker.random.word(),
+ correctionDeadLine: new Date(),
+ reportedDate: new Date(),
+ closedDate: null,
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ sourceDeletedAt: null,
+ },
+ {
// 4
- findingId: findingId4,
- granteeId: grantThatFallsAfterCutOffDateNumberGranteeId4,
- statusId: statusId4,
- findingType: faker.random.word(),
- source: faker.random.word(),
- correctionDeadLine: new Date(),
- reportedDate: new Date(),
- closedDate: null,
- hash: faker.datatype.uuid(),
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- sourceDeletedAt: null,
- },
- {
+ findingId: findingId4,
+ granteeId: grantThatFallsAfterCutOffDateNumberGranteeId4,
+ statusId: statusId4,
+ findingType: faker.random.word(),
+ source: faker.random.word(),
+ correctionDeadLine: new Date(),
+ reportedDate: new Date(),
+ closedDate: null,
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ sourceDeletedAt: null,
+ },
+ {
// 5
- findingId: findingId5,
- granteeId: grantThatIsInactiveNumberGranteeId5,
- statusId: statusId5,
- findingType: faker.random.word(),
- source: faker.random.word(),
- correctionDeadLine: new Date(),
- reportedDate: new Date(),
- closedDate: null,
- hash: faker.datatype.uuid(),
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- sourceDeletedAt: null,
- },
- {
+ findingId: findingId5,
+ granteeId: grantThatIsInactiveNumberGranteeId5,
+ statusId: statusId5,
+ findingType: faker.random.word(),
+ source: faker.random.word(),
+ correctionDeadLine: new Date(),
+ reportedDate: new Date(),
+ closedDate: null,
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ sourceDeletedAt: null,
+ },
+ {
// 6
- findingId: findingId6,
- granteeId: grantThatsMonitoringReviewStatusIsNotCompleteNumberGranteeId6,
- statusId: statusId6,
- findingType: faker.random.word(),
- source: faker.random.word(),
- correctionDeadLine: new Date(),
- reportedDate: new Date(),
- closedDate: null,
- hash: faker.datatype.uuid(),
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- sourceDeletedAt: null,
- },
- {
+ findingId: findingId6,
+ granteeId: grantThatsMonitoringReviewStatusIsNotCompleteNumberGranteeId6,
+ statusId: statusId6,
+ findingType: faker.random.word(),
+ source: faker.random.word(),
+ correctionDeadLine: new Date(),
+ reportedDate: new Date(),
+ closedDate: null,
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ sourceDeletedAt: null,
+ },
+ {
// 7
- findingId: findingId7,
- granteeId: grantThatsMonitoringFindingStatusIsNotActiveNumberGranteeId7,
- statusId: statusId7,
- findingType: faker.random.word(),
- source: faker.random.word(),
- correctionDeadLine: new Date(),
- reportedDate: new Date(),
- closedDate: null,
- hash: faker.datatype.uuid(),
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- sourceDeletedAt: null,
- },
- {
+ findingId: findingId7,
+ granteeId: grantThatsMonitoringFindingStatusIsNotActiveNumberGranteeId7,
+ statusId: statusId7,
+ findingType: faker.random.word(),
+ source: faker.random.word(),
+ correctionDeadLine: new Date(),
+ reportedDate: new Date(),
+ closedDate: null,
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ sourceDeletedAt: null,
+ },
+ {
// 8
- findingId: findingId8,
- granteeId: grantThatsMonitoringReviewReviewTypeIsNotAllowedNumberGranteeId8,
- statusId: statusId8,
- findingType: faker.random.word(),
- source: faker.random.word(),
- correctionDeadLine: new Date(),
- reportedDate: new Date(),
- closedDate: null,
- hash: faker.datatype.uuid(),
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- sourceDeletedAt: null,
- },
- {
+ findingId: findingId8,
+ granteeId: grantThatsMonitoringReviewReviewTypeIsNotAllowedNumberGranteeId8,
+ statusId: statusId8,
+ findingType: faker.random.word(),
+ source: faker.random.word(),
+ correctionDeadLine: new Date(),
+ reportedDate: new Date(),
+ closedDate: null,
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ sourceDeletedAt: null,
+ },
+ {
// 9
- findingId: findingId9,
- granteeId: inactiveGrantThatHasBeenReplacedByActiveGrantNumberGranteeId9,
- statusId: statusId9,
- findingType: faker.random.word(),
- source: faker.random.word(),
- correctionDeadLine: new Date(),
- reportedDate: new Date(),
- closedDate: null,
- hash: faker.datatype.uuid(),
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- sourceDeletedAt: null,
- },
- ], { individualHooks: true });
+ findingId: findingId9,
+ granteeId: inactiveGrantThatHasBeenReplacedByActiveGrantNumberGranteeId9,
+ statusId: statusId9,
+ findingType: faker.random.word(),
+ source: faker.random.word(),
+ correctionDeadLine: new Date(),
+ reportedDate: new Date(),
+ closedDate: null,
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ sourceDeletedAt: null,
+ },
+ ], { individualHooks: true });
- // Retrieve the goal template.
- goalTemplate = await GoalTemplate.findOne({ where: { id: goalTemplateId } });
+ // Retrieve the goal template.
+ goalTemplate = await GoalTemplate.findOne({ where: { templateName: goalTemplateName } });
- // Create a goal for grantThatAlreadyHasMonitoringGoal2.
- await Goal.create({
- id: faker.datatype.number({ min: 9999 }),
- grantId: grantThatAlreadyHasMonitoringGoal2.id,
- goalTemplateId: goalTemplate.id,
- status: 'Active',
- });
- } catch (error) {
- console.log('\n\n\n============create error', error);
- }
+ // Create a goal for grantThatAlreadyHasMonitoringGoal2.
+ await Goal.create({
+ id: faker.datatype.number({ min: 9999 }),
+ name: goalTemplateName,
+ grantId: grantThatAlreadyHasMonitoringGoal2.id,
+ goalTemplateId: goalTemplate.id,
+ status: 'In progress',
+ });
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
});
afterAll(async () => {
- try {
- await rollbackToSnapshot(snapShot);
- // Close the connection to the database.
- await sequelize.close();
- } catch (error) {
- console.log('\n\n\n============delete error', error);
- }
+ await rollbackToSnapshot(snapShot);
+ // Close the connection to the database.
+ await sequelize.close();
});
it('logs an error is the monitoring goal template doesn\'t exist', async () => {
+ // Get the current function for the GoalTemplate.findOne method and but it back later.
+ const originalFindOne = GoalTemplate.findOne;
// Mock the GoalTemplate.findOne method to return null.
GoalTemplate.findOne = jest.fn().mockResolvedValueOnce(null);
jest.spyOn(auditLogger, 'error');
await createMonitoringGoals();
- expect(auditLogger.error).toHaveBeenCalledWith('Monitoring Goal template with ID 18172 not found');
+ expect(auditLogger.error).toHaveBeenCalledWith('Monitoring Goal template not found');
+ // Put the original function back.
+ GoalTemplate.findOne = originalFindOne;
});
it('creates monitoring goals for grants that need them', async () => {
- expect(true).toBe(true);
+ // Run the CRON job.
+ await createMonitoringGoals();
+
+ // CASE 1: Properly creates the monitoring goal.
+ const grant1Goals = await Goal.findAll({ where: { grantId: grantThatNeedsMonitoringGoal1.id } });
+ expect(grant1Goals.length).toBe(1);
+
+ // Assert that the goal that was created was the monitoring goal and is using the correct template.
+ expect(grant1Goals[0].goalTemplateId).toBe(goalTemplate.id);
+ expect(grant1Goals[0].name).toBe(goalTemplateName);
+ expect(grant1Goals[0].status).toBe('Not started');
+
+ // CASE 2: Does not create a monitoring goal for a grant that already has one.
+ const grant2Goals = await Goal.findAll({ where: { grantId: grantThatAlreadyHasMonitoringGoal2.id } });
+ expect(grant2Goals.length).toBe(1);
+ expect(grant2Goals[0].goalTemplateId).toBe(goalTemplate.id);
+ expect(grant2Goals[0].name).toBe(goalTemplateName);
+ expect(grant2Goals[0].status).toBe('In progress');
+
+ // CASE 3: Does not create a monitoring goal for a grant that falls before the start date.
+ const grant3Goals = await Goal.findAll({ where: { grantId: grantThatFallsBeforeStartDate3.id } });
+ expect(grant3Goals.length).toBe(0);
+
+ // CASE 4: Does not create a monitoring goal for a grant that falls after the cut off date.
+ const grant4Goals = await Goal.findAll({ where: { grantId: grantThatFallsAfterCutOffDate4.id } });
+ expect(grant4Goals.length).toBe(0);
+
+ // CASE 5: Does not create a monitoring goal for an inactive grant.
+ const grant5Goals = await Goal.findAll({ where: { grantId: grantThatIsInactive5.id } });
+ expect(grant5Goals.length).toBe(0);
+
+ // CASE 6: Does not create a monitoring goal for a grant that has a monitoring review with a status that is not complete.
+ const grant6Goals = await Goal.findAll({ where: { grantId: grantThatsMonitoringReviewStatusIsNotComplete6.id } });
+ expect(grant6Goals.length).toBe(0);
+
+ // CASE 7: Does not create a monitoring goal for a grant that has a monitoring finding with a status that is not active.
+ const grant7Goals = await Goal.findAll({ where: { grantId: grantThatsMonitoringFindingStatusIsNotActive7.id } });
+ expect(grant7Goals.length).toBe(0);
+
+ // CASE 8: Does not create a monitoring goal for a grant that has a monitoring review with a review type that is not allowed.
+ const grant8Goals = await Goal.findAll({ where: { grantId: grantThatsMonitoringReviewReviewTypeIsNotAllowed8.id } });
+ expect(grant8Goals.length).toBe(0);
+
+ // CASE 9: Does not create a monitoring goal for an inactive grant that has been replaced by an active grant.
+ const grant9Goals = await Goal.findAll({ where: { grantId: inactiveGrantThatHasBeenReplacedByActiveGrant9.id } });
+ expect(grant9Goals.length).toBe(1);
+ expect(grant9Goals[0].goalTemplateId).toBe(goalTemplate.id);
+ expect(grant9Goals[0].name).toBe(goalTemplateName);
+ expect(grant9Goals[0].status).toBe('Not started');
});
});
From b2d09ee469b3ccdbc60592c6693e32c6e8c0e2b0 Mon Sep 17 00:00:00 2001
From: GarrettEHill
Date: Tue, 12 Nov 2024 12:11:12 -0800
Subject: [PATCH 004/198] Add reopen and do not propagate to CDI or replacement
recipients
---
src/tools/createMonitoringGoals.js | 62 +++++++++++++++++++++++++++++-
1 file changed, 60 insertions(+), 2 deletions(-)
diff --git a/src/tools/createMonitoringGoals.js b/src/tools/createMonitoringGoals.js
index 07b53b599c..a1e724c3c5 100644
--- a/src/tools/createMonitoringGoals.js
+++ b/src/tools/createMonitoringGoals.js
@@ -51,7 +51,10 @@ const createMonitoringGoals = async () => {
ON (grta."grantId" = g."grantId"
OR grta."activeGrantId" = g."grantId")
AND g."goalTemplateId" = ${monitoringGoalTemplate.id}
- WHERE gr.status = 'Active'
+ JOIN "Grants" gr2
+ ON grta."activeGrantId" = gr2.id
+ AND gr."recipientId" = gr2."recipientId"
+ WHERE NOT gr2.cdi
AND mrs."name" = 'Complete'
AND mfs."name" = 'Active'
AND mr."reportDeliveryDate" BETWEEN '${cutOffDate}' AND NOW()
@@ -90,7 +93,62 @@ const createMonitoringGoals = async () => {
SELECT
"name", "status", "timeframe", "isFromSmartsheetTtaPlan", "createdAt", "updatedAt", "goalTemplateId", "grantId", "onApprovedAR", "createdVia", "isRttapa", "onAR", "source"
FROM new_goals;
- `);
+ `);
+
+ // Reopen monitoring goals for grants that need them.
+ await sequelize.query(`
+ WITH
+ grants_needing_goal_reopend AS (
+ SELECT
+ g.id AS "goalId"
+ FROM "Grants" gr
+ JOIN "GrantRelationshipToActive" grta
+ ON gr.id = grta."grantId"
+ AND grta."activeGrantId" IS NOT NULL
+ JOIN "MonitoringReviewGrantees" mrg
+ ON gr.number = mrg."grantNumber"
+ JOIN "MonitoringReviews" mr
+ ON mrg."reviewId" = mr."reviewId"
+ JOIN "MonitoringReviewStatuses" mrs
+ ON mr."statusId" = mrs."statusId"
+ JOIN "MonitoringFindingHistories" mfh
+ ON mr."reviewId" = mfh."reviewId"
+ JOIN "MonitoringFindings" mf
+ ON mfh."findingId" = mf."findingId"
+ JOIN "MonitoringFindingStatuses" mfs
+ ON mf."statusId" = mfs."statusId"
+ JOIN "MonitoringFindingGrants" mfg
+ ON mf."findingId" = mfg."findingId"
+ AND mrg."granteeId" = mfg."granteeId"
+ LEFT JOIN "Goals" g
+ ON (grta."grantId" = g."grantId"
+ OR grta."activeGrantId" = g."grantId")
+ AND g."goalTemplateId" = ${monitoringGoalTemplate.id}
+ JOIN "Grants" gr2
+ ON grta."activeGrantId" = gr2.id
+ AND gr."recipientId" = gr2."recipientId"
+ WHERE NOT gr2.cdi
+ AND mrs."name" = 'Complete'
+ AND mfs."name" = 'Active'
+ AND mr."reportDeliveryDate" BETWEEN '${cutOffDate}' AND NOW()
+ AND mr."reviewType" IN (
+ 'AIAN-DEF',
+ 'RAN',
+ 'Follow-up',
+ 'FA-1', 'FA1-FR',
+ 'FA-2', 'FA2-CR',
+ 'Special'
+ )
+ AND g.status = 'Closed'
+ GROUP BY 1
+ )
+ UPDATE "Goals"
+ SET "status" = 'In progress',
+ "updatedAt" = NOW()
+ FROM grants_needing_goal_reopend
+ WHERE "Goals".id = grants_needing_goal_reopend."goalId";
+ `);
+
};
export default createMonitoringGoals;
From cac728e7eb24b9e47f3dd3aad0ca5922096634ca Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Thu, 14 Nov 2024 11:49:34 -0500
Subject: [PATCH 005/198] more uses cases
---
src/tools/createMonitoringGoals.test.js | 591 +++++++++++++++++++++++-
1 file changed, 585 insertions(+), 6 deletions(-)
diff --git a/src/tools/createMonitoringGoals.test.js b/src/tools/createMonitoringGoals.test.js
index ba4a569781..589d8a6a78 100644
--- a/src/tools/createMonitoringGoals.test.js
+++ b/src/tools/createMonitoringGoals.test.js
@@ -15,6 +15,7 @@ import {
MonitoringFindingStatus,
MonitoringFindingGrant,
Goal,
+ GrantRelationshipToActive,
} from '../models';
import { captureSnapshot, rollbackToSnapshot } from '../lib/programmaticTransaction';
import { auditLogger } from '../logger';
@@ -23,6 +24,8 @@ jest.mock('../logger');
describe('createMonitoringGoals', () => {
let recipient;
+ let recipientForSplitCase10;
+ let recipientForMergeCase11;
const startingReportDeliveryDate = new Date('2023-12-01');
// const goalTemplateId = 18172;
@@ -38,6 +41,17 @@ describe('createMonitoringGoals', () => {
let grantThatsMonitoringFindingStatusIsNotActive7;
let grantThatsMonitoringReviewReviewTypeIsNotAllowed8;
let inactiveGrantThatHasBeenReplacedByActiveGrant9;
+ // These grants represent a slightly more complex case when a
+ // grant that has a monitoring goal is then split into two recipients.
+ // We would expect a new monitoring goal to be created for only one of two new recipients.
+ let grantBeingMonitoredSplit10A;
+ let grantBeingMonitoredSplit10B;
+ let grantBeingMonitoredSplit10C;
+ // These grants represent a cae when two goals have a monitoring goal
+ // but then they are merged into a single grant.
+ let grantBeingMerged11A;
+ let grantBeingMerged11B;
+ let grantBeingMerged11C;
const grantThatNeedsMonitoringGoalNumber1 = faker.datatype.string(4);
const grantThatAlreadyHasMonitoringGoalNumber2 = faker.datatype.string(4);
@@ -48,18 +62,35 @@ describe('createMonitoringGoals', () => {
const grantThatsMonitoringFindingStatusIsNotActiveNumber7 = faker.datatype.string(4);
const grantThatsMonitoringReviewReviewTypeIsNotAllowedNumber8 = faker.datatype.string(4);
const inactiveGrantThatHasBeenReplacedByActiveGrantNumber9 = faker.datatype.string(4);
+ const grantBeingMonitoredSplitNumber10A = uuidv4();
+ const grantBeingMonitoredSplitNumber10B = uuidv4();
+ const grantBeingMonitoredSplitNumber10C = uuidv4();
+ const grantBeingMergedNumber11A = uuidv4();
+ const grantBeingMergedNumber11B = uuidv4();
+ const grantBeingMergedNumber11C = uuidv4();
let snapShot;
beforeAll(async () => {
// Create a snapshot of the database so we can rollback after the tests.
snapShot = await captureSnapshot();
+
// Recipient.
recipient = await Recipient.create({
id: faker.datatype.number({ min: 64000 }),
name: faker.random.alphaNumeric(6),
});
+ recipientForSplitCase10 = await Recipient.create({
+ id: faker.datatype.number({ min: 64000 }),
+ name: faker.random.alphaNumeric(6),
+ });
+
+ recipientForMergeCase11 = await Recipient.create({
+ id: faker.datatype.number({ min: 64000 }),
+ name: faker.random.alphaNumeric(6),
+ });
+
// Grants.
const grants = await Grant.bulkCreate([
{
@@ -152,6 +183,66 @@ describe('createMonitoringGoals', () => {
endDate: new Date(),
status: 'Active',
},
+ {
+ // 10 A
+ id: faker.datatype.number({ min: 9999 }),
+ number: grantBeingMonitoredSplitNumber10A,
+ recipientId: recipient.id,
+ regionId: 1,
+ startDate: new Date(),
+ endDate: new Date(),
+ status: 'Active',
+ },
+ {
+ // 10 B
+ id: faker.datatype.number({ min: 9999 }),
+ number: grantBeingMonitoredSplitNumber10B,
+ recipientId: recipient.id,
+ regionId: 1,
+ startDate: new Date(),
+ endDate: new Date(),
+ status: 'Active',
+ },
+ {
+ // 10 C
+ id: faker.datatype.number({ min: 9999 }),
+ number: grantBeingMonitoredSplitNumber10C,
+ recipientId: recipientForSplitCase10.id, // Note the different recipient here.
+ regionId: 1,
+ startDate: new Date(),
+ endDate: new Date(),
+ status: 'Active',
+ },
+ {
+ // 11 A
+ id: faker.datatype.number({ min: 9999 }),
+ number: grantBeingMergedNumber11A,
+ recipientId: recipientForMergeCase11.id,
+ regionId: 1,
+ startDate: new Date(),
+ endDate: new Date(),
+ status: 'Active',
+ },
+ {
+ // 11 B
+ id: faker.datatype.number({ min: 9999 }),
+ number: grantBeingMergedNumber11B,
+ recipientId: recipient.id,
+ regionId: 1,
+ startDate: new Date(),
+ endDate: new Date(),
+ status: 'Active',
+ },
+ {
+ // 11 C
+ id: faker.datatype.number({ min: 9999 }),
+ number: grantBeingMergedNumber11C,
+ recipientId: recipientForMergeCase11.id,
+ regionId: 1,
+ startDate: new Date(),
+ endDate: new Date(),
+ status: 'Active',
+ },
]);
[
@@ -164,6 +255,12 @@ describe('createMonitoringGoals', () => {
grantThatsMonitoringFindingStatusIsNotActive7,
grantThatsMonitoringReviewReviewTypeIsNotAllowed8,
inactiveGrantThatHasBeenReplacedByActiveGrant9,
+ grantBeingMonitoredSplit10A,
+ grantBeingMonitoredSplit10B,
+ grantBeingMonitoredSplit10C,
+ grantBeingMerged11A,
+ grantBeingMerged11B,
+ grantBeingMerged11C,
] = grants;
// Create an inactive grant that has 'cdi' true that points to inactiveGrantThatHasBeenReplacedByActiveGrant9.
@@ -195,6 +292,33 @@ describe('createMonitoringGoals', () => {
VALUES (${cdiGrant.id}, ${inactiveGrantThatHasBeenReplacedByActiveGrant9.id}, ${grantReplacementType[0].id}, NOW(), NOW(), NOW())
`);
+ // Grant replacement for split case 10.
+ // Grant A is replaced by Grant B and Grant C (C uses a different recipient).
+ await sequelize.query(`
+ INSERT INTO "GrantReplacements" ("replacedGrantId", "replacingGrantId", "grantReplacementTypeId", "replacementDate", "createdAt", "updatedAt")
+ VALUES (${grantBeingMonitoredSplit10A.id}, ${grantBeingMonitoredSplit10B.id}, ${grantReplacementType[0].id}, NOW(), NOW(), NOW())
+ `);
+
+ await sequelize.query(`
+ INSERT INTO "GrantReplacements" ("replacedGrantId", "replacingGrantId", "grantReplacementTypeId", "replacementDate", "createdAt", "updatedAt")
+ VALUES (${grantBeingMonitoredSplit10A.id}, ${grantBeingMonitoredSplit10C.id}, ${grantReplacementType[0].id}, NOW(), NOW(), NOW())
+ `);
+
+ // Create a grant replacement for merge case 11.
+ // Grant A and B are replaced by Grant C.
+ await sequelize.query(`
+ INSERT INTO "GrantReplacements" ("replacedGrantId", "replacingGrantId", "grantReplacementTypeId", "replacementDate", "createdAt", "updatedAt")
+ VALUES (${grantBeingMerged11A.id}, ${grantBeingMerged11C.id}, ${grantReplacementType[0].id}, NOW(), NOW(), NOW())
+ `);
+
+ await sequelize.query(`
+ INSERT INTO "GrantReplacements" ("replacedGrantId", "replacingGrantId", "grantReplacementTypeId", "replacementDate", "createdAt", "updatedAt")
+ VALUES (${grantBeingMerged11A.id}, ${grantBeingMerged11C.id}, ${grantReplacementType[0].id}, NOW(), NOW(), NOW())
+ `);
+
+ // We need to call this to ensure the materialized view is updated.
+ await GrantRelationshipToActive.refresh();
+
// granteeId GUID.
const grantThatNeedsMonitoringGoalNumberGranteeId1 = uuidv4();
const grantThatAlreadyHasMonitoringGoalNumberGranteeId2 = uuidv4();
@@ -205,6 +329,12 @@ describe('createMonitoringGoals', () => {
const grantThatsMonitoringFindingStatusIsNotActiveNumberGranteeId7 = uuidv4();
const grantThatsMonitoringReviewReviewTypeIsNotAllowedNumberGranteeId8 = uuidv4();
const inactiveGrantThatHasBeenReplacedByActiveGrantNumberGranteeId9 = uuidv4();
+ const grantBeingMonitoredSplitNumberGranteeId10A = uuidv4();
+ const grantBeingMonitoredSplitNumberGranteeId10B = uuidv4();
+ // Exclude grantBeingMonitoredSplitNumberGranteeId10C from the granteeIds array below.
+ const grantBeingMergedNumberGranteeId11A = uuidv4();
+ const grantBeingMergedNumberGranteeId11B = uuidv4();
+ const grantBeingMergedNumberGranteeId11C = uuidv4();
// reviewId GUID.
const grantThatNeedsMonitoringGoalNumberReviewId1 = uuidv4();
@@ -216,6 +346,12 @@ describe('createMonitoringGoals', () => {
const grantThatsMonitoringFindingStatusIsNotActiveNumberReviewId7 = uuidv4();
const grantThatsMonitoringReviewReviewTypeIsNotAllowedNumberReviewId8 = uuidv4();
const inactiveGrantThatHasBeenReplacedByActiveGrantNumberReviewId9 = uuidv4();
+ const grantBeingMonitoredSplitNumberReviewId10A = uuidv4();
+ const grantBeingMonitoredSplitNumberReviewId10B = uuidv4();
+ // Exclude grantBeingMonitoredSplitNumberReviewId10C from the reviewIds array below.
+ const grantBeingMergedNumberReviewId11A = uuidv4();
+ const grantBeingMergedNumberReviewId11B = uuidv4();
+ const grantBeingMergedNumberReviewId11C = uuidv4();
// MonitoringReviewGrantee.
await MonitoringReviewGrantee.bulkCreate([
@@ -327,17 +463,86 @@ describe('createMonitoringGoals', () => {
sourceCreatedAt: new Date(),
sourceUpdatedAt: new Date(),
},
+ {
+ // 10 A
+ id: faker.datatype.number({ min: 999999 }),
+ grantNumber: grantBeingMonitoredSplitNumber10A,
+ reviewId: grantBeingMonitoredSplitNumberReviewId10A,
+ granteeId: grantBeingMonitoredSplitNumberGranteeId10A,
+ createTime: new Date(),
+ updateTime: new Date(),
+ updateBy: 'Support Team',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 10 B
+ id: faker.datatype.number({ min: 999999 }),
+ grantNumber: grantBeingMonitoredSplitNumber10B,
+ reviewId: grantBeingMonitoredSplitNumberReviewId10B,
+ granteeId: grantBeingMonitoredSplitNumberGranteeId10B,
+ createTime: new Date(),
+ updateTime: new Date(),
+ updateBy: 'Support Team',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 11 A
+ id: faker.datatype.number({ min: 999999 }),
+ grantNumber: grantBeingMergedNumber11A,
+ reviewId: grantBeingMergedNumberReviewId11A,
+ granteeId: grantBeingMergedNumberGranteeId11A,
+ createTime: new Date(),
+ updateTime: new Date(),
+ updateBy: 'Support Team',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ // create remaining entries for B and C.
+ {
+ // 11 B
+ id: faker.datatype.number({ min: 999999 }),
+ grantNumber: grantBeingMergedNumber11B,
+ reviewId: grantBeingMergedNumberReviewId11B,
+ granteeId: grantBeingMergedNumberGranteeId11B,
+ createTime: new Date(),
+ updateTime: new Date(),
+ updateBy: 'Support Team',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 11 C
+ id: faker.datatype.number({ min: 999999 }),
+ grantNumber: grantBeingMergedNumber11C,
+ reviewId: grantBeingMergedNumberReviewId11C,
+ granteeId: grantBeingMergedNumberGranteeId11C,
+ createTime: new Date(),
+ updateTime: new Date(),
+ updateBy: 'Support Team',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
], { individualHooks: true });
// Create 9 statusId variables made up of 6 random numbers and set each one to its corresponding statusId below in MonitoringReview.bulkCreate.
- const statusIds = new Set();
- while (statusIds.size < 9) {
- statusIds.add(faker.datatype.number({ min: 1, max: 9 }));
- }
- const [statusId1, statusId2, statusId3, statusId4, statusId5, statusId6, statusId7, statusId8, statusId9] = [...statusIds];
+ const statusId1 = 1;
+ const statusId2 = 2;
+ const statusId3 = 3;
+ const statusId4 = 4;
+ const statusId5 = 5;
+ const statusId6 = 6;
+ const statusId7 = 7;
+ const statusId8 = 8;
+ const statusId9 = 9;
+ const status10A = 10;
+ const status10B = 11;
+ const status11A = 12;
+ const status11B = 13;
+ const status11C = 14;
// MonitoringReview.
-
// Allowed review types:
/*
'AIAN-DEF',
@@ -494,6 +699,86 @@ describe('createMonitoringGoals', () => {
sourceCreatedAt: new Date(),
sourceUpdatedAt: new Date(),
},
+ {
+ // 10 A
+ reviewId: grantBeingMonitoredSplitNumberReviewId10A,
+ contentId: uuidv4(),
+ statusId: status10A,
+ name: faker.random.words(3),
+ startDate: new Date(),
+ endDate: new Date(),
+ reviewType: 'RAN',
+ reportDeliveryDate: new Date(),
+ reportAttachmentId: uuidv4(),
+ outcome: faker.random.words(5),
+ hash: uuidv4(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 10 B
+ reviewId: grantBeingMonitoredSplitNumberReviewId10B,
+ contentId: uuidv4(),
+ statusId: status10B,
+ name: faker.random.words(3),
+ startDate: new Date(),
+ endDate: new Date(),
+ reviewType: 'RAN',
+ reportDeliveryDate: new Date(),
+ reportAttachmentId: uuidv4(),
+ outcome: faker.random.words(5),
+ hash: uuidv4(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 11 A
+ reviewId: grantBeingMergedNumberReviewId11A,
+ contentId: uuidv4(),
+ statusId: status11A,
+ name: faker.random.words(3),
+ startDate: new Date(),
+ endDate: new Date(),
+ reviewType: 'RAN',
+ reportDeliveryDate: new Date(),
+ reportAttachmentId: uuidv4(),
+ outcome: faker.random.words(5),
+ hash: uuidv4(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 11 B
+ reviewId: grantBeingMergedNumberReviewId11B,
+ contentId: uuidv4(),
+ statusId: status11B,
+ name: faker.random.words(3),
+ startDate: new Date(),
+ endDate: new Date(),
+ reviewType: 'RAN',
+ reportDeliveryDate: new Date(),
+ reportAttachmentId: uuidv4(),
+ outcome: faker.random.words(5),
+ hash: uuidv4(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 11 B
+ reviewId: grantBeingMergedNumberReviewId11C,
+ contentId: uuidv4(),
+ statusId: status11C,
+ name: faker.random.words(3),
+ startDate: new Date(),
+ endDate: new Date(),
+ reviewType: 'RAN',
+ reportDeliveryDate: new Date(),
+ reportAttachmentId: uuidv4(),
+ outcome: faker.random.words(5),
+ hash: uuidv4(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
], { individualHooks: true });
// MonitoringReviewStatus.
@@ -561,6 +846,41 @@ describe('createMonitoringGoals', () => {
sourceCreatedAt: new Date(),
sourceUpdatedAt: new Date(),
},
+ {
+ // 10 A
+ statusId: status10A,
+ name: 'Complete',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 10 B
+ statusId: status10B,
+ name: 'Complete',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 11 A
+ statusId: status11A,
+ name: 'Complete',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 11 B
+ statusId: status11B,
+ name: 'Complete',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 11 C
+ statusId: status11C,
+ name: 'Complete',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
], { individualHooks: true });
// MonitoringFindingHistory.
@@ -573,6 +893,13 @@ describe('createMonitoringGoals', () => {
const findingId7 = uuidv4();
const findingId8 = uuidv4();
const findingId9 = uuidv4();
+ const findingId10A = uuidv4();
+ const findingId10B = uuidv4();
+ const findingId11A = uuidv4();
+ const findingId11B = uuidv4();
+ const findingId11C = uuidv4();
+
+ // Exclude findingId10C from the findingIds array below.
await MonitoringFindingHistory.bulkCreate([
{
// 1
@@ -700,6 +1027,71 @@ describe('createMonitoringGoals', () => {
sourceUpdatedAt: new Date(),
sourceDeletedAt: null,
},
+ {
+ // 10 A
+ reviewId: grantBeingMonitoredSplitNumberReviewId10A,
+ findingHistoryId: uuidv4(),
+ findingId: findingId10A,
+ statusId: status10A,
+ narrative: faker.random.words(10),
+ ordinal: faker.datatype.number({ min: 1, max: 10 }),
+ determination: faker.random.words(5),
+ hash: uuidv4(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 10 B
+ reviewId: grantBeingMonitoredSplitNumberReviewId10B,
+ findingHistoryId: uuidv4(),
+ findingId: findingId10B,
+ statusId: status10B,
+ narrative: faker.random.words(10),
+ ordinal: faker.datatype.number({ min: 1, max: 10 }),
+ determination: faker.random.words(5),
+ hash: uuidv4(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 11 A
+ reviewId: grantBeingMergedNumberReviewId11A,
+ findingHistoryId: uuidv4(),
+ findingId: findingId11A,
+ statusId: status11A,
+ narrative: faker.random.words(10),
+ ordinal: faker.datatype.number({ min: 1, max: 10 }),
+ determination: faker.random.words(5),
+ hash: uuidv4(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 11 B
+ reviewId: grantBeingMergedNumberReviewId11B,
+ findingHistoryId: uuidv4(),
+ findingId: findingId11B,
+ statusId: status11B,
+ narrative: faker.random.words(10),
+ ordinal: faker.datatype.number({ min: 1, max: 10 }),
+ determination: faker.random.words(5),
+ hash: uuidv4(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 11 C
+ reviewId: grantBeingMergedNumberReviewId11C,
+ findingHistoryId: uuidv4(),
+ findingId: findingId11C,
+ statusId: status11C,
+ narrative: faker.random.words(10),
+ ordinal: faker.datatype.number({ min: 1, max: 10 }),
+ determination: faker.random.words(5),
+ hash: uuidv4(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
], { individualHooks: true });
// MonitoringFinding.
@@ -785,6 +1177,51 @@ describe('createMonitoringGoals', () => {
sourceCreatedAt: new Date(),
sourceUpdatedAt: new Date(),
},
+ {
+ // 10 A
+ findingId: findingId10A,
+ statusId: status10A,
+ findingType: faker.random.word(),
+ hash: uuidv4(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 10 B
+ findingId: findingId10B,
+ statusId: status10B,
+ findingType: faker.random.word(),
+ hash: uuidv4(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 11 A
+ findingId: findingId11A,
+ statusId: status11A,
+ findingType: faker.random.word(),
+ hash: uuidv4(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 11 B
+ findingId: findingId11B,
+ statusId: status11B,
+ findingType: faker.random.word(),
+ hash: uuidv4(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 11 C
+ findingId: findingId11C,
+ statusId: status11C,
+ findingType: faker.random.word(),
+ hash: uuidv4(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
], { individualHooks: true });
// MonitoringFindingStatus.
@@ -852,6 +1289,27 @@ describe('createMonitoringGoals', () => {
sourceCreatedAt: new Date(),
sourceUpdatedAt: new Date(),
},
+ {
+ // 10 A
+ statusId: status10A,
+ name: 'Active',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 10 B
+ statusId: status10B,
+ name: 'Active',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 11 C
+ statusId: status11C,
+ name: 'Active',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
], { individualHooks: true });
// MonitoringFindingGrant.
@@ -991,6 +1449,76 @@ describe('createMonitoringGoals', () => {
sourceUpdatedAt: new Date(),
sourceDeletedAt: null,
},
+ {
+ // 10 A
+ findingId: findingId10A,
+ granteeId: grantBeingMonitoredSplitNumberGranteeId10A,
+ statusId: status10A,
+ findingType: faker.random.word(),
+ source: faker.random.word(),
+ correctionDeadLine: new Date(),
+ reportedDate: new Date(),
+ closedDate: null,
+ hash: uuidv4(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 10 B
+ findingId: findingId10B,
+ granteeId: grantBeingMonitoredSplitNumberGranteeId10B,
+ statusId: status10B,
+ findingType: faker.random.word(),
+ source: faker.random.word(),
+ correctionDeadLine: new Date(),
+ reportedDate: new Date(),
+ closedDate: null,
+ hash: uuidv4(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 11 A
+ findingId: findingId11A,
+ granteeId: grantBeingMergedNumberGranteeId11A,
+ statusId: status11A,
+ findingType: faker.random.word(),
+ source: faker.random.word(),
+ correctionDeadLine: new Date(),
+ reportedDate: new Date(),
+ closedDate: null,
+ hash: uuidv4(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 11 B
+ findingId: findingId11B,
+ granteeId: grantBeingMergedNumberGranteeId11B,
+ statusId: status11B,
+ findingType: faker.random.word(),
+ source: faker.random.word(),
+ correctionDeadLine: new Date(),
+ reportedDate: new Date(),
+ closedDate: null,
+ hash: uuidv4(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 11 C
+ findingId: findingId11C,
+ granteeId: grantBeingMergedNumberGranteeId11C,
+ statusId: status11C,
+ findingType: faker.random.word(),
+ source: faker.random.word(),
+ correctionDeadLine: new Date(),
+ reportedDate: new Date(),
+ closedDate: null,
+ hash: uuidv4(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
], { individualHooks: true });
// Retrieve the goal template.
@@ -1004,6 +1532,32 @@ describe('createMonitoringGoals', () => {
goalTemplateId: goalTemplate.id,
status: 'In progress',
});
+
+ // Create a monitoring goal for grantBeingMonitoredSplit10A in case 10 the split.
+ await Goal.create({
+ id: faker.datatype.number({ min: 9999 }),
+ name: goalTemplateName,
+ grantId: grantBeingMonitoredSplit10A.id,
+ goalTemplateId: goalTemplate.id,
+ status: 'Not started',
+ });
+
+ // Create a existing monitoring goal for Case 11 on Grant A and B.
+ await Goal.create({
+ id: faker.datatype.number({ min: 9999 }),
+ name: goalTemplateName,
+ grantId: grantBeingMerged11A.id,
+ goalTemplateId: goalTemplate.id,
+ status: 'Not started',
+ });
+
+ await Goal.create({
+ id: faker.datatype.number({ min: 9999 }),
+ name: goalTemplateName,
+ grantId: grantBeingMerged11B.id,
+ goalTemplateId: goalTemplate.id,
+ status: 'Not started',
+ });
});
afterEach(() => {
@@ -1078,5 +1632,30 @@ describe('createMonitoringGoals', () => {
expect(grant9Goals[0].goalTemplateId).toBe(goalTemplate.id);
expect(grant9Goals[0].name).toBe(goalTemplateName);
expect(grant9Goals[0].status).toBe('Not started');
+
+ // CASE 10: Creates a monitoring goal ONLY for the grant that initially had the monitoring goal and does NOT create one for the split grant..
+ const grant10AGoals = await Goal.findAll({ where: { grantId: grantBeingMonitoredSplit10A.id } });
+ expect(grant10AGoals.length).toBe(1);
+ expect(grant10AGoals[0].goalTemplateId).toBe(goalTemplate.id);
+
+ const grant10CGoals = await Goal.findAll({ where: { grantId: grantBeingMonitoredSplit10B.id } });
+ expect(grant10CGoals.length).toBe(1);
+ expect(grant10CGoals[0].goalTemplateId).toBe(goalTemplate.id);
+
+ const grant10BGoals = await Goal.findAll({ where: { grantId: grantBeingMonitoredSplit10C.id } });
+ expect(grant10BGoals.length).toBe(0);
+
+ // CASE 11: Creates a monitoring goal for the merged grant.
+ const grant11AGoals = await Goal.findAll({ where: { grantId: grantBeingMerged11A.id } });
+ expect(grant11AGoals.length).toBe(1);
+ expect(grant11AGoals[0].goalTemplateId).toBe(goalTemplate.id);
+
+ const grant11BGoals = await Goal.findAll({ where: { grantId: grantBeingMerged11B.id } });
+ expect(grant11BGoals.length).toBe(1);
+ expect(grant11BGoals[0].goalTemplateId).toBe(goalTemplate.id);
+
+ const grant11CGoals = await Goal.findAll({ where: { grantId: grantBeingMerged11C.id } });
+ expect(grant11CGoals.length).toBe(1);
+ expect(grant11CGoals[0].goalTemplateId).toBe(goalTemplate.id);
});
});
From f4eef5e44383643139c90717c403799ab224b849 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Thu, 14 Nov 2024 13:57:28 -0500
Subject: [PATCH 006/198] remove lint return
---
src/tools/createMonitoringGoals.js | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/tools/createMonitoringGoals.js b/src/tools/createMonitoringGoals.js
index a1e724c3c5..74204d5d4c 100644
--- a/src/tools/createMonitoringGoals.js
+++ b/src/tools/createMonitoringGoals.js
@@ -148,7 +148,6 @@ const createMonitoringGoals = async () => {
FROM grants_needing_goal_reopend
WHERE "Goals".id = grants_needing_goal_reopend."goalId";
`);
-
};
export default createMonitoringGoals;
From 4e57217904f3d43d67c1900ff427463b565720dc Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Thu, 14 Nov 2024 14:24:39 -0500
Subject: [PATCH 007/198] rename migrations
---
...8-AddMonitoringEnum.js => 20241114191330-AddMonitoringEnum.js} | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename src/migrations/{20241110022648-AddMonitoringEnum.js => 20241114191330-AddMonitoringEnum.js} (100%)
diff --git a/src/migrations/20241110022648-AddMonitoringEnum.js b/src/migrations/20241114191330-AddMonitoringEnum.js
similarity index 100%
rename from src/migrations/20241110022648-AddMonitoringEnum.js
rename to src/migrations/20241114191330-AddMonitoringEnum.js
From ac5ff7043c5cf377a2cfcaa89970b67594bdac09 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Thu, 14 Nov 2024 17:52:08 -0500
Subject: [PATCH 008/198] add post processing and tests
---
docs/logical_data_model.encoded | 2 +-
docs/logical_data_model.puml | 1 +
src/lib/importSystem/postProcess.ts | 39 +++++
.../importSystem/tests/postProcess.test.js | 158 ++++++++++++++++++
src/lib/maintenance/import.ts | 6 +
...114202733-add-post-processing-to-import.js | 42 +++++
src/tools/createMonitoringGoals.js | 2 +-
src/tools/createMonitoringGoals.test.js | 128 +++++++++++++-
8 files changed, 372 insertions(+), 6 deletions(-)
create mode 100644 src/lib/importSystem/postProcess.ts
create mode 100644 src/lib/importSystem/tests/postProcess.test.js
create mode 100644 src/migrations/20241114202733-add-post-processing-to-import.js
diff --git a/docs/logical_data_model.encoded b/docs/logical_data_model.encoded
index ec5698656b..b348ddc176 100644
--- a/docs/logical_data_model.encoded
+++ b/docs/logical_data_model.encoded
@@ -1 +1 @@
-xLrjSzmsalxENy7IVaZYccnjaijLdMhlQcLPTbndoof9pjOcbL9193I3c0Hc0L2Eaij_lmB05m04IO3ao7Q2JzA0O7VpGQFHwCRBFnW7b5TbaQJfPocu9oXFzvJS5h1awNl4Tod0smBcQKR9UvRUGSYJD6Nl4Du32igqG1ZoXprC2UKxOZpA2i1O-bkIfcdwbD0SqbDI49h-vPjl_lFal_wcbL_lKFOEGyRIhvCq_LsASYhUXYn9Qped6KVtY3zSmWpj4SIS_PQaJrAKfySfmXnk8KsFqxzl8OOKS9Z_Jv9kAS3yfLdXykpvsUNP-UdPLNhqGZndHtz5UP8IleFq37F47qRUUDFMGh1-yKaAMidsX8xnEp4gjkzd50g1JtXwd79KGiP1kLVOYv_fhmJ3z3Z5vFtdNuZwrJxth_zxajZu2UOd-pqbzv1EWxZy-oe6wSXRRncaZuD5ciC2JjLEKaouU8V3NtYICWvunTBZ5EAg74Bpb1G5k2KKSFBdGdG0Z-UeW8-1mnqguCXRKemG77o_V-YxNn1GF4Umo2yXsnFCPe0WI7W38Igt7xWbeBY6vRu074eXBYWfzwFsp_t6OI1BQOr9tRL-_6aCnKl8I4MpHmMGrZn7tcx5EWn4YhXiYiRaA9Z4V_BdpGXKDyRhloKf1gN2NPOdajCT5OKrINzgV_-_BxySQwRXbfnkGKBH5c5GJBISqLPBBSSsWDUwRPmLXC6hYkqXpay95bsX7MpqPvSGC8ARWIU5E7y6vBWac2GfOlBBOF5mbYCEnQVS4b9085wv0mMJTIuv1tlG4X4AqOriikNQP-r0TmzejWFqyw-__VOBJMUQzmOwxPlRdw4Kw9HKT8xXQmbuDsRxHjTIalS8WLgu3pasjbBF6oJNWEbZ6pPR2UTR_JLW-GJjVn-vtY2WctzUY3ySN51oZtAO3um3KrGTWq7_bOcclYPxb7sOTkQ2zSZxIhGFqW7y1qiV8FgVldJO9BEu7dPI1-WR4KCmvFbgzZZRVk6f4lexI64tjWuEawpQ2w2kHIIa8INxMyjJwtOJG-byAnZUe9-WpHisTMUdh_xtj7tybi4yIK6U8uu81iKvuIASb2bT4DDyx7gXjPTks-Xl4YSiuNJh5e3njmh3fmz-q_sGm025MqJmQCEB2e_KYv-a8Xg5P4_inFawqJIyZllLVSpiVrOpjxqEg3ZNf5CEYgmIq-3yoeZtDDhIMGu10g6NfEZwUGuBT0_fmsl8ToJlYPPN573rOJdCK0cApx_PVFM5IrsxgN7Vto-5qiWgFpE2RuLwu6q5jZOMukK2Ra6cLCQKnQdp6bTSbRUGlhcx09IZ3Er5VBHha2JTWoqK2C5Q355YrBGbsLEofQ0KRUPwJ1lZst_mxmeoBfN0ihGYNcr0mwhR4Z6XErRZoGUSGnD93kWMyWko5mqk5Dp1Nhe8pWSSEFHFFBb7b5UWU5OhWqjBMtklnmk5AeLRUfs2jzKtmfIj6I2Zam4ftc5z8LqigIYJQ9HWpwx9o2pF8jZ8VmyAb3yVHUpxkifSgrTFiKsTVIBwmHvi0OTly6j8jz2OhdDQmGC5ipMGoxB2A9Cg_ufiVMUVcVNUwEJQhFeHpJTxedbyEMq6dD4Xiab67IeqB-hbW8FExeT_krz0Ph9JBGgT6A6d9-bU3FQCipVuxRuWm1c-Eco7EBk7c2kxU84Ami-1yXzE8UQGodRoK19eKHbfUfjX2oKsE8zGC4Lyb6oGATkvsxY5Udf2O1UfmRRnCrJwYAXvBM9mzlxmqxUNbsVdrpVNBr-VNLsVlBvmzJ_o7fk8kztf06WSRFiEtvvazeUrY7cRSq-J8qps5VeLcb_p3Zuuchy9IGWP1ZJ4pA86cp0HZW_DiMQM4p0wS0tu2o5PWayjorqW_EufoDwr-cMW7Pp5387gmsKg2Jvf9eINUbH7Fh5A5cSrG_EO8H0UCcxZ1aaVbqsmGaTihvZFqipcL2xJcRqKXLUeH0MWY3-yeAJQ-wKmm_NVOZFngbNxGA4u-4jML3dCNzwT4twIbmWx6ldt4R2cWReBJVJEyyBNnKKPz-y0Uudb318Cv5jx4ImBB3a8dku0tdhOWv3nPErizlonnuwNOVZ-oXqn9qK-yAGD2YaIGzwYV2334_reZ86xQ-3mxD7Dgvv3KxYffRde4AQNjPSYQfCbCv7WlT00L7NOKRrt9pxVI53i4ExOa7qJ36k2WRMkrwKotEV4jSlfDJ0S65rYC_J9Hirket1NjM4LElZFnUEJIE8uWPaQsFNHVEBbTbJlrNCrpNIJ728zqjP31BVPWcPAj0DiftVwMj9pCt2Y5xycERf3gpMYp3VL-O29bXwSkVkvI_ELNGMfBW25c8Uu4I5BmJycJxcMv7i4z7IJn_IklrxiEINbLGaePpi8-JK75mN0-fHdVBaOvUuy-z9bP1BvSvbtUeNmktLNzVitvROJ8tYa3GiyIlakuRu06IphVERYwAoJw4gnsaD6OMsvFPlV-j8fRHW2Xgho_4MGIgct4Dw-M5qJvplUjFj4lT-RGFVH3H_d3ZSXCrImwOdBOiq0umeKpokS8xnzJJ3VzKXcEN8Pw5gDNOywqpfi4gIMGdSuL3wxsiCCWG8nVbO2L4nLSX92M2Q5fJinAP5UsLhpL2VffqJgLNCAfUVdHzm8iOZWbEJEznq3YAR7fDf_a5ZmL9Z62CpyWQB1CWhcjXwEQKoGIGp3z40JDh_0bJ_x6QEdyBqkgji2PSPImnsWCFydaaRBt7xsKYQ7dW4EV1C_AftqV3mJ_xAj50owaU59ZzFFNZuxExz--VpbsRFFZe-Fv2pEalf6lNFaZv9SI83lI9SJCoPD56Q4vgUaKckHwYeisy4Iz1LR-rYFiAN_2WQ5z2JAuQi0o7iiKkKdNCm1zNLPwN7wjpfmb476XYluDzBS1kBxvwDjPA6_Ph_BuHt2I0zNoMGjnE2sW0CB3Exu_WfoZl3MUCVgmaA3ycf624wepMl0thKlxW7lG_4Qe09ISrNAhKYfSlSgC6tajfU5pKrxjsCB52E_d6VOwzkUf3-JMyUImYTYcPs8iIrsrYXaLS7RbOMioeGsQnPHHmnpY07Ee0zidABj5j9s6MrRI--L5issTm8EjuG-Q93FMBOZ8pTwwDL_NBXICDnLXHC9WX4d5E6jt2z_LPKbe0Xw3zN6ZDSrLcipywE_7cdl7Ad9m3s2xr_cRWATS9UoNaEbUUieQ82HSORJeDflVTCxdZlt06EKaF8DtGACVhLNzNwx-GzlVmkMH2sikL8R1tfaZ84DOSPfbOs_GQK5QDQVJdlW4g5dGRFrha6V8pcbylOBxrJzQKsf2JUgqhvtrAeiXDDKNgYAXjgAzuwwKN1aWEv9en9L3RNc_QEm1JMIKfr_QOOHA-RxCUWuON1wUfiHIg7KnsSGvAyGXii3PBK2Ut1lGuxwHjQ5clmTOkAtbo-uN9N3LL39zp2YYOzeY-yCcLgM7uSA-obR3Zca9SB0o94Q_H43AdPkt7UQvaZNhgXyUkEkw1VtTUrGaUtnx_9oIWxHCMPEt3yLJLv50zTMEjj8FfhANN60Sq3pntDAMCtHIsaRyJH5hL_-n9pXHzJDFvnPFAWzZ8PmKTiygjs8qv0u3N7CM5UqSlQC-V0sJaqlwi_nZaCjdjH_Oj4iLUyQbV23Z_HF7u9rewatAVzZ66wjDLRiE9OkF5Q_-UFuElhthEtgzB2Adavmo_XjWLvi45TjLR6ve5vUq3JP7st6PA_Rj29IchgIYcT4Nw2f3lRb76IxqeGfC3MW6j3m4GCqhdz2XqC7ZNdmkfQkDna_lAqB2Jgi6Yv0N2ItpKzTqai-lea0ucQ4rgyNRIkKYqpkBFtLDp2rZJtlU82FRMj_-F8eIx2bwvG7llEGQwkwJu6_y2iEzo7RSd3zpbZJrLK-hUquFJ3wMt48Tst5cNXHRiFfrR6Ru36EjlFMAVsuT_NpUivIZOoKUs5Ud2EVTZpkuozTdNTgunzd2UkC3OeVo-wlQslhfr7-0VP_zJ7rs-tbjqzBtzqrvu3JDJQ6BhW89BGN77hxcBOjUzvXgBDtdzqXolXfRH1zwhx45s9tWLozEbtxdF0hCSdmawF-DY_zzvHPLFKMMLJsbbXKzXLuS4lUq1uypPPq7LdNv9LcOXu7uiqxIlr2hDcGHptwXI_2Z3ZBoEW0UoMUVyINi5oe3JeNa3A2CIoMEkisFUCKQMYUbXFwXRsD8lK2Ti6mTxIMEcrrsUfYU7UdxFra-0CwTpyfm-DIPjc_NG_s5DwXNnndHT-vYflkwTxtLBU5QmVV8NOtptVMkW2MTVIi6w7sUDZHZMIE9_-2W8Pk7XkvE-n3cRQnZOPqzCZZ2YAhJEpe-EpuMFm6cJWxznxG1uItEW1RJzk9nyxdkrIdyvvHQanYXZOjh3kvBTt7B9CzRCzPji-JaT8-9Vb3kno_ERRBMUZSbrXHD8A7VqJ1VxjF2H2UavgZHVkMz2RKiKFgcrfjBo47vhc6JObV0Az49NIgfTlB4EVqU5_v478aySTmcOFl4Sw-t17hzjYQe-m2Y-FjkbT5uHpDR6UX-txb5nt0TymXooZQEtlrofxb5PHkfNRegE_Eof2zWQp7PSpTx5K-i1xt6TPxJH3UTdcbFpictiS2ppaBUCrZQcUVFTjzF2glCaAXF7yLPBjLhPm82trb-R4455_Rn-hMFj2DxQqn8LdlKeNWzaKSdXJGRolg8T8nFgT9F4bBo9Ywww7DpQdJUPkTl4rciDt7sttOZQZQ9ECzpcQOU-2STSZlFQ08I7VGtBlkxGO49eKEEIWEZtrlcqC8L_PhFdpcJx2eDqU_kOUKeXoktKUgZiN4AyqkxKCTy7aFYWhsCmSwQn-DbI_rLJ5W2pleja3V3X-v7TwItxU7EQhvm0UoTfoyWovVZhkc4hxDrnxA6e37D00xKiBB0z-Hj6Fo2XuwOCB4zEqLsQffuFgH6dUSgm3RBccS7gSdbvSd__hnfsPSaU3To5f7ubm0yGad5TyHAYF8ZfC3cezqtKVa30hj28RdpWEWZ1Is9ed8LmDyBm-iG8oZZ3Lho6-4Jt5JYZjNRaXjneGz5-T2tPI0lfCLn1WfJp9EqdzNe43yuGOmLb6nXkGc1sntIm3DcxGWA1HLxG6d0664jpTggHNJdZM9SrrScIMN1OrEDlVEB8tPBhs_1Z7bbhgpJezuCjmF9y_gsPy7ADrKaT1UxE67a-Ueu93wPTkh4ZkdltAbrUYj5J-Sl3gnBolpY8u9j_r6go7Bok86i5xHxKElg0DoOcWHTN-HRAfAgOOiqbSwT1XQpZQ6xsyeUNzZpZ18aJjAw-dS3KDRHqkyCXThrFbF6c4QXMbWqKY4gW-L5RM94FKX6hlV8-BbniiHuOzGkQ6A48r2fAq4jEvbYsYDe1NjkZzMqLY5uML792DO8s15d4PnYdtF2wO8ZmNFo9NG4I0R8ElAX0YG0cZqXkS8aWka250Fe3WhSS3oxDah3qZoW0_BhGyJ5wq3dDzmiEt5daLb2MBxQOFVhBR0tA0SoYqi_MMIwhaptwPEIhzn9cKcy0cwJJkgb0ko2905K1p07A2CN8KT0Me0Yj7jAOJYD7K4dF4he2DE0pX5Z5mOkIzzZW0r08tVpXnnid4QEymWecMZ5SSv3v0YE0vm8YWsA8CNzKPyMdo6x_MD8Bb0YW2v08gez50PllKr8jGY59Ld49g8YeNAu1Mo4PuEdWZD83GH5WkMa6jS_tJ8X5pN-gU3P_8EruYJYTF0xQCH9YTCZgij8r8ZIBdlOOoGJKCMLMmIhwXLFYWvBbI8I0qX5MrL45RHI4q25H4QHCXzJrz4oKHfw1B0Y9CBfwXC7Y35WIuGgjZlfd2A00q2D70_MuJ93LE4o88exly8X2Z2OyCpV5jR14W7A09Z0JDrrrf4oK0fhWATGYWYOoFZlZCgWYF0STvH5Q6Hq0nUK1NnqV29UcFX4ezJ7KPc6msD2vOuQN03uxY7AIBJ81D1iK2nRtQAg4XKfkwAYZ2vOPguJYh2veOjGcH3D78lLKH88TI_rYeY8v29teyB5SSrLX6O1fZXMx0YHXP64Hi7DiDtRqMC6emYA1Oe_GkX8Yu25_UzK14RXOrzqzMV6fVai_I4CP8XQNjPaYHodfuUVNMKVldbZt-vBK3sRhCl0C8RlaFPEmmP---lV_hoIrb5sm8iv6f_RUJrnedQVUPBp3uz9JrEjw9ee_vqJv9avEudYvkuP0xc5hlVqlG87wMz0yNJOt6zcLcsIS4WiyHnjKi0_waxXz9ZYxUeid6KcQ-jBbKRAlOMg7qZ6ReYORP0zkKmFPdQ2fkX1_MR1vIjwH6Xaidt3YZNS4jEo_OORQfcdZE4KozQaiqJgCcZzQJGcJULLk5eNFFdOjiyMusRQJ77FhHNUpKPt7raazBkwMgyb0sS-avPQpMUMh-OOjnLNOtxS4-tcG_rwn3UxKAxz3XbUvWXK236sz9DTqUzLcQAaVHkhvWvagjtZ-jROEqd6rd-d9AozyuY2Ng7JWllnIhR6Dw5uB2CPU_49-anMOKhCtMl74oFwrFphGTEvjRr4a7DxBY_GpfjPlccLwBCIUCohEXxPFA_ILSZd7F7ebDOnQfHRF4hwHsH7y32uqO2CGSHjJRgptZbHS8uSZh2cEYbh9R2EKS97tZkv4H57swMqUrvVpe3oyKBvJIa-Og5Mzj3UGpaRBjMa3_hgTrouivCfs3cAcycb8uZjGEDjElayjfMMuyshaAM39t8lEx4Ro-9DjhJpUrMu3x1pyKfbBnJGWLoysXJiro7KN-fTe_TThJwAbBeJjSmyyxXMiApuS5CuidowNv3UPCIAc9Qe1YQval73Z26jXZc3aB1mWnvKPkUBpMFTa_j5N5JwKE9sgskX3LRUKKhvJHBy-7kcLCngBHjFcW1WzdeX_otNyBqqbbqfUq25E8w5fNjK97FGyEOiBNkRDs_Wgdr5s28iKsKlC5me_wxVayKTLaak_5xalJYeFvCXyv94rxamAD-T7KL_kRn1WP_yz40V_kKwA1vaUlzpe-s7ZKEjLMykXXjF5ils_IAEjwBmrTrzDmHjGgctngWEY2lEbWm5GSaLFtCNoZAwcTpYiZ6V6mYPrRLdysHh3qRk9b4grzr0uLJj3ToA1_BA5fActKqvyjPuHpU8GppJZqBbGpQeJPA0r_qdYJXtlTWl6BJ3rlCfM-suKPloaJFLlcJjKu1jl9So7nQymYdRzBUQ3nOOXqE6cAFhJUFKqxHTWifp9y-RBeVYSSvVxE60VjyHMCWw9tFjUa_RDzy717RK_fSWbfL6lT53bhkK-oM2DHgybr7q05GhTlhynzji_3MMYnFUv34W0wfXniThWD8uUG-nNKb5MqrWetrc265M4c8iGxYrpDM4cIe9PoizJF9Lcc2RKVGfmqMym_mfqiNryHvejRQKukEz6Pmmh6Y9AEMoYtBYA5QAira4zcoc2-iBebB1BPnIfMC72MsG0AdhJ0V3VJNDQKOmC79rpZKQQtHQjHSjnLNbS8zM3Qb5kpMCJkhPv2mKUgNIfelavyC-80yk-ppkSMr2K5OzPCkVnwUSzV3ZxWksxJ_TM58HuXynLWxiGid0-wBv1cCrVNaIDPSdsHmw-BZq-kJHYBbskAFMQ9EP-GtxkJyKdvTw5NxVXb_rJz9FmTbQK2BjCGUd66jkSpCzjMPTAgLz08wYs91ZIMuwCM4bEMmovCsvhD626hJYqKb6j3pqz8ge5SJwkUbsJmk0saAZP4pJJ6FL-FWwzILDp-BdZiDK1Vg2wEI-foAsKPA2Ires5Q8pQ1jfYOxohfHdqMtmxScs80RIiav33LKIN-y2UUW7LEaaTvtfJDeDlv89VvkqXqfeIAITEFuMckrQVt_-0hjvNoTouk7c3lM4o-Nm8VnCdx3s6uNepcqXC3aqLy-C8UNosli-FIzHhKlOjcU2ahK8YNhIJ3jxcIMbYXjYuAfjVjvwzvOx_OG51DrgCTiKy74wwQbS2rlMt5fLhEqLr-fjJhFmHLRJXcSjPEw_TRRAS35FrFx0eAv6gfMiz1qJT_E6wXozavfYPyisaWrj9ydQiHOOr-18D-iA6lB5zAt9nqw3tEoiOoFNCTUyPrOKrm0nj_kSx5JJsInQEtaX39mEQyDYxxd6ahTBpIYuF89Y8JoTosbKjpwIwdcqBONz7IqZIVXj5rPuffuFqaSIdsi5V0Ag0zEnXMBWNbuA1Tpj56azS4cb5vt58fOqKwXMhhFsj9WWlROkkb6s5cq74Qp8xlOOheE86Wvt5iFacRN-SBjAVSrcVuMYLaisbUQqEFtBVqcrD2KJ6oVnKVfxnfFmhc3PpD6DfuOCpzZO0-gFsc7Tyfyjclf4IrlRlPk3jPRjByA6ttD_e5OUE6sBalJIs8XMzkM8RU4eBM0sWwNB21iv8TN9JqGyluduZjIDSknpV9UuQho6C5MFMwArcNafTJEtNjpzI7psHeOe1Drm5BwBwkJ7qo5hCl625nTxoYfjhkUZxan7_VyDVEvsrbo23x0RgJcz2x8KI5SknJdMgj2p1xgHOVUD4Ptmx5sZYrx-TNmXYbHqpaDTs-acjjVcFyUgRai1ZOBBJKYPZXx9JTbgsy_cDV1x2NMyurD7LyFRc9R7DMmlHJvN7fuE0E-5FucY6dcD-SAgcK0ssY_VpTMSSGw8YXTbwrA791YcPT5zGZgt6zJH9fWvrmgwjkIv1yGvrLP9FyF
\ No newline at end of file
+xLrRSzqsadxdh-0g3vwuZcTpcMpLg3AfKMJ9SQqlAadEr2ILKa4a3WzY4ZW3W7AKRFxx5W1U010a0P97jXC-IGS4kviV6er6Dox_P1nGNXPv5CUFOd17K1hlARdDOS7YTuZlOe2p7APnGiax5cyXv54SixS8xm45HPmZ3Fdz7iQ4yXqn7YKvO2p-BOhZEFwMq1JIKv8J6d_ovPV_S_BtpxJownrAtcB8MFn55CU_Ir5EnTkGHN55voJXw0xn-qkOe3s2cBFVelXnZDBuE4QOmmpI-3Z-EeCOKi1X-aqKPnHW_joiS7bsVdPvzkhqx2e-UYQUiwE_eZIA2Tz1UeOvulS3RxnkQY5OlVWu1eraUqA7-9qO5Plt4mg5m2SyFeuvAY3ZeDWhxCNF-5U2ONmSO_BkBv-A-kK-zQ__UnJP-0dc9_iz9NUGZeCu__jA1kd0MsyOf0-3HPp278xLZc9Ck7Y7mrzu53CEUCdYunZYikX3yvJaERWb573ovqAq1uzpbCE7m66BSZZmbXGZ10VVx-_xxcS4vES8vkabP7k2sGGG10bl84JbvWCp0l9hMEnpmA4KueAIOZze_znl70Qoc5QOL5thnv-6KBo4Z9GqUL00QSLpuEr6hCD0f8ZBfgXDZe99_2T_tWn0TMEy_biKOr0et6Lv93bjgIYiIFfJ-_h_dZmPQgR-bXnlGK9HXZ38HrhEQAkvbYCBuBLi6oSvOV2gP7k8qrE2HLTe1XlzsIK4ZE1wu8aXJfy2EIu99b6CcFmocD-VfOX3yIctXEGGY5Us86UopYNdO0yQ8a8Xsf5Djbpph1dexW1DTe3-vOklllfKqvdwlPvE-_hsPoW5EYKbdMBu6WBUZTcyqRQKv3r0e1BkVOwDZTJJ1iabOFhGXir6mkbMVmbOFaBxtqMkCmZevf_duWy75vJSeHIcFUF065N7OD1zPQIfhyaU9U-ddRbWFV2-4kszT03_mT87YEwdpnqsoIIkXvtK0VfQn10CUJwVFCusNtYgXFuEKjYEhRt392lt0gXh4GcfISd-rl3Kkjt8K3hT2eOtw4Tey_fDtTdfY_yzRHw_OJ2DOf2cY2E2GVwAS15EIfGkY6a-ThtGs4ktRVGtYIEMSBnrSy3u6uNXj_T-q_sGm0256I9uiE55XGVgnOzZ4Or2iYUsudmHQHfUnVsgFcRsFwkPcru7L9nhr2a7ULAAQN1wPSHxQcrfBOS0WT1BahVzF8KvkeFq_YNaEv9sH4jB2hZwi1Gcg03vNxxSVFwfIrtRgN7Nto-4qiWgFpE2RuLwuAm4cOs5U3c76KYKofYoE7LUuuhBuXRIrziBG3bAq5x4Hzi6YU8zoA1025PYY1AZfYt9dfACWaAqcUinhOrlzy5_IiYuL0BBqefujG4DAsyBn8JkM8qd7t0EJIGve1da5sGl6bmek8EzT16S3pXmw9zuSeSeBq7-j586bvOszbwF5mfL2hVqEWLlgs-4AHipG0Od0rAym_f2kbXIKKRHAC2UNPE1MPv5i9R_3kGeFHz4xFkgobIhLqsnJPrzBFh17cI0mzVu9QGPDARhd9RmGC5i3UIYA35AfCg_89dlx3EntXkSN9jLFw3vPaSqZo-dzO0pE4JMoQX3XUP5lHHmqDhzy6_t2yYCTgfPeHCZzFGutel1dd7s6h_PvmGumrU7xH1dTn1pPHSlK4xuAw3yXrE8EQIodJoK15fK1fhUPjX0YGtE0nHC4Hya6sJAzifjt48zFI6mYrHls_WPAdr4rBmMCJXxV_ZfcylBirVNDzVFNvvTNP-yl73rF_8EcuYxtUa0Q1nikmxVdYJsXpM8UPjpJvCZJFOL-XMQN_CMtZkQlmv921bwDCJCeWQRC16E3ysn9fOJC3XmzVWB8Lc2ZotBdIFyzbkGl6tqoq0xE8iP0jNxorGIV59F22_rg8vyOfNCpcg6vZ5383natCODaZukcs25ZjXUCP-vcSsnNAOpUoaAhr21Sa0Hltz6IRdtIs66wx_5PkHLg_Q1Gd7mboGlKvW-txuY_2Gj47Oqyky3O8q3jGrDz4xpmfV5HHdtxm1xZkKC4lJaMpiHB0iiEGYUxW3EFEn1o7YoT3Px-jXZnyimV7VbZfYJFErvqWI558a-Rr4-aE69RZJ6mDqrS7ZswFfLpw4ftDJIL7G8qylQIn6roPAPIF1UQ03AAkoetjkAppSI53k4MpPaNmV3wc1WfUjLgGptEN5jSter30T6bzWCVR9Haziet5MjMOKE_bFn-2YIE4vWnWPsVJGVURaPrJlrN4qptYI7I8yqzH31JRQWdXBj0DlfdRwIz9KPED6BNv8KRT5YZIZpJLK-OAIbUsVkVkxIT6LNGQeBG27c8Mv4o5Bm3ybJhYJv7W4ztUGnlUkk5pkdfBWg0ELi1o6_vk0Y1rYVyWml5wFSdMUVkunC8lzAvfrU8FnssLNz_asPjM84ZxHXWKSftoNSvo21HPNFd1sTTH9TA5Pxo2YiBRTdyrjVEgODVS1GLPu-2r8fzHR2sRF5DN6wuqtRJzJxlGbq5ztmSUvm4p931UkUE2_M330kGNvUuXJXx2N1VDSXcQJ8PQ0hDNOzQarhlqcGMWhTuL3vx6eF4mHonFXP0L0-Lib9266P59RknAX4UcLhpLELf1yJgLFDAPIUdnzm8yGYWbEIEjzt322Q7fDo_rvYmMDX6YCmymUA1iiecDjwEAOnGIOn3DC3JTZy0bV-x2UCdiBtkYXlSvGOImrtWCBqNqWQBNFxsNEP77eAEF1D_AXqqlbm9lvvKoaOT2F34n_7Zvu_FNjr_Vpy-TdJHyV7Ht8MPqbzezuvyaT9BYJ0MzBcn8n94uK9eUafATLQfAeAItQm05t5rZxM8spflu918JsASlWg0F8EYrJv2HTpG7rTLdmSVrO5ZXB8M7y5_qQQsa1yVpxOHXRwI_Qx56uHHdguIYPh8GQtEUnPOB3b-ol8ESAPyOxLXOK6fUM449nGcvU0lMrUt0FUGF5VYB4I_kDHGlAomB5SmeQpU4j94HSaXVcZ-Ej4R9W-_YJzwczVVlRtR_Q4yGlLnWXdOcqNmUph_vQLlkdezqk0SaZDnIwt8bFBtOlBD8V0NcqsDPlU35bGE1Ho-c8lctiS_qnk7IiL7ulLUo9qDzi7f92LEMyMBB8o4G1gXbP737E80KwW3soSeYo3j5cTszJIExj5i-dqWCECq7iDyWchWMH0I8_e-WytLmg6RPaxaI0OSK8HpkGs_AkoA015qDi3C2RIBXT0c_0V_VL8U-T8pJ7k47ptF7KJQ8MxbFLgAlLRHq7ZbWnVd0RLVkERDVBS40WOeuGKhsa6CFhLNYpyrUPltcuSB8XQhtEbzYDqo1ZZ9SACqp8PVeDA2r0lQnrtx2L2JuE206s3dYCvK_DsDxUg_hYcL8HhLMdUEsfK5i9nAizKHKDjnNj7xU0wCEvtf57vhvfQ_VvHs3eTIQbAwzIZanLA_X1qd30uFcjE2ALGwkDJ2D8N2CFvSRTAWJsuDv5CVN_iJ4t-3Z7nsyl5dA_AuIgevBiVqCG7jCLRGwQ6vOUXmdxAba8EQO4mC5AlHd_4W8eD8NUjfZcom0igdzwu2xhRNLqxLC1y-Bkyd993T8o9yO2FHPCNqS2bbSusaW-cSbSSu1JGzE6S4bPnz5BQMZnDKQkNFp9d-05rymzd5Wzg3rT3EAjj7jHkhDEGTHhYcB4kQENi6VFXRPoQNbIVuns7cZsf_yYYMQhUDIhWUn_flxqFreswtgZy3s6ujTPOiEDOkl1G_UQFu-letxEqgzF1QlLqWbl6RsVrR8EuQAkAFGlLgeMwoVveCoPxEQWLaTBKbPGy8VgwJtLUCESWsHKcJ86f0TM0XeyOe77TWpySEcZCWvUrT9t9-E5jNKZGOT5m0U8YPsv-wfnUyk5D110t8xH-lIWhKIeokPFqLzyNrJJsj5C7FhGJ0y9NNLc2pLt-tFETXroorNuA_8LVSRWFsUs3gtU6cgwgyd7jykc0qTtXGxXhXy_8Yt9tJwUEtGGFSMAUjqxffyYhdzTJbceafDWBy-8S-RHJTnz-HUoqmo3_E2jZ9-nx_50sbLrREqgDylUmhQoBgTycCh_jN_gciIddfYwnttJ6_IAXlU3GkzAqtTxJpbYTlAbjYoZZfvL1zAdx75-8JGjoKUnuxLU4Nub9XfySrRTvwR-dpAAgjyYYihV4ehAlmBqx_VgszccsfEt8kYOlD1FpE6TlsWdj5sBDXZn1qYzy4MR4MK861zWhyVm3ljlbGktxkO2K4OnXFjTPj-aPXrD3yn2Uq2_ix3-friKBXfqJkzGXhlDM5yFjLjdxqV07T6wgKeR7fSooVxiUx2dSebySHr1lNSLrztHlfwhQmhKny12RfgSxQsq0ophHrerGS-hiuFuqnvD_Gq13zwGDN4dslapRqFv5EdYISWcHLIOsYZmwFXQ_GIAEvBq3z1xXJ0SWyxITUEZOlqjrwEyPfSOafc_JmhAJsy9zUZFPm-RuPlkO6YdjjoIzRvRZrwU1EGkTsnrBYWRmy8ye-EyYAW32Kr9pD6_Uajw4Eir8VLEZxGNam3mkiIxn2-0Lg81EjNHx6UBu7--R949EEluGxeZmFHdQbpjYxTPTSvITON6yNVSopvsZcUqijD-l_A83-8ufP0b5UsUlNjYpl8BIhTJEClNTi1covP1rk0ovcvtA1zRZBiDghudOnDRabVxic7YVDZpbYE4bJx-TVAHj-_6ek4e8XV9OLv3i5hLo8YpqkUN74f1_OP_MM_b0DxQtneHadRiQWjiNSNYIGxyTheT8n_WSKl4uBY1ZwWg99ZUd3lsjDmZNcS5cUt3xOJUYQfsCzpZoOUevTjRPmNU08Y3TGNDBlBSR49WKEkIWs3_slQdFDbpOhtd_cJ_1ejfp_VeEKefIk7GVALiM8w_KcxyFTMxe7SXBsCqSwAnzD5Q-r5N5W9tneDq2VTj_adTuItxVd16fvWFloTfnyXQwV3dkcahuDbzprci07kC0x4W1BmzyzTAEoPjzQ8y94zEt5oHJqy3b8pNkE5SEiZPf71_79vUN9__-yQTwN97WtSXQH-9S0F499oNV4IeZo8wJ0-gFT5rpv0mAxGY6hvpdIXafR4qIaQy6-4wUWu4OHmchrv3V29_YfX1NkToGoem9UowEcxef07qhAuWnKXwG7CN_AK6E-FqDOAoaOWt9JGhOxfO0cjPfGP0hgje3JW7226StQgaLqvxLYNDTN9abbmMrJjRNsSoDsSwz5WSnvPQwiquFUE3S3oTFgzcV1oZjL97GJkpXXvDpb7D8_RBjPO6Tqr-vqchmCemVJbwSs7UWUKJ7XDj-ezMGPKL-0rYdwFQ-KzG2EJAqYBg_I3PbgLH35kah7LeCBMSRmtStbBoxCMSO94yTfTLqxeQXpD4IByp56bN-awum3SBKisXaXQeBbHMrMI7gGZHqRSYAbnilHuH_XSeDrGhH84JQIa3hkhSLhG6qLhlkZzLghCBmiYEIArW3OAqu3UAqzZqlc5PuB7W6RffU0R81icvYM05I00rU7Bs2v09f1QW3q3ehSS3ozEaB3qZIW0zBhGyJzxS3dDvma7LYpwAo1B7vjDvlLbjWRb0AvHQMxZ99zToPRrC7iDyuap8Z-0HTqhsgb0ko5I0Be7K0Su0oSCFs2b0Dq1hT7aDedQOEW3Dyi_IAKmzEAqOkZDntIQy0rG0DN_gyucJZD7IOmOgMZTOwpdc0j8BZ06S5nGQbz_xV5Py6d_xhWbUGN22j0Ba0Mb7feD9_cUoAK8FGgZozqAoeMAZqtaoxmjF2iqBJ2yqAYmLBo3MkVpfaGgvh_TF1Ctg7wugJYLFFxQChJ4wO75TRhaWD8EM-Xt65R1ooLSL6yLgroWSbnuKAAr9zIAssAWYhrKYD0hKYr8kG-vw-5IKJfRvB05USN3YjokG1qZhG1KArzhind5Q0UW7gkPzjXSaDqmgH1T6w_oCGeXKU6vx-szfMW7I0Lc0iOCRkMbjHCb1QLu6E8RGYOoFZlZDg1KU0uxoZQeL6GjDxGTMAZuRFgZSMBkjnglt8PAkXSS6oJXhSm3ZkOLgHgP5PGR50iMzsMb6Hg8pT5RN2v8Pfv3gf5JOtR2j4DaG3zrAj22c4kjTQhSW8P5RlnuMAungh5PWQcFvRi5QCB8nMR1pOzD-zjM9KOxI2Oe7IlH1gnKNWuhuNQiL6OMFVTFMdnYLw59ymHX8aRI-BeaZEq-EZpu_Y9qz-_EGs1ydRpJu726_u3YLlCMJi7r_yzEI9hA9j0HPoDRysydhZIEq-yoLcdvuI7yUPADgeF-sJA1gu-ydY9YwPWwd5xhSuFi97SUT0yVXON6zcbcrYqCiiSbpj4i1_xCwX-DZYBQfi76NcQujBrOIAlGLgteZwhaYOB91zEKn7fgR2fkz1zUO1PQjxHAXayhq3ojNVakEoFKIRgfdd1E7KYvPaqmIgysWTANJcTQL5E1hN_BdOriyMusPQpA4FxTHUJ0Pt7zd4j7sw62zb0yU-KzPQZAVMByOOjrKNuzxVqstcVVqwX7Upq2u-ZjaU9WZaYF6cTDFUKQ-LcM94dTih9exawjtZwXPO-yb6bfzJKdJUcIL1vz1fuRrOPHl3kn0Svh7idFWaFIQhS66cxfM3yN5rQhxrmCdSkbuYg9aTztT8fyqittHAb3cfNOO5VOz4_ZVgQeHpRbcq2gjOrOejVaMT8_83M7ZSI23c8ABMHlqPhZnB64TEHnWJlPHLKZZdAE43prPELFJXPXxTZlUdCx1Sl51EVPglMBYrFPH7JywsRGNvivhQpIVkp7GAPgxoUaBfE58xq4YxJYwlRBNrQECgP57VYSpBJlpsacYjFUtSRW7k8_nSd4B9EoLK8DUFDXNBPHXrbsfts6M3rHUHGdUw-fct3jSSdZKFPXAFva_tQygRc50Hqm9vtpHVEdQ0CPHXc3e81GlJvqLjEhxKDDeTjLV4JQKF9MgtkXBMREK5hPJJB4s7ksDEnQ3Ij7cl1Gncen_ottm9qqidqPMs2r28grXKiaDvEWqDOyBMkBDL_mgcrbs08SOsKV8Qmutwx_eTKTHbaUp6xqZKYOFwC-qx9qruaJsFkj7LHVYRnniO_Cr7FVpjKwA3vaMkzri_sdfKEzHMyU9YDF5ilMpJAMfvpmvVLT5pHzGgc7vZWAg0lEfWmLGSa53rCtscAAkVzIaY6_EnYPnPLNysHRBcRE1w4gr-LGqKJj7UoA5yBADeBctMqfmkPubJk4KOfgrwvYePj4DDb0O-wpo9mhtltdZ5fXksc4hVRCEDtfIHdgto9skT0cpbkP3vjE8HJj-alT5uiCWw73J47bjl7iUSeYo35ARVtc_Q7v77EN-JXW7xV4LZ8EYSpxNfFcpUV1WHcrFwN89QLMhtHGvQxbFivWZKQFAs9UW0gDBjvVaFjbbuQysMfpb8eaU7rCED3jS1f71o7-Ewa8es6i66UaoVGYman507yUiP2mdIrn9ErlgPP8iqGTOIz6c3sVm3_Ed2nJLodgYrzDIY9RtfdN0iACcWfNBBCc2evaepsaIsBEOBYqlY4e5jd5950qU9pP30QIlCHptzTKafUd1myZKETLfgj1eqrqt5HQLm3vPrgGMxTSpEuXaaR9JwfPAco-Gd0puWppRx_ChYMeSWhFfHcpyFpxdhuKTSbMsQ_zemf2D4lc2idTY5em7tMVBqnkewSaHBhayokBNnyV7rIGEHSctnnoHHnpFo6tToVYw_DlIgVR-Cl-gVf9-3CZUWHHfYzywmrbHcPllgJ5hLYZe1dLcneDeIN7HYGifosELH4_DP8WIrwSIY8WtewMbf5T2h27NpqcmQbvuqXSR8cQQOngjsy7NgofkVHC-T1gWAzOLHKNLEnMGZfOGMD6mhn2RGDbEJBMNTg4-Ys-9R4Qp13QNa78PgggGxteHpq8ufqaZlsz89D6j_f1H_D_6E550HYTfnVAcrshJwVtY5zdo-pkN5VSmTwuddI-33U1w_VcopsMZsBI4mUNIN3qpXvV8QMpvThr7jYvYtfu8IjKZ9UX9CkpsPfI5Acw9WQir-tgetLhiz1CL87UensLGmyRffALpBsnPScbKlxTLNQYtEitV5bbC6P-seQh_bTWfmyS_Ole1WheQgLImqNTEtymQgdBrJMk8dYpPI3UtdaHenbjXN8CZNAygQyfdqxKc7piESRCpZ8zUnbtmdrXJdWF4t-rJir1DPB5evEQ4CNCvhpQBhkOPIzmkDABXy0YAX_5rBgHItxbAgARJj1JqjRUD9E6rNbhWYtaqIXr8VQWMy0Egzat75ecSU7ih576qKQNsm2QNN7eKYLhHJg9RkiwOqcA0zjcxQ4NOIBOVHRCdEjfYk0mWQ3dVMWwZUTJvpUyfTHQQxHM8IY-QLElJuVKE_3ZNK9IFRP_5H_Bl6at3ku5cFaRKdnipFwDc3wa_QuPqod-rQUaJBcvkzcuDr5kqlmaPVityW5bwuBSlIz99OoDQsUSZD8MWjeBR39JC86_dXHKdtXBp_XVYEbFLoRBEyKxYeF4QmLO-RecKPUIcrS_VUL7r8VBO61kX45R2K_ilYv7jJ8SkoCG9NrpjAocpNyJ4tvkEElsO-TpjB3i5xs0tK7Dy5EIY4QvVYL6kLYzb3jUZGcyRWRZZMpB5bBx_gtp0bQZgduUwDL5Fx2_FV8pMLHSzceRb6n0ndB-IcPFhsWsaTp5vYFTtOLF7ru2QsnL4DsqcHJwM7-pFWS-6lWvXcVdEge5e6i8ds-tTZ5KUi8eBITSbKue3ayxmigaTGftwR8b87EwTIKTyMHV-4E2sBFFf_
\ No newline at end of file
diff --git a/docs/logical_data_model.puml b/docs/logical_data_model.puml
index 5ccd5efd4b..a475b5fb7f 100644
--- a/docs/logical_data_model.puml
+++ b/docs/logical_data_model.puml
@@ -541,6 +541,7 @@ class Imports{
* updatedAt : timestamp with time zone
fileMask : text
path : text
+!issue='column missing from model' postProcessingActions: jsonb
}
class MailerLogs{
diff --git a/src/lib/importSystem/postProcess.ts b/src/lib/importSystem/postProcess.ts
new file mode 100644
index 0000000000..c79f492857
--- /dev/null
+++ b/src/lib/importSystem/postProcess.ts
@@ -0,0 +1,39 @@
+import db from '../../models';
+import { auditLogger } from '../../logger';
+import createMonitoringGoals from '../../tools/createMonitoringGoals';
+
+/**
+ * Determines what post processing actions need to be executed.
+ * This uses the object set on the import.postProcessingActions field.
+ * */
+const handlePostProcessing = async (id) => {
+ try {
+ // Get the import for the import id.
+ const importRecord = await db.Import.findOne({
+ where: {
+ id,
+ },
+ });
+
+ // Loop and execute post processing actions.
+ await Promise.all(importRecord.postProcessingActions.map(async (action) => {
+ switch (action.function) {
+ case 'createMonitoringGoals':
+ // Create monitoring goals for the import.
+ auditLogger.info(`Starting Post Processing: Creating monitoring goals for import: ${id} - ${importRecord.name} task: ${action.name}`);
+ await createMonitoringGoals();
+ auditLogger.info(`Finished Post Processing: Creating monitoring goals for import: ${id} - ${importRecord.name} task: ${action.name}`);
+ break;
+ // Add more post processing cases here...
+ default:
+ // If we don't find a match, log it and skip.
+ auditLogger.error(`Unknown import post processing action: ${action.function} for import: ${id} - ${importRecord.name} skipping`);
+ break;
+ }
+ }));
+ } catch (err) {
+ auditLogger.error(`Error in Import - handlePostProcessing: ${err.message}`, err);
+ }
+};
+
+export default handlePostProcessing;
diff --git a/src/lib/importSystem/tests/postProcess.test.js b/src/lib/importSystem/tests/postProcess.test.js
new file mode 100644
index 0000000000..8d5c7e42cf
--- /dev/null
+++ b/src/lib/importSystem/tests/postProcess.test.js
@@ -0,0 +1,158 @@
+import { Import } from '../../../models';
+import handlePostProcessing from '../postProcess';
+import { auditLogger } from '../../../logger';
+import createMonitoringGoals from '../../../tools/createMonitoringGoals';
+
+jest.mock('../../../logger');
+jest.mock('../../../models', () => ({
+ Import: {
+ findOne: jest.fn(),
+ },
+}));
+jest.mock('../../../tools/createMonitoringGoals', () => jest.fn());
+
+describe('processRecords', () => {
+ afterAll(() => {
+ // Restore mocks.
+ jest.restoreAllMocks();
+ });
+ afterEach(() => {
+ jest.clearAllMocks();
+ jest.resetAllMocks();
+ });
+
+ it('logs a warning if the post process function is not found', async () => {
+ // Mock Import.findOne to return the data we want to test.
+ const mockRecord = {
+ id: 1,
+ name: 'Bad Post Process Import',
+ postProcessingActions: [{
+ name: 'Bad Post Process',
+ function: 'badPostProcess',
+ }],
+ };
+
+ // Mock the find one to return the mockRecord.
+ Import.findOne.mockResolvedValue(mockRecord);
+
+ // Call post processing.
+ await handlePostProcessing(1);
+
+ // Assert
+ expect(auditLogger.error).toHaveBeenCalledWith('Unknown import post processing action: badPostProcess for import: 1 - Bad Post Process Import skipping');
+ });
+
+ it('properly calls the post process function', async () => {
+ // Mock Import.findOne to return the data we want to test.
+ const mockRecord = {
+ id: 1,
+ name: 'Good Post Process Import',
+ postProcessingActions: [{
+ name: 'Good Post Process',
+ function: 'createMonitoringGoals',
+ }],
+ };
+
+ // Mock the find one to return the mockRecord.
+ Import.findOne.mockResolvedValue(mockRecord);
+
+ // Mock the post process function.
+ const mockPostProcess = jest.fn();
+
+ // Call post processing.
+ await handlePostProcessing(1);
+
+ // Expect auditLogger.info to have been called with the correct message.
+ expect(auditLogger.info).toHaveBeenCalledWith('Starting Post Processing: Creating monitoring goals for import: 1 - Good Post Process Import task: Good Post Process');
+ expect(createMonitoringGoals).toHaveBeenCalled();
+ expect(auditLogger.info).toHaveBeenCalledWith('Finished Post Processing: Creating monitoring goals for import: 1 - Good Post Process Import task: Good Post Process');
+ });
+
+ it('properly logs an error if the post process function throws an error', async () => {
+ // Mock Import.findOne to return the data we want to test.
+ const mockRecord = {
+ id: 1,
+ name: 'Error Post Process Import',
+ postProcessingActions: [{
+ name: 'Error Post Process',
+ function: 'createMonitoringGoals',
+ }],
+ };
+
+ // Mock the find one to return the mockRecord.
+ Import.findOne.mockResolvedValue(mockRecord);
+
+ // Mock the post process function.
+ createMonitoringGoals.mockRejectedValue(new Error('Test Error'));
+
+ // Call post processing.
+ await handlePostProcessing(1);
+
+ // Expect auditLogger.error to have been called with the correct message.
+ expect(auditLogger.error).toHaveBeenCalledWith('Error in Import - handlePostProcessing: Test Error', new Error('Test Error'));
+ });
+
+ it('properly runs all valid functions even if some are invalid', async () => {
+ // Mock Import.findOne to return the data we want to test.
+ const mockRecord = {
+ id: 1,
+ name: 'Mixed Post Process Import',
+ postProcessingActions: [
+ {
+ name: 'Bad Post Process',
+ function: 'badPostProcess',
+ },
+ {
+ name: 'Good Post Process',
+ function: 'createMonitoringGoals',
+ },
+ ],
+ };
+
+ // Mock the find one to return the mockRecord.
+ Import.findOne.mockResolvedValue(mockRecord);
+
+ // Call post processing.
+ await handlePostProcessing(1);
+
+ // Expect auditLogger.info to have been called with the correct message.
+ expect(auditLogger.info).toHaveBeenCalledWith('Starting Post Processing: Creating monitoring goals for import: 1 - Mixed Post Process Import task: Good Post Process');
+ expect(createMonitoringGoals).toHaveBeenCalled();
+ expect(auditLogger.info).toHaveBeenCalledWith('Finished Post Processing: Creating monitoring goals for import: 1 - Mixed Post Process Import task: Good Post Process');
+ expect(auditLogger.error).toHaveBeenCalledWith('Unknown import post processing action: badPostProcess for import: 1 - Mixed Post Process Import skipping');
+ });
+
+ it('correctly runs valid jobs even if one throws an error', async () => {
+ // Mock Import.findOne to return the data we want to test.
+ const mockRecord = {
+ id: 1,
+ name: 'Mixed Error Post Process Import',
+ postProcessingActions: [
+ {
+ name: 'Error Post Process',
+ function: 'createMonitoringGoals',
+ },
+ {
+ name: 'Good Post Process',
+ function: 'createMonitoringGoals',
+ },
+ ],
+ };
+
+ // Mock the find one to return the mockRecord.
+ Import.findOne.mockResolvedValue(mockRecord);
+
+ // Mock the post process function.
+ createMonitoringGoals.mockRejectedValueOnce(new Error('Test Error'));
+
+ // Call post processing.
+ await handlePostProcessing(1);
+
+ // Expect auditLogger.info to have been called with the correct message.
+ expect(auditLogger.info).toHaveBeenCalledWith('Starting Post Processing: Creating monitoring goals for import: 1 - Mixed Error Post Process Import task: Error Post Process');
+ expect(auditLogger.info).toHaveBeenCalledWith('Starting Post Processing: Creating monitoring goals for import: 1 - Mixed Error Post Process Import task: Good Post Process');
+ expect(auditLogger.info).toHaveBeenCalledWith('Finished Post Processing: Creating monitoring goals for import: 1 - Mixed Error Post Process Import task: Good Post Process');
+ expect(auditLogger.error).toHaveBeenCalledWith('Error in Import - handlePostProcessing: Test Error', new Error('Test Error'));
+ expect(createMonitoringGoals).toHaveBeenCalled();
+ });
+});
diff --git a/src/lib/maintenance/import.ts b/src/lib/maintenance/import.ts
index 0099cd656b..50ffbc7d7d 100644
--- a/src/lib/maintenance/import.ts
+++ b/src/lib/maintenance/import.ts
@@ -17,6 +17,7 @@ import {
} from '../importSystem';
import LockManager from '../lockManager';
import { auditLogger } from '../../logger';
+import handlePostProcessing from '../importSystem/postProcess';
/**
* Enqueues a maintenance job for imports with a specified type and optional id.
@@ -252,6 +253,11 @@ const importProcess = async (id) => maintenanceCommand(
// Process the import and await the results.
const processResults = await processImport(id);
auditLogger.log('info', `import: importProcess->maintenanceCommand: ${JSON.stringify({ processResults })}`);
+
+ // Post process.
+ // Uses the object set on the import.postProcessingActions field in the database.
+ await handlePostProcessing(id);
+
// Check if there are more items to process after the current import.
const more = await moreToProcess(id);
auditLogger.log('info', `import: importProcess->maintenanceCommand: ${JSON.stringify({ more })}`);
diff --git a/src/migrations/20241114202733-add-post-processing-to-import.js b/src/migrations/20241114202733-add-post-processing-to-import.js
new file mode 100644
index 0000000000..ec84c54796
--- /dev/null
+++ b/src/migrations/20241114202733-add-post-processing-to-import.js
@@ -0,0 +1,42 @@
+const { GROUP_SHARED_WITH } = require('@ttahub/common');
+const {
+ prepMigration,
+ removeTables,
+} = require('../lib/migration');
+const { GROUP_COLLABORATORS } = require('../constants');
+
+/** @type {import('sequelize-cli').Migration} */
+module.exports = {
+ async up(queryInterface, Sequelize) {
+ await queryInterface.sequelize.transaction(async (transaction) => {
+ const sessionSig = __filename;
+ await prepMigration(queryInterface, transaction, sessionSig);
+
+ // Add column postProcessingActions to table Imports of JSONB type.
+ await queryInterface.addColumn(
+ 'Imports',
+ 'postProcessingActions',
+ {
+ type: Sequelize.JSONB,
+ allowNull: true,
+ },
+ { transaction },
+ );
+
+ // Update Imports set the postProcessingActions column to the object.
+ await queryInterface.sequelize.query(/* sql */`
+ UPDATE "Imports"
+ SET "postProcessingActions" = '{"name": "Monitoring Goal CRON job", "function": "createMonitoringGoals"}'
+ WHERE "name" = 'ITAMS Monitoring Data';
+ `, { transaction });
+ });
+ },
+
+ down: async (queryInterface, Sequelize) => {
+ await queryInterface.sequelize.transaction(async (transaction) => {
+ const sessionSig = __filename;
+ await prepMigration(queryInterface, transaction, sessionSig);
+ await queryInterface.removeColumn('Imports', 'postProcessingActions', { transaction });
+ });
+ },
+};
diff --git a/src/tools/createMonitoringGoals.js b/src/tools/createMonitoringGoals.js
index 74204d5d4c..35766df19a 100644
--- a/src/tools/createMonitoringGoals.js
+++ b/src/tools/createMonitoringGoals.js
@@ -143,7 +143,7 @@ const createMonitoringGoals = async () => {
GROUP BY 1
)
UPDATE "Goals"
- SET "status" = 'In progress',
+ SET "status" = 'Not started',
"updatedAt" = NOW()
FROM grants_needing_goal_reopend
WHERE "Goals".id = grants_needing_goal_reopend."goalId";
diff --git a/src/tools/createMonitoringGoals.test.js b/src/tools/createMonitoringGoals.test.js
index 589d8a6a78..1325bc47b8 100644
--- a/src/tools/createMonitoringGoals.test.js
+++ b/src/tools/createMonitoringGoals.test.js
@@ -1,3 +1,4 @@
+/* eslint-disable jest/expect-expect */
/* eslint-disable max-len */
import faker from '@faker-js/faker';
import { v4 as uuidv4 } from 'uuid';
@@ -52,6 +53,8 @@ describe('createMonitoringGoals', () => {
let grantBeingMerged11A;
let grantBeingMerged11B;
let grantBeingMerged11C;
+ // Make sure if a monitoring goal is closed but meets the criteria for a finding its re-open'd.
+ let grantReopenMonitoringGoal12;
const grantThatNeedsMonitoringGoalNumber1 = faker.datatype.string(4);
const grantThatAlreadyHasMonitoringGoalNumber2 = faker.datatype.string(4);
@@ -68,6 +71,7 @@ describe('createMonitoringGoals', () => {
const grantBeingMergedNumber11A = uuidv4();
const grantBeingMergedNumber11B = uuidv4();
const grantBeingMergedNumber11C = uuidv4();
+ const grantReopenMonitoringGoalNumber12 = uuidv4();
let snapShot;
@@ -243,6 +247,16 @@ describe('createMonitoringGoals', () => {
endDate: new Date(),
status: 'Active',
},
+ {
+ // 12
+ id: faker.datatype.number({ min: 9999 }),
+ number: grantReopenMonitoringGoalNumber12,
+ recipientId: recipient.id,
+ regionId: 1,
+ startDate: new Date(),
+ endDate: new Date(),
+ status: 'Active',
+ },
]);
[
@@ -261,6 +275,7 @@ describe('createMonitoringGoals', () => {
grantBeingMerged11A,
grantBeingMerged11B,
grantBeingMerged11C,
+ grantReopenMonitoringGoal12,
] = grants;
// Create an inactive grant that has 'cdi' true that points to inactiveGrantThatHasBeenReplacedByActiveGrant9.
@@ -335,6 +350,7 @@ describe('createMonitoringGoals', () => {
const grantBeingMergedNumberGranteeId11A = uuidv4();
const grantBeingMergedNumberGranteeId11B = uuidv4();
const grantBeingMergedNumberGranteeId11C = uuidv4();
+ const grantReopenMonitoringGoalNumberGranteeId12 = uuidv4();
// reviewId GUID.
const grantThatNeedsMonitoringGoalNumberReviewId1 = uuidv4();
@@ -352,6 +368,7 @@ describe('createMonitoringGoals', () => {
const grantBeingMergedNumberReviewId11A = uuidv4();
const grantBeingMergedNumberReviewId11B = uuidv4();
const grantBeingMergedNumberReviewId11C = uuidv4();
+ const grantReopenMonitoringGoalNumberReviewId12 = uuidv4();
// MonitoringReviewGrantee.
await MonitoringReviewGrantee.bulkCreate([
@@ -524,6 +541,18 @@ describe('createMonitoringGoals', () => {
sourceCreatedAt: new Date(),
sourceUpdatedAt: new Date(),
},
+ {
+ // 12
+ id: faker.datatype.number({ min: 999999 }),
+ grantNumber: grantReopenMonitoringGoalNumber12,
+ reviewId: grantReopenMonitoringGoalNumberReviewId12,
+ granteeId: grantReopenMonitoringGoalNumberGranteeId12,
+ createTime: new Date(),
+ updateTime: new Date(),
+ updateBy: 'Support Team',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
], { individualHooks: true });
// Create 9 statusId variables made up of 6 random numbers and set each one to its corresponding statusId below in MonitoringReview.bulkCreate.
@@ -541,6 +570,7 @@ describe('createMonitoringGoals', () => {
const status11A = 12;
const status11B = 13;
const status11C = 14;
+ const status12 = 15;
// MonitoringReview.
// Allowed review types:
@@ -779,6 +809,22 @@ describe('createMonitoringGoals', () => {
sourceCreatedAt: new Date(),
sourceUpdatedAt: new Date(),
},
+ {
+ // 12
+ reviewId: grantReopenMonitoringGoalNumberReviewId12,
+ contentId: uuidv4(),
+ statusId: status12,
+ name: faker.random.words(3),
+ startDate: new Date(),
+ endDate: new Date(),
+ reviewType: 'RAN',
+ reportDeliveryDate: new Date(),
+ reportAttachmentId: uuidv4(),
+ outcome: faker.random.words(5),
+ hash: uuidv4(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
], { individualHooks: true });
// MonitoringReviewStatus.
@@ -881,6 +927,13 @@ describe('createMonitoringGoals', () => {
sourceCreatedAt: new Date(),
sourceUpdatedAt: new Date(),
},
+ {
+ // 12
+ statusId: status12,
+ name: 'Complete',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
], { individualHooks: true });
// MonitoringFindingHistory.
@@ -898,6 +951,7 @@ describe('createMonitoringGoals', () => {
const findingId11A = uuidv4();
const findingId11B = uuidv4();
const findingId11C = uuidv4();
+ const findingId12 = uuidv4();
// Exclude findingId10C from the findingIds array below.
await MonitoringFindingHistory.bulkCreate([
@@ -1092,6 +1146,19 @@ describe('createMonitoringGoals', () => {
sourceCreatedAt: new Date(),
sourceUpdatedAt: new Date(),
},
+ {
+ // 12
+ reviewId: grantReopenMonitoringGoalNumberReviewId12,
+ findingHistoryId: uuidv4(),
+ findingId: findingId12,
+ statusId: status12,
+ narrative: faker.random.words(10),
+ ordinal: faker.datatype.number({ min: 1, max: 10 }),
+ determination: faker.random.words(5),
+ hash: uuidv4(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
], { individualHooks: true });
// MonitoringFinding.
@@ -1222,6 +1289,15 @@ describe('createMonitoringGoals', () => {
sourceCreatedAt: new Date(),
sourceUpdatedAt: new Date(),
},
+ {
+ // 12
+ findingId: findingId12,
+ statusId: status12,
+ findingType: faker.random.word(),
+ hash: uuidv4(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
], { individualHooks: true });
// MonitoringFindingStatus.
@@ -1310,6 +1386,13 @@ describe('createMonitoringGoals', () => {
sourceCreatedAt: new Date(),
sourceUpdatedAt: new Date(),
},
+ {
+ // 12
+ statusId: status12,
+ name: 'Active',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
], { individualHooks: true });
// MonitoringFindingGrant.
@@ -1519,6 +1602,20 @@ describe('createMonitoringGoals', () => {
sourceCreatedAt: new Date(),
sourceUpdatedAt: new Date(),
},
+ {
+ // 12
+ findingId: findingId12,
+ granteeId: grantReopenMonitoringGoalNumberGranteeId12,
+ statusId: status12,
+ findingType: faker.random.word(),
+ source: faker.random.word(),
+ correctionDeadLine: new Date(),
+ reportedDate: new Date(),
+ closedDate: null,
+ hash: uuidv4(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
], { individualHooks: true });
// Retrieve the goal template.
@@ -1558,6 +1655,15 @@ describe('createMonitoringGoals', () => {
goalTemplateId: goalTemplate.id,
status: 'Not started',
});
+
+ // Create a monitoring goal for grantReopenMonitoringGoalNumberReviewId12 in case 12 thats closed and should be set to Not started.
+ await Goal.create({
+ id: faker.datatype.number({ min: 9999 }),
+ name: goalTemplateName,
+ grantId: grantReopenMonitoringGoal12.id,
+ goalTemplateId: goalTemplate.id,
+ status: 'Closed',
+ });
});
afterEach(() => {
@@ -1582,10 +1688,7 @@ describe('createMonitoringGoals', () => {
GoalTemplate.findOne = originalFindOne;
});
- it('creates monitoring goals for grants that need them', async () => {
- // Run the CRON job.
- await createMonitoringGoals();
-
+ const assertMonitoringGoals = async () => {
// CASE 1: Properly creates the monitoring goal.
const grant1Goals = await Goal.findAll({ where: { grantId: grantThatNeedsMonitoringGoal1.id } });
expect(grant1Goals.length).toBe(1);
@@ -1657,5 +1760,22 @@ describe('createMonitoringGoals', () => {
const grant11CGoals = await Goal.findAll({ where: { grantId: grantBeingMerged11C.id } });
expect(grant11CGoals.length).toBe(1);
expect(grant11CGoals[0].goalTemplateId).toBe(goalTemplate.id);
+
+ // CASE 12: Reopen closed monitoring goal if it meets the criteria.
+ const grant12Goals = await Goal.findAll({ where: { grantId: grantReopenMonitoringGoal12.id } });
+ expect(grant12Goals.length).toBe(1);
+ expect(grant12Goals[0].goalTemplateId).toBe(goalTemplate.id);
+ expect(grant12Goals[0].status).toBe('Not started');
+ };
+
+ it('creates monitoring goals for grants that need them', async () => {
+ // 1st Run of the CRON job.
+ await createMonitoringGoals();
+ await assertMonitoringGoals();
+
+ // 2nd Run of the CRON job.
+ // Run the job again to make sure we don't duplicate goals.
+ await createMonitoringGoals();
+ await assertMonitoringGoals();
});
});
From b69906d2479e25118498c7ef88b21d666cc9d07d Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Fri, 15 Nov 2024 13:32:57 -0500
Subject: [PATCH 009/198] rename migration
---
...0-AddMonitoringEnum.js => 20241115143738-AddMonitoringEnum.js} | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename src/migrations/{20241114191330-AddMonitoringEnum.js => 20241115143738-AddMonitoringEnum.js} (100%)
diff --git a/src/migrations/20241114191330-AddMonitoringEnum.js b/src/migrations/20241115143738-AddMonitoringEnum.js
similarity index 100%
rename from src/migrations/20241114191330-AddMonitoringEnum.js
rename to src/migrations/20241115143738-AddMonitoringEnum.js
From d4a9f267e3f06b1e5618c04321143cb3a57269d7 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Fri, 15 Nov 2024 15:30:08 -0500
Subject: [PATCH 010/198] rename file add proper cli running and wrap in txn
---
package.json | 4 +-
src/tools/createMonitoringGoals.js | 225 +++++++++---------
...oalsCLI.js => createMonitoringGoalsCLI.js} | 0
3 files changed, 116 insertions(+), 113 deletions(-)
rename src/tools/{creatMonitoringGoalsCLI.js => createMonitoringGoalsCLI.js} (100%)
diff --git a/package.json b/package.json
index a6b437a0b0..11107a9ddd 100644
--- a/package.json
+++ b/package.json
@@ -104,7 +104,9 @@
"updateCompletedEventReportPilots:local": "./node_modules/.bin/babel-node ./src/tools/updateCompletedEventReportPilotsCLI.js",
"publish:common": "yarn --cwd ./packages/common publish",
"merge-coverage": "node ./src/tools/merge-coverage.js",
- "check-coverage": "node -r esm ./src/tools/check-coverage.js --fail-on-uncovered=true"
+ "check-coverage": "node -r esm ./src/tools/check-coverage.js --fail-on-uncovered=true",
+ "createMonitoringGoalsCLI": "node ./build/server/src/tools/createMonitoringGoalsCLI.js",
+ "createMonitoringGoalsCLI:local": "tsx ./src/tools/createMonitoringGoalsCLI.js"
},
"repository": {
"type": "git",
diff --git a/src/tools/createMonitoringGoals.js b/src/tools/createMonitoringGoals.js
index 35766df19a..f907640c33 100644
--- a/src/tools/createMonitoringGoals.js
+++ b/src/tools/createMonitoringGoals.js
@@ -2,11 +2,10 @@ import {
sequelize,
GoalTemplate,
} from '../models';
-
import { auditLogger } from '../logger';
const createMonitoringGoals = async () => {
- const cutOffDate = '2023-12-01';
+ const cutOffDate = '2024-10-01';
const monitoringTemplateName = '(Monitoring) The recipient will develop and implement a QIP/CAP to address monitoring findings.';
// Verify that the monitoring goal template exists.
@@ -23,131 +22,133 @@ const createMonitoringGoals = async () => {
}
// Create monitoring goals for grants that need them.
- await sequelize.query(`
- WITH
- grants_needing_goal AS (
- SELECT
- grta."activeGrantId" "grantId"
- FROM "Grants" gr
- JOIN "GrantRelationshipToActive" grta
- ON gr.id = grta."grantId"
- AND grta."activeGrantId" IS NOT NULL
- JOIN "MonitoringReviewGrantees" mrg
- ON gr.number = mrg."grantNumber"
- JOIN "MonitoringReviews" mr
- ON mrg."reviewId" = mr."reviewId"
- JOIN "MonitoringReviewStatuses" mrs
- ON mr."statusId" = mrs."statusId"
- JOIN "MonitoringFindingHistories" mfh
- ON mr."reviewId" = mfh."reviewId"
- JOIN "MonitoringFindings" mf
- ON mfh."findingId" = mf."findingId"
- JOIN "MonitoringFindingStatuses" mfs
- ON mf."statusId" = mfs."statusId"
- JOIN "MonitoringFindingGrants" mfg
- ON mf."findingId" = mfg."findingId"
- AND mrg."granteeId" = mfg."granteeId"
- LEFT JOIN "Goals" g
- ON (grta."grantId" = g."grantId"
- OR grta."activeGrantId" = g."grantId")
- AND g."goalTemplateId" = ${monitoringGoalTemplate.id}
- JOIN "Grants" gr2
- ON grta."activeGrantId" = gr2.id
- AND gr."recipientId" = gr2."recipientId"
- WHERE NOT gr2.cdi
- AND mrs."name" = 'Complete'
- AND mfs."name" = 'Active'
- AND mr."reportDeliveryDate" BETWEEN '${cutOffDate}' AND NOW()
- AND mr."reviewType" IN (
- 'AIAN-DEF',
- 'RAN',
- 'Follow-up',
- 'FA-1', 'FA1-FR',
- 'FA-2', 'FA2-CR',
- 'Special'
- )
- AND g.id IS NULL
- GROUP BY 1
- ),
- new_goals AS (
- SELECT
- gt."templateName" "name",
- 'Not started' "status",
- NULL "timeframe",
- FALSE "isFromSmartsheetTtaPlan",
- NOW() "createdAt",
- NOW() "updatedAt",
- gt.id "goalTemplateId",
- gng."grantId" "grantId",
- FALSE "onApprovedAR",
- 'monitoring'::"enum_Goals_createdVia" "createdVia",
- 'Yes'::"enum_Goals_isRttapa" "isRttapa",
- FALSE "onAR",
- 'Federal monitoring issues, including CLASS and RANs'::"enum_Goals_source" "source"
- FROM "GoalTemplates" gt
- CROSS JOIN grants_needing_goal gng
- WHERE gt.id = ${monitoringGoalTemplate.id}
- )
- INSERT INTO "Goals"
- ("name", "status", "timeframe", "isFromSmartsheetTtaPlan", "createdAt", "updatedAt", "goalTemplateId", "grantId", "onApprovedAR", "createdVia", "isRttapa", "onAR", "source")
- SELECT
- "name", "status", "timeframe", "isFromSmartsheetTtaPlan", "createdAt", "updatedAt", "goalTemplateId", "grantId", "onApprovedAR", "createdVia", "isRttapa", "onAR", "source"
- FROM new_goals;
- `);
-
- // Reopen monitoring goals for grants that need them.
- await sequelize.query(`
- WITH
- grants_needing_goal_reopend AS (
+ await sequelize.transaction(async (transaction) => {
+ await sequelize.query(`
+ WITH
+ grants_needing_goal AS (
SELECT
- g.id AS "goalId"
+ grta."activeGrantId" "grantId"
FROM "Grants" gr
JOIN "GrantRelationshipToActive" grta
- ON gr.id = grta."grantId"
- AND grta."activeGrantId" IS NOT NULL
+ ON gr.id = grta."grantId"
+ AND grta."activeGrantId" IS NOT NULL
JOIN "MonitoringReviewGrantees" mrg
- ON gr.number = mrg."grantNumber"
+ ON gr.number = mrg."grantNumber"
JOIN "MonitoringReviews" mr
- ON mrg."reviewId" = mr."reviewId"
+ ON mrg."reviewId" = mr."reviewId"
JOIN "MonitoringReviewStatuses" mrs
- ON mr."statusId" = mrs."statusId"
+ ON mr."statusId" = mrs."statusId"
JOIN "MonitoringFindingHistories" mfh
- ON mr."reviewId" = mfh."reviewId"
+ ON mr."reviewId" = mfh."reviewId"
JOIN "MonitoringFindings" mf
- ON mfh."findingId" = mf."findingId"
+ ON mfh."findingId" = mf."findingId"
JOIN "MonitoringFindingStatuses" mfs
- ON mf."statusId" = mfs."statusId"
+ ON mf."statusId" = mfs."statusId"
JOIN "MonitoringFindingGrants" mfg
- ON mf."findingId" = mfg."findingId"
- AND mrg."granteeId" = mfg."granteeId"
+ ON mf."findingId" = mfg."findingId"
+ AND mrg."granteeId" = mfg."granteeId"
LEFT JOIN "Goals" g
- ON (grta."grantId" = g."grantId"
- OR grta."activeGrantId" = g."grantId")
- AND g."goalTemplateId" = ${monitoringGoalTemplate.id}
+ ON (grta."grantId" = g."grantId"
+ OR grta."activeGrantId" = g."grantId")
+ AND g."goalTemplateId" = ${monitoringGoalTemplate.id}
JOIN "Grants" gr2
- ON grta."activeGrantId" = gr2.id
- AND gr."recipientId" = gr2."recipientId"
+ ON grta."activeGrantId" = gr2.id
+ AND gr."recipientId" = gr2."recipientId"
WHERE NOT gr2.cdi
- AND mrs."name" = 'Complete'
- AND mfs."name" = 'Active'
- AND mr."reportDeliveryDate" BETWEEN '${cutOffDate}' AND NOW()
- AND mr."reviewType" IN (
- 'AIAN-DEF',
- 'RAN',
- 'Follow-up',
- 'FA-1', 'FA1-FR',
- 'FA-2', 'FA2-CR',
- 'Special'
- )
- AND g.status = 'Closed'
+ AND mrs."name" = 'Complete'
+ AND mfs."name" = 'Active'
+ AND mr."reportDeliveryDate" BETWEEN '${cutOffDate}' AND NOW()
+ AND mr."reviewType" IN (
+ 'AIAN-DEF',
+ 'RAN',
+ 'Follow-up',
+ 'FA-1', 'FA1-FR',
+ 'FA-2', 'FA2-CR',
+ 'Special'
+ )
+ AND g.id IS NULL
GROUP BY 1
+ ),
+ new_goals AS (
+ SELECT
+ gt."templateName" "name",
+ 'Not started' "status",
+ NULL "timeframe",
+ FALSE "isFromSmartsheetTtaPlan",
+ NOW() "createdAt",
+ NOW() "updatedAt",
+ gt.id "goalTemplateId",
+ gng."grantId" "grantId",
+ FALSE "onApprovedAR",
+ 'monitoring'::"enum_Goals_createdVia" "createdVia",
+ 'Yes'::"enum_Goals_isRttapa" "isRttapa",
+ FALSE "onAR",
+ 'Federal monitoring issues, including CLASS and RANs'::"enum_Goals_source" "source"
+ FROM "GoalTemplates" gt
+ CROSS JOIN grants_needing_goal gng
+ WHERE gt.id = ${monitoringGoalTemplate.id}
)
- UPDATE "Goals"
- SET "status" = 'Not started',
- "updatedAt" = NOW()
- FROM grants_needing_goal_reopend
- WHERE "Goals".id = grants_needing_goal_reopend."goalId";
- `);
+ INSERT INTO "Goals"
+ ("name", "status", "timeframe", "isFromSmartsheetTtaPlan", "createdAt", "updatedAt", "goalTemplateId", "grantId", "onApprovedAR", "createdVia", "isRttapa", "onAR", "source")
+ SELECT
+ "name", "status", "timeframe", "isFromSmartsheetTtaPlan", "createdAt", "updatedAt", "goalTemplateId", "grantId", "onApprovedAR", "createdVia", "isRttapa", "onAR", "source"
+ FROM new_goals;
+ `, { transaction });
+
+ // Reopen monitoring goals for grants that need them.
+ await sequelize.query(`
+ WITH
+ grants_needing_goal_reopend AS (
+ SELECT
+ g.id AS "goalId"
+ FROM "Grants" gr
+ JOIN "GrantRelationshipToActive" grta
+ ON gr.id = grta."grantId"
+ AND grta."activeGrantId" IS NOT NULL
+ JOIN "MonitoringReviewGrantees" mrg
+ ON gr.number = mrg."grantNumber"
+ JOIN "MonitoringReviews" mr
+ ON mrg."reviewId" = mr."reviewId"
+ JOIN "MonitoringReviewStatuses" mrs
+ ON mr."statusId" = mrs."statusId"
+ JOIN "MonitoringFindingHistories" mfh
+ ON mr."reviewId" = mfh."reviewId"
+ JOIN "MonitoringFindings" mf
+ ON mfh."findingId" = mf."findingId"
+ JOIN "MonitoringFindingStatuses" mfs
+ ON mf."statusId" = mfs."statusId"
+ JOIN "MonitoringFindingGrants" mfg
+ ON mf."findingId" = mfg."findingId"
+ AND mrg."granteeId" = mfg."granteeId"
+ LEFT JOIN "Goals" g
+ ON (grta."grantId" = g."grantId"
+ OR grta."activeGrantId" = g."grantId")
+ AND g."goalTemplateId" = ${monitoringGoalTemplate.id}
+ JOIN "Grants" gr2
+ ON grta."activeGrantId" = gr2.id
+ AND gr."recipientId" = gr2."recipientId"
+ WHERE NOT gr2.cdi
+ AND mrs."name" = 'Complete'
+ AND mfs."name" = 'Active'
+ AND mr."reportDeliveryDate" BETWEEN '${cutOffDate}' AND NOW()
+ AND mr."reviewType" IN (
+ 'AIAN-DEF',
+ 'RAN',
+ 'Follow-up',
+ 'FA-1', 'FA1-FR',
+ 'FA-2', 'FA2-CR',
+ 'Special'
+ )
+ AND g.status = 'Closed'
+ GROUP BY 1
+ )
+ UPDATE "Goals"
+ SET "status" = 'Not started',
+ "updatedAt" = NOW()
+ FROM grants_needing_goal_reopend
+ WHERE "Goals".id = grants_needing_goal_reopend."goalId";
+ `, { transaction });
+ });
};
export default createMonitoringGoals;
diff --git a/src/tools/creatMonitoringGoalsCLI.js b/src/tools/createMonitoringGoalsCLI.js
similarity index 100%
rename from src/tools/creatMonitoringGoalsCLI.js
rename to src/tools/createMonitoringGoalsCLI.js
From 305d6f35c3243f1f53b538a3439ad10451e86131 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Fri, 15 Nov 2024 15:56:35 -0500
Subject: [PATCH 011/198] add missing model entry run migrate and rename file
---
docs/logical_data_model.encoded | 2 +-
docs/logical_data_model.puml | 2 +-
...ort.js => 20241115203616-add-post-processing-to-import.js} | 0
src/models/import.js | 4 ++++
4 files changed, 6 insertions(+), 2 deletions(-)
rename src/migrations/{20241114202733-add-post-processing-to-import.js => 20241115203616-add-post-processing-to-import.js} (100%)
diff --git a/docs/logical_data_model.encoded b/docs/logical_data_model.encoded
index 25f0486e20..9e809d4546 100644
--- a/docs/logical_data_model.encoded
+++ b/docs/logical_data_model.encoded
@@ -1 +1 @@
-xLrRSzqsadxdh-0g3vwuZcTpcMpLg3AfKMJ9SQqlAadEr2ILKa4a3WzY4ZW3W7AKRFxx5W1U010a0P97jXC-IGS4kviV6er6Dox_P1nGNXPv5CUFOd17K1hlARdDOS7YTuZlOe2p7APnGiax5cyXv54SixS8xm45HPmZ3Fdz7iQ4yXqn7YKvO2p-BOhZEFwMq1JIKv8J6d_ovPV_S_BtpxJownrAtcB8MFn55CU_Ir5EnTkGHN55voJXw0xn-qkOe3s2cBFVelXnZDBuE4QOmmpI-3Z-EeCOKi1X-aqKPnHW_joiS7bsVdPvzkhqx2e-UYQUiwE_eZIA2Tz1UeOvulS3RxnkQY5OlVWu1eraUqA7-9qO5Plt4mg5m2SyFeuvAY3ZeDWhxCNF-5U2ONmSO_BkBv-A-kK-zQ__UnJP-0dc9_iz9NUGZeCu__jA1kd0MsyOf0-3HPp278xLZc9Ck7Y7mrzu53CEUCdYunZYikX3yvJaERWb573ovqAq1uzpbCE7m66BSZZmbXGZ10VVx-_xxcS4vES8vkabP7k2sGGG10bl84JbvWCp0l9hMEnpmA4KueAIOZze_znl70Qoc5QOL5thnv-6KBo4Z9GqUL00QSLpuEr6hCD0f8ZBfgXDZe99_2T_tWn0TMEy_biKOr0et6Lv93bjgIYiIFfJ-_h_dZmPQgR-bXnlGK9HXZ38HrhEQAkvbYCBuBLi6oSvOV2gP7k8qrE2HLTe1XlzsIK4ZE1wu8aXJfy2EIu99b6CcFmocD-VfOX3yIctXEGGY5Us86UopYNdO0yQ8a8Xsf5Djbpph1dexW1DTe3-vOklllfKqvdwlPvE-_hsPoW5EYKbdMBu6WBUZTcyqRQKv3r0e1BkVOwDZTJJ1iabOFhGXir6mkbMVmbOFaBxtqMkCmZevf_duWy75vJSeHIcFUF065N7OD1zPQIfhyaU9U-ddRbWFV2-4kszT03_mT87YEwdpnqsoIIkXvtK0VfQn10CUJwVFCusNtYgXFuEKjYEhRt392lt0gXh4GcfISd-rl3Kkjt8K3hT2eOtw4Tey_fDtTdfY_yzRHw_OJ2DOf2cY2E2GVwAS15EIfGkY6a-ThtGs4ktRVGtYIEMSBnrSy3u6uNXj_T-q_sGm0256I9uiE55XGVgnOzZ4Or2iYUsudmHQHfUnVsgFcRsFwkPcru7L9nhr2a7ULAAQN1wPSHxQcrfBOS0WT1BahVzF8KvkeFq_YNaEv9sH4jB2hZwi1Gcg03vNxxSVFwfIrtRgN7Nto-4qiWgFpE2RuLwuAm4cOs5U3c76KYKofYoE7LUuuhBuXRIrziBG3bAq5x4Hzi6YU8zoA1025PYY1AZfYt9dfACWaAqcUinhOrlzy5_IiYuL0BBqefujG4DAsyBn8JkM8qd7t0EJIGve1da5sGl6bmek8EzT16S3pXmw9zuSeSeBq7-j586bvOszbwF5mfL2hVqEWLlgs-4AHipG0Od0rAym_f2kbXIKKRHAC2UNPE1MPv5i9R_3kGeFHz4xFkgobIhLqsnJPrzBFh17cI0mzVu9QGPDARhd9RmGC5i3UIYA35AfCg_89dlx3EntXkSN9jLFw3vPaSqZo-dzO0pE4JMoQX3XUP5lHHmqDhzy6_t2yYCTgfPeHCZzFGutel1dd7s6h_PvmGumrU7xH1dTn1pPHSlK4xuAw3yXrE8EQIodJoK15fK1fhUPjX0YGtE0nHC4Hya6sJAzifjt48zFI6mYrHls_WPAdr4rBmMCJXxV_ZfcylBirVNDzVFNvvTNP-yl73rF_8EcuYxtUa0Q1nikmxVdYJsXpM8UPjpJvCZJFOL-XMQN_CMtZkQlmv921bwDCJCeWQRC16E3ysn9fOJC3XmzVWB8Lc2ZotBdIFyzbkGl6tqoq0xE8iP0jNxorGIV59F22_rg8vyOfNCpcg6vZ5383natCODaZukcs25ZjXUCP-vcSsnNAOpUoaAhr21Sa0Hltz6IRdtIs66wx_5PkHLg_Q1Gd7mboGlKvW-txuY_2Gj47Oqyky3O8q3jGrDz4xpmfV5HHdtxm1xZkKC4lJaMpiHB0iiEGYUxW3EFEn1o7YoT3Px-jXZnyimV7VbZfYJFErvqWI558a-Rr4-aE69RZJ6mDqrS7ZswFfLpw4ftDJIL7G8qylQIn6roPAPIF1UQ03AAkoetjkAppSI53k4MpPaNmV3wc1WfUjLgGptEN5jSter30T6bzWCVR9Haziet5MjMOKE_bFn-2YIE4vWnWPsVJGVURaPrJlrN4qptYI7I8yqzH31JRQWdXBj0DlfdRwIz9KPED6BNv8KRT5YZIZpJLK-OAIbUsVkVkxIT6LNGQeBG27c8Mv4o5Bm3ybJhYJv7W4ztUGnlUkk5pkdfBWg0ELi1o6_vk0Y1rYVyWml5wFSdMUVkunC8lzAvfrU8FnssLNz_asPjM84ZxHXWKSftoNSvo21HPNFd1sTTH9TA5Pxo2YiBRTdyrjVEgODVS1GLPu-2r8fzHR2sRF5DN6wuqtRJzJxlGbq5ztmSUvm4p931UkUE2_M330kGNvUuXJXx2N1VDSXcQJ8PQ0hDNOzQarhlqcGMWhTuL3vx6eF4mHonFXP0L0-Lib9266P59RknAX4UcLhpLELf1yJgLFDAPIUdnzm8yGYWbEIEjzt322Q7fDo_rvYmMDX6YCmymUA1iiecDjwEAOnGIOn3DC3JTZy0bV-x2UCdiBtkYXlSvGOImrtWCBqNqWQBNFxsNEP77eAEF1D_AXqqlbm9lvvKoaOT2F34n_7Zvu_FNjr_Vpy-TdJHyV7Ht8MPqbzezuvyaT9BYJ0MzBcn8n94uK9eUafATLQfAeAItQm05t5rZxM8spflu918JsASlWg0F8EYrJv2HTpG7rTLdmSVrO5ZXB8M7y5_qQQsa1yVpxOHXRwI_Qx56uHHdguIYPh8GQtEUnPOB3b-ol8ESAPyOxLXOK6fUM449nGcvU0lMrUt0FUGF5VYB4I_kDHGlAomB5SmeQpU4j94HSaXVcZ-Ej4R9W-_YJzwczVVlRtR_Q4yGlLnWXdOcqNmUph_vQLlkdezqk0SaZDnIwt8bFBtOlBD8V0NcqsDPlU35bGE1Ho-c8lctiS_qnk7IiL7ulLUo9qDzi7f92LEMyMBB8o4G1gXbP737E80KwW3soSeYo3j5cTszJIExj5i-dqWCECq7iDyWchWMH0I8_e-WytLmg6RPaxaI0OSK8HpkGs_AkoA015qDi3C2RIBXT0c_0V_VL8U-T8pJ7k47ptF7KJQ8MxbFLgAlLRHq7ZbWnVd0RLVkERDVBS40WOeuGKhsa6CFhLNYpyrUPltcuSB8XQhtEbzYDqo1ZZ9SACqp8PVeDA2r0lQnrtx2L2JuE206s3dYCvK_DsDxUg_hYcL8HhLMdUEsfK5i9nAizKHKDjnNj7xU0wCEvtf57vhvfQ_VvHs3eTIQbAwzIZanLA_X1qd30uFcjE2ALGwkDJ2D8N2CFvSRTAWJsuDv5CVN_iJ4t-3Z7nsyl5dA_AuIgevBiVqCG7jCLRGwQ6vOUXmdxAba8EQO4mC5AlHd_4W8eD8NUjfZcom0igdzwu2xhRNLqxLC1y-Bkyd993T8o9yO2FHPCNqS2bbSusaW-cSbSSu1JGzE6S4bPnz5BQMZnDKQkNFp9d-05rymzd5Wzg3rT3EAjj7jHkhDEGTHhYcB4kQENi6VFXRPoQNbIVuns7cZsf_yYYMQhUDIhWUn_flxqFreswtgZy3s6ujTPOiEDOkl1G_UQFu-letxEqgzF1QlLqWbl6RsVrR8EuQAkAFGlLgeMwoVveCoPxEQWLaTBKbPGy8VgwJtLUCESWsHKcJ86f0TM0XeyOe77TWpySEcZCWvUrT9t9-E5jNKZGOT5m0U8YPsv-wfnUyk5D110t8xH-lIWhKIeokPFqLzyNrJJsj5C7FhGJ0y9NNLc2pLt-tFETXroorNuA_8LVSRWFsUs3gtU6cgwgyd7jykc0qTtXGxXhXy_8Yt9tJwUEtGGFSMAUjqxffyYhdzTJbceafDWBy-8S-RHJTnz-HUoqmo3_E2jZ9-nx_50sbLrREqgDylUmhQoBgTycCh_jN_gciIddfYwnttJ6_IAXlU3GkzAqtTxJpbYTlAbjYoZZfvL1zAdx75-8JGjoKUnuxLU4Nub9XfySrRTvwR-dpAAgjyYYihV4ehAlmBqx_VgszccsfEt8kYOlD1FpE6TlsWdj5sBDXZn1qYzy4MR4MK861zWhyVm3ljlbGktxkO2K4OnXFjTPj-aPXrD3yn2Uq2_ix3-friKBXfqJkzGXhlDM5yFjLjdxqV07T6wgKeR7fSooVxiUx2dSebySHr1lNSLrztHlfwhQmhKny12RfgSxQsq0ophHrerGS-hiuFuqnvD_Gq13zwGDN4dslapRqFv5EdYISWcHLIOsYZmwFXQ_GIAEvBq3z1xXJ0SWyxITUEZOlqjrwEyPfSOafc_JmhAJsy9zUZFPm-RuPlkO6YdjjoIzRvRZrwU1EGkTsnrBYWRmy8ye-EyYAW32Kr9pD6_Uajw4Eir8VLEZxGNam3mkiIxn2-0Lg81EjNHx6UBu7--R949EEluGxeZmFHdQbpjYxTPTSvITON6yNVSopvsZcUqijD-l_A83-8ufP0b5UsUlNjYpl8BIhTJEClNTi1covP1rk0ovcvtA1zRZBiDghudOnDRabVxic7YVDZpbYE4bJx-TVAHj-_6ek4e8XV9OLv3i5hLo8YpqkUN74f1_OP_MM_b0DxQtneHadRiQWjiNSNYIGxyTheT8n_WSKl4uBY1ZwWg99ZUd3lsjDmZNcS5cUt3xOJUYQfsCzpZoOUevTjRPmNU08Y3TGNDBlBSR49WKEkIWs3_slQdFDbpOhtd_cJ_1ejfp_VeEKefIk7GVALiM8w_KcxyFTMxe7SXBsCqSwAnzD5Q-r5N5W9tneDq2VTj_adTuItxVd16fvWFloTfnyXQwV3dkcahuDbzprci07kC0x4W1BmzyzTAEoPjzQ8y94zEt5oHJqy3b8pNkE5SEiZPf71_79vUN9__-yQTwN97WtSXQH-9S0F499oNV4IeZo8wJ0-gFT5rpv0mAxGY6hvpdIXafR4qIaQy6-4wUWu4OHmchrv3V29_YfX1NkToGoem9UowEcxef07qhAuWnKXwG7CL5i_ZcHZQv2Yf4yNzAa2D-VmCOAwbOMtAJ0hQx9O2cTReGv8gRze1JWB2WfPPWCUrPdL3NFqDcpsL1hQlrDJTpOxbphyj1JBchxcpK0nv4zyF9qsfMw05AEuKaj1EJ-E7a7EKSKhyiV5bWvxHOhi0Ql2JZXvCN9_RTQ1vHC-7EtsWrP5cHtm3MARgzxnIrG8uCEQBkBv9DMIhLa4Nw2eWMGyi9n_3T3INlRioP1ybJHsarpRjXA7Eqn0jp4KUL5wKhJ8CmzImcMQ7gGcL5SrQ8kX0DNIUoukN6on7vNw5oGxL2D0YHjaBGkWPkHMi0hTMk-sEr6gimlEn8vGhM05WhJeCuhNtF2wOL7WkUWHkeLu1iWAmR6bQ054235qgl8Bb0MW7g0BJEIXomF7twmWEIDE33Kkl3nBtk0ASt76ITsBEeB45iFgttczNMc1kKGhcrfRjyKZrtvk7KmUmtpcJD23v1btUlAkM2R0N80cWTm1oW3DoylG9K0xH6jySGMgUf0-0CtpYzujI3qugH2oFt7TChG3L0mvT1hpZPE0qTSh2YPQErZdCU8AtWEC1PGR51wRqNUCMdmQV_6k4L91T8Aq0kW5PKEgZqNpAx8jGWj2fFBpGhAXOglJVMhl2qyApGz4ApmcB14dAzwvyEcP1hNFzqy8pUxRhYPEBKSolf2fCJ9iULbojI0qYvxsTS8Hk7R5MnKRnMhV91oN7HmefK7rAhhGh2QZLIeq2j2FKYvBw_RqN9HAdlSi0L9nTEgp9vWBGET85GhVrk3ASLe5w0UkvjMs5omxH2P46qxly8X6Y5nuQd_nQt5I0Te1KO2vYnEzXMb0oKrbMWOmYjoDZ8kE-IMi4Hu3ZlhbgXaT3qNborugFXi-gDXSjwd6e_CjbgQ5omh9C6Dp2E-vbMP6faLb0iKEmRdrPK92fZTqYjCBbX6hbcQeND3TiAaGtHm7rLAq8AeUvrLoio0fdL-t6XuhZUQWMc1gR_TgmL8mkZ5Hi7DlrtSgqOrHYje5YWT2-4Md4HkBZlXLgnKTZODrtzgN697iKdZ944ITlBeYYIy_JuwFEZ-CcJ7p-vpK7oThFF0S8RlaF9MmmP-ySNFprv8gjeMu25NCslZRpsE5Axpto9sQUdnCTn9eeswa_x98e6xdwok8bBvc1giVijZe_mKTmvK3m-5jURcMMR6FGoYnnNkmJm7xjpA3wsU8ig6qUP-ThYKZMXOc-1cZUYlYiI9WjaNmxJaQcfiEbxKBrvGDdg7X4gcNplW7Brj-IuR0_HfchcEK4uzQ8bsJI1wZnQHmgTULrf4Ox6DNykjlMpnRYP5dFeGpkr5vF1tKSsCMsVBiPBsS2nxvIr5kEfzOinnctLnRZtztIRUL-_3k7TBBIBZsEsXma2EMAyATsqTzThPQOOKUVsYeaZkNhtkBe5rdwoaUNdb1ITTsP9q3bqMdWlLjd6y2u4XtaiEoS-IO_9AboOgNlbO7nSdPflFN0ojwwN26fcn_qTqcapopUT4cLEQfTXGTyZaV-DkXhXd5iMhS8gLhLYYrzHvuZy01OUDnA8EOYejL5_XYlF4iQHqn76H2zbbHJEEKhumBEL4nLzk9b7j-FzwGmi5o-KKv_cAnPkRG_bqPEpRLj1_gocTlE9EpETWfchl9vGEexK3ZJIhfFBArjjFTgu2bdKToBpiXE_lIHQQqyxjrk0-mW_5wTGyau95SZreqt5SXb6tQNQ7NRPOFL5fD2ThdxchSCrXsVDWvd4e_dJ_Pho9YPKnBH0tZVDbuuTO0nb6AOEGi72z7bHcuulDOrsH-qLSLFfGudQhQw4DLjvGIlbD4lJuUwOKp6eD6tUQm63sUY7_3TVmlJIINIbxG8KuYeMbUnGdav3mvYmDUvirR-2gNKNO8YnJPIynd2Z_hj-HnHrMIIxyJiITUAW_apxpaaJNcJFesvqTH7-vl461d_pKGz_-vJee7cHw_tMZxOUrGwrLRou64qyMo_RT8fQthF3LrNqt16r2gRV6A0g8AywM30L1oHK_KoVAShgPtsAoCPyR29dLjMVJP6iEHkudaIhNrK3XLEqDt8e7yiecakRjJIdordY56uHXcchdhbA1krG4wM1ppfFed3lkpTUiUc6BQRIDzjmepTbf6Uh_CbQfm1RUIvaFgruXDCtQMyq7Yoo3WSDiSVMcySnfoZB88Nfj_URzaVaSSxVf670Fb_H60WwvtDjkW-RDnz6nEQKFfUWLfNQVP63bliKkpa2DPgyRGaw0AfqUtd-WsqMtbgpvMaEakZH8VLmeyDr02cStCUuBgIYZKQmeHxJfn0B2J7KGVnwnaA2zBM4axL-9jaYJP2r17tQO3R_07-wS76Dd2TgBJsrQ4blUYSS2qeoQAdSiWmOghdIZ3QHBOlvmkAIk4HW6wTKqS1HOZCaSDeAyr5FVrrIYXuSd7oDGvsM6gq6pNLJSL4ft8DbNMh1RXspitW62HkbVgcawJBvoO0FoBFDFh-okDOX2Aj-rERFmtFkUlWHLsMRvhys3Ea8aIyOQoTsuMZ0VPOylR6wZXpHakkJ36vjV7nyVT90f5nR_359rF6C_CQTd9_BhmszQjyluw_w9-ddeCmDQ966sBqph3MLcTc-EfDMjQ9EG6TMBAYs11UTc92o77PvrCHybWX1RJgng4W3UdfQMWNqAaBTl3IRngKdJQ4nSgOffl6g7NoTklBc9z5pfm6gGdrXL5ITKp7PI2cX1GtR2h49jCtKP4jPD-gJwBRuLWHhS4CfEOUXcYhfZZVX7BHZYZJI-xPq0itQ7od5tqsy8mLKHA9s75zghJQj_XzUeJsVhtDvSHzpnthY-HBuC9x7hn_RR3QQVKi8J1xTvGDJUBcynfPFrsjK-oBcxQcWH2rIifu4qsvFPcc8qgOec5gpdxVgZTLk3q4nKeTwJ3OLJBnkMWhNyhO5bsRLItlrbLgBy-nTiUMKmLaxgjflkPq2NFmpjYyWcAkXQXKBJTVqxRo1AcVlb9RuoQADL4FxUQH6Z2Ns5GZozOgofdmcVNlIeVCm9nlp-CXrx2NVYJN5EM3ypJuLE_L45ejMJaue0vVpMhFeUgwXrFr28mekdu18A7_NKYh5hRkKQWfjUq5FIzju4axRbIMkY7UJnA7KXre1Bu1wxsHSiUYPXuToCKSRHLeVR4BfjKUXY1Nj5Agb-wmf3IPeZwsRTeITHCjXD5koioscwm22XiETzI2gTvtFt9voLv7fRf6OnA9vfGuzVj_GhqCDTSd8zbby5F-kyIISEtZMOsJjYJ7pCthseFgJTdYdp6UxbXxHykPcx-PWdKNxYx3Hr-mVY8KNRikoDBqa5hArRHvoSmXQ2wZjSCaCmaP-U57IFI7lVo6-8sMz7DiixnGkAeyHRDNZfcXPHbvAxTozDrKV4f-jWG4wKGKifV-oEFdULCYoR8p0rVMEaZBRTNpCZVcuuw-PpztEaaFm7dO3DSTtGGuAuTgbEDLQvMBsK2qwzAOnk1kEDNDisSkl-ZTCYPhEQRXx8zNK_iByzqXDPT6pMMYk4J73sGkvAPc-_M1Q1xENM4ytDbLyFRY9RN5KmtPIv5FfuVuC-5puwo2ccT-SAgYM0MoY_VxTMCKHguYWT5qobJWW-JmlY-eHrAdVfeZKmSwfLDItHT4_8KxB8e_-7m00
\ No newline at end of file
+xLrjSzmsalxENy7IVaZYccnjaijLdMhlQcLPTbndoof9pjOcbL9193I3c0Hc0L2Eaij_lmB05m04IO3ao7Q2JzA0O7VpGQFHwCRBFnW7b5TbaQJfPocu9oXFzvJS5h1awNl4Tod0smBcQKR9UvRUGSYJD6Nl4Du32igqG1ZoXprC2UKxOZpA2i1O-bkIfcdwbD0SqbDI49h-vPjl_lFal_wcbL_lKFOEGyRIhvCq_LsASYhUXYn9Qped6KVtY3zSmWpj4SIS_PQaJrAKfySfmXnk8KsFqxzl8OOKS9Z_Jv9kAS3yfLdXykpvsUNP-UdPLNhqGZndHtz5UP8IleFq37F47qRUUDFMGh1-yKaAMidsX8xnEp4gjkzd50g1JtXwd79KGiP1kLVOYv_fhmJ3z3Z5vFtdNuZwrJxth_zxajZu2UOd-pqbzv1EWxZy-oe6wSXRRncaZuD5ciC2JjLEKaouU8V3NtYICWvunTBZ5EAg74Bpb1G5k2KKSFBdGdG0Z-UeW8-1mnqguCXRKemG77o_V-YxNn1GF4Umo2yXsnFCPe0WI7W38Igt7xWbeBY6vRu074eXBYWfzwFsp_t6OI1BQOr9tRL-_6aCnKl8I4MpHmMGrZn7tcx5EWn4YhXiYiRaA9Z4V_BdpGXKDyRhloKf1gN2NPOdajCT5OKrINzgV_-_BxySQwRXbfnkGKBH5c5GJBISqLPBBSSsWDUwRPmLXC6hYkqXpay95bsX7MpqPvSGC8ARWIU5E7y6vBWac2GfOlBBOF5mbYCEnQVS4b9085wv0mMJTIuv1tlG4X4AqOriikNQP-r0TmzejWFqyw-__VOBJMUQzmOwxPlRdw4Kw9HKT8xXQmbuDsRxHjTIalS8WLgu3pasjbBF6oJNWEbZ6pPR2UTR_JLW-GJjVn-vtY2WctzUY3ySN51oZtAO3um3KrGTWq7_bOcclYPxb7sOTkQ2zSZxIhGFqW7y1qiV8FgVldJO9BEu7dPI1-WR4KCmvFbgzZZRVk6f4lexI64tjWuEawpQ2w2kHIIa8INxMyjJwtOJG-byAnZUe9-WpHisTMUdh_xtj7tybi4yIK6U8uu81iKvuIASb2bT4DDyx7gXjPTks-Xl4YSiuNJh5e3njmh3fmz-q_sGm025MqJmQCEB2e_KYv-a8Xg5P4_inFawqJIyZllLVSpiVrOpjxqEg3ZNf5CEYgmIq-3yoeZtDDhIMGu10g6NfEZwUGuBT0_fmsl8ToJlYPPN573rOJdCK0cApx_PVFM5IrsxgN7Vto-5qiWgFpE2RuLwu6q5jZOMukK2Ra6cLCQKnQdp6bTSbRUGlhcx09IZ3Er5VBHha2JTWoqK2C5Q355YrBGbsLEofQ0KRUPwJ1lZst_mxmeoBfN0ihGYNcr0mwhR4Z6XErRZoGUSGnD93kWMyWko5mqk5Dp1Nhe8pWSSEFHFFBb7b5UWU5OhWqjBMtklnmk5AeLRUfs2jzKtmfIj6I2Zam4ftc5z8LqigIYJQ9HWpwx9o2pF8jZ8VmyAb3yVHUpxkifSgrTFiKsTVIBwmHvi0OTly6j8jz2OhdDQmGC5ipMGoxB2A9Cg_ufiVMUVcVNUwEJQhFeHpJTxedbyEMq6dD4Xiab67IeqB-hbW8FExeT_krz0Ph9JBGgT6A6d9-bU3FQCipVuxRuWm1c-Eco7EBk7c2kxU84Ami-1yXzE8UQGodRoK19eKHbfUfjX2oKsE8zGC4Lyb6oGATkvsxY5Udf2O1UfmRRnCrJwYAXvBM9mzlxmqxUNbsVdrpVNBr-VNLsVlBvmzJ_o7fk8kztf06WSRFiEtvvazeUrY7cRSq-J8qps5VeLcb_p3Zuuchy9IGWP1ZJ4pA86cp0HZW_DiMQM4p0wS0tu2o5PWayjorqW_EufoDwr-cMW7Pp5387gmsKg2Jvf9eINUbH7Fh5A5cSrG_EO8H0UCcxZ1aaVbqsmGaTihvZFqipcL2xJcRqKXLUeH0MWY3-yeAJQ-wKmm_NVOZFngbNxGA4u-4jML3dCNzwT4twIbmWx6ldt4R2cWReBJVJEyyBNnKKPz-y0Uudb318Cv5jx4ImBB3a8dku0tdhOWv3nPErizlonnuwNOVZ-oXqn9qK-yAGD2YaIGzwYV2334_reZ86xQ-3mxD7Dgvv3KxYffRde4AQNjPSYQfCbCv7WlT00L7NOKRrt9pxVI53i4ExOa7qJ36k2WRMkrwKotEV4jSlfDJ0S65rYC_J9Hirket1NjM4LElZFnUEJIE8uWPaQsFNHVEBbTbJlrNCrpNIJ728zqjP31BVPWcPAj0DiftVwMj9pCt2Y5xycERf3gpMYp3VL-O29bXwSkVkvI_ELNGMfBW25c8Uu4I5BmJycJxcMv7i4z7IJn_IklrxiEINbLGaePpi8-JK75mN0-fHdVBaOvUuy-z9bP1BvSvbtUeNmktLNzVitvROJ8tYa3GiyIlakuRu06IphVERYwAoJw4gnsaD6OMsvFPlV-j8fRHW2Xgho_4MGIgct4Dw-M5qJvplUjFj4lT-RGFVH3H_d3ZSXCrImwOdBOiq0umeKpokS8xnzJJ3VzKXcEN8Pw5gDNOywqpfi4gIMGdSuL3wxsiCCWG8nVbO2L4nLSX92M2Q5fJinAP5UsLhpL2VffqJgLNCAfUVdHzm8iOZWbEJEznq3YAR7fDf_a5ZmL9Z62CpyWQB1CWhcjXwEQKoGIGp3z40JDh_0bJ_x6QEdyBqkgji2PSPImnsWCFydaaRBt7xsKYQ7dW4EV1C_AftqV3mJ_xAj50owaU59ZzFFNZuxExz--VpbsRFFZe-Fv2pEalf6lNFaZv9SI83lI9SJCoPD56Q4vgUaKckHwYeisy4Iz1LR-rYFiAN_2WQ5z2JAuQi0o7iiKkKdNCm1zNLPwN7wjpfmb476XYluDzBS1kBxvwDjPA6_Ph_BuHt2I0zNoMGjnE2sW0CB3Exu_WfoZl3MUCVgmaA3ycf624wepMl0thKlxW7lr7LF6BzGJORmLYpLqaLlaNeDK06fkMZbLcHSkNkHc3Qmswl6vcR5sw472iT0JaFivMzFqf_9hMF5uHEn2a-4u1RxSXIogk3jmaDMPG8vpM3gY64ECS0Pz86DKxJTGjgkicrhwPtMejbsNi3Xbj073V8PgtfaO4UFpFeF6rSAXhkgS291CEA48hoLs-clgh84547laToO4Bk6SctYVlJN8-sT8pLNk4Vm_SlSJQ0JxbBMQwWqRnq5ZJyo374QLFkjR_R7SsCVO8mIKhwXMu3Hh_QYtsyt_-7jZi6Ig7KtLsfVGO-CCUuXp3Yjil4tgBG0pVBKQGTT8lGiQ3BVDUXp96TStcxZdQe_hIaruKPLslUEMfM5S9hA2rNHK5lntb4tI8wCtprfL1wheTQyVnIstaPIgjCppIXZXOG_1Xqd34xFVZD2APJw-0o2_1N2S7dyh5PW3-vjH5BVvxfGaxz3JFpsSXMNgtAu2Yhv7aOqyH5jyPqXKylour1XFsNB8OUqH1WOALIZFs90nSQE-qxJ7DdS5PLFRzmLtUQ-hXqgy3t-NLvEoI4wninv_QUYoQjeuDfAPnl9HrEvAmvm3cZ-U4u9gpbwAMqZdgQezSkVc3FyYBhvXpEB1xM7qH0kQfj7jHjn6XAd8OuPwufMpfun7zx6gUb5_JdUSQZ5a_fFB9gbwbsZ4dxmaVxfGx1kDFKcvNyCmwsrneeTnzA5n-etVnnVH_-UjLsT7bRHqmbk6RyTqeiDuggjAlOUrAiFcYRxeysOx6MTLaHAKrTIyOpewpVLkR-SWsnNcZ05fWPK0ne-Oe36zG_mSEYWCGzUrzBr9kE7jtKXGOTLmmM8Yzot-QbBUid7Dn50t8pG-lMYROLoaScTfR_w_gQsiUUT7l2nxVmFNprb2RPq6VF0TnvowLNtAV0NViNXFcJN3gxV6MkwkigdjLkd1qP_fmxXhfqw4o_ANJgzEdOJFCR9TzuwfPy6htvUprcgaP5oByoBS-JJJizz_9Mjymvr_EEiS9snV_5ZsHnsRUqCDSe_mFQmFgP-cy7yjd7fcx_cd7-wnhxH1VU4XFQ2G-ykqtPjppihTVRcj3j46J-j3Q9FtPVuGcm-a8jRfskV4tub9Xb-SjJVvgN_dZABgjuYYyhU4elAlW8FxjWRsgAdsPAk8-kQlD9CpEEyl7adhL-8DHlo94Y_y4MO4MSP6HrWhyJpZ_YEbWkrV-S2KaOmXiLIPzscPnmo3St3T4A_iBUFf5xVBXZsJjvIXx4kMrqCx-wKlPyCVq3dvYXbU5pABFktwy6UmZlqY-CewJjNSTCztTkYwhPmRV0xnERd-SvQ5y2oJgPr8zIU7Zkwpypnv5yGqD2zPIDNYtqeqpQs3v6EdeXSGMILYGqVdnqVYzyW4qVhtWFwWF1M1s3hQJlnsF5zb-hmtZDAZKdCqAQ5TIUtfViKPhA7xJ7Czd4pKjgd97zeb-Ctftev2vtR5YkAHl3mZoZuxmOg0S9pKdEqQD-IteGwbKXzKwlj9UJ0FFQmAV4Bu9MeX4wrT7iPuXWVxniXGaui_XZkUl0zuTcNEsBT5bnpb5rWSRnTzweedAEPxInqtw_yeWFuZZba6KLxPwzUsREyWjAjrCuazTru6R9raFMuZBcRlSe7rkC-mtglYTYzrkQL_koOU9yQFEUeu3LFiPrzD6ttyQYyoWY5ydXMaErMjN8YBFILvSSIK7njdxfR-q0tjhV6X6IzEnU2snTnU590lvsgXqZ7-1msyIGj8MFgAeWsDwSExsqs-ZQPmNQRR_TXDw9gauptEBfXxdbqrXd0zu0Y8Dr1SskxjniGc1GwvA0uFFQzRSueNDcl-V2PFy6YtZlzvXvIYdAuTX-fEXOJhpIRk0zr_kSzA2hOpnpeh7qqLhxKLyM07UwWsm9zEtmYTtXBVj-UzQZc0n_8sd7o3hfyEUwQIlWsNtjcQm0Uqm3iI0al3to5qex96theW0aJqxTNPAkcWUj7QjnnhW9ikwHnUfoUN5wU_El7dvfnHO9t8siTYNC1n2USL7n7g8mWEquEQ3xITLsHCoYq8nYUEy-7CbBOcYGYNmtmlJpF0Z6ECjQi8RyHFiLDAQvakI6r6HFsN1mRTLC2-avM46EaFEWuISjsySsDR78LL4ZIVrUWGFpX1Z1MKR6sv2O7R7TB0CsRT24e53Vj0QS0OK5BBS5YshCceQv_XioUou96LvlhQfR6SfTUUuEOSbVTsQX7F53kXvDdzQpG0vHk2abeBoRnmybpL718_RBmLOcTqsAv0clqieiVJbwTs7USUKJDX3j_ezMGPMLn0rYlwFQXLzG1EJ7aYBg_I3PLfLH35kah83eCBISSm_StbBo_CMSUfCWTfVKqxuQXhQCbNfcBEEgyA8qmZS8qi9WaGjM7oehcH8Zw48rT2nBnSkFbYFn7gDnGHOX68T9M25hNkKMqHj2ATjqVgsYiml2o8vKHh16m8awZECM-vmLJXCU2P-GcwGYG3P1rqq84I04qUZ5q1Ca5KWIe1r2SbJXWUVRibGSaUS27vTQ7YVlP0SvlEDZsOi-YiWIn_RJ1RrPROsvG3kNMblxoIVNScOTJ1wpVE9Cr4_W4NS8TLSe5MGJ80YWEO0vGHcxGZW0r0CNezXH2SPew0ixupT4Hfm6S8aOkZDntHKU06e36BtMEEDauZHro4D6oqOhZd0T84Hm7E14K6vJ1-tqZFYq-mzS1Hv1S84M0N015L7ge35yM6n5g4OhACmXDHCL2PV3csGXF1q-4PX2Q28i5IyZrhdywP4AkSVtJmJDvjsl4oSHfu5RI29CJ9iULbn6f4QJSzpD6oAOXYoesYLVKAX-K71SjHAI6a8gsAWYhQAGcWOe83IBalhylecIYD7GvOCJ9XLDK9W-Gua0N25NiTrCuHO060Hgujot2v8OfGcH15FT_148KuR7X6Rx5BGAa0nI1iO2PkeCjecIW55U1JY4KaR6HSTyb5S4Hu3ZlhehGI6W6BwwA-6ZuHBsnS8d7gGuZius6nWLBd3IuWN7SpnIHQP29e5YWs3S-HLGaAbDtI4MOt31DN3CLONF35Y6o8HgvhoeYf13gNtSLaH78HE_7XOhZUgi8J0FCyErO4ICBeuYD0vlXExaYHWr64HGBbFw5K14NWOlxNgY8ZSB6lkdgJurBybbwmXZ9aBIzB4aIEKzFZxuwIh_yyiU_t1OWUxVP5m3X3T-Xx1s6ZFttrpzz-QMiecq15dArlpRoskD4xJxp9MQVdf8UfrjHj55_kYT9CdBtayLDN387SulTRsbwX8_Ite7YwR6utimiswHWa5dYkDebWF_KdKFfiSLRLDauoipNLfUg3HLx2zI-aGnTaJ1R8Fjoc9vCRONDq8FwpGFArlI8KCdaUmUKQxWbfsLxZ3PLCyyPmgaNBSdc2TJaqNfIwCoRIgjmDAxvSx5jdgt6pRIOOn_QgxsQ3Eu-iabfztGrNie6JdsdhBKQJwrVJ37kggx6VRYdsyo7-dK8xtQXNNgSiZtC42WGustffhkxNYipnKXwjrVC7CdLkyVrBR3sauqiVqv9sNjdaGIzGwU5zsALRGplGd1Onh9tufFqcAp2bPcwLmwcn_Mf-TO39tFhUecWPdRSto6TDhDyqojHvgHn6LRqFJBvtwIh4SwvOz4fh6BLABRubVIEo0zWuN4ZWPY3Y5eRzMUyyY9X73aTOKpqKbPBuPoZX0yyTtAYeWyto-Zsl3-TWUNYXN8Qqdp5mgtjeJm6ShPTAyYVzTHkEN5dfbCmSvKt4qh7aTg1HjfrSdbjgwr76rSXIuPEP5vtuhUNH1ljwUPsAt2VuEVY54hUAQ42kNcqgLckmwY_LBl7RZjQVHKfTAThc7ddS2tXsV1W9l5a-NI_eRn92HKnBL0CJVEbOmSOGriCSmUXO666lAXDpvUQHxkdzegugVIXHErMLyAQhRoYbNAQfVbmzyof65JQDX-qWC5iz4F-sozXUkaikj9s0Gfn7OlATgZ8vw5XJDXQTxRkNy7K-WimnDWcIjvWk57_NRydYheiajtuFKbwSL1_faFdf0alSk1HFxgw2d_pU0E3l_beWBzzIdJGlCZrVkT7MuyQXzegNbsCDfwjbsrwHHtlnU6hEdhkY5e5qsyDK1sGLnqic8e34gf-vYyKPVMpEKLaOpusaJEhwi_cIDQU3TnCejMlke72ATeREPIFPHGjfSqwclFbh70Exn26UQUUXKe6xL0RfO4lUa-ISEzxC5wnwOSjvj8tst2ZDsMYPwlyoLgd05jvBcG-hNc4qpTfRpGUBB4E1msnnzQRnocdQBk55EPF7pRTZyJZdBzPmu1zlg8n4FJEvzhq7xPlFew8xIbzBa6jAetx8WSjzobsImHgDVck8-W0gDRjzVcFjbbuQosMfpr8Oa27rCED3jS1f71od-Awaegs6i76UioGGgman5W7yUiPAmaor19ErlgPPAiqmROZwDC6Y_a7-DEbYslYF55hxQb5HtepEE5OKP9HI-MMPSHGBPLciebiMSmNLfV49GBREALAHeuIMo21qrQOZmRwwvfI3E3Wv6iSwhJMQ3LghjkAgqhX7YoRKWjswvYTrJD8s2ZrIoNDbybF1dn0dbts-TpYseGWhFf9bpyFpxdhuKTSbstQ_xemf2D4lcAidTY5am7tHV8CnkfwSYHhhayok7NnyUdrIIEHSctnnonHfpFo6tToVYc_BlIgVR-Cl-gVf9-3iZIWHLfY3ywmrbncPllgJ3hLIde1dKMneCOINFHYGifosEL96_DP8mIrwSMY4Wte-Mbf5T2h2VNpqcmUbu4qXSR8cQQOnwjny7NgofkVHS-T1gWBzOLHINrEnMmZfOGMD6mhn6RGDbEJ7MNTgC-Ys-5R4sp03QNa78OQggG_teHpq8ufqaZlkz8PD1j_f19_D-cEb50HIRfnVAsrshJ-Vto5zdA-pkN5GymTwucNI-33U1c_OMotYz4SMa9WykWl7vZ3o-KrTdpwNgFQ5x5iJmKbQf6IzIIOTdSoIqkKjaN1rDhzl7Llh7Tx28g9EjHZjgbWudNJKhYMjwsuDAjPsgklr5gTv-2ABQSCpjf9tNxhxHJWuf-fVG71N0rLgrbekgPlvmrKENkdjCHF5csa6jhF4pLYhB6lG91lLfIrvGlfsvCEdOSvsLZ6HwvZh_XEh2ak0EDlzxdOgIUoMBHsSa8Pk9pNXaNVSmsbxXSQKN3v14H2-RkMKYbk_QLKSsZR2tewsiQJSDgkB75Dl9ya3gK-rWfu1TG79-EAnS0yF9IBETeeqlfW4qglEmf5h6YdK2tTPsrfC45xRDrq8-miMWwZsP5TR35T1n2q7Ewj1qcpwtpXzfJxcan_YyGi5kqhJUZn-vP-4sfeIYQsp-AZz7UD9-7SmRCP8njFZ9cVCR07rP-qmpjbFjkrz8YMDpTxDmVhBTfVXOs-Plz0BBnmMvUbwIMnaArjov1RGj1QG6s7InOGDlB3gv8U27d_4_4TgPhbs6Pvht1LUOnWgnutHUioyb9gv-wzkVgG-MmD3D29Ec0f_PVLoG-cGjPbOmIkhdUKLDjTpyVSc8zx_fhvtEqiEGGVO3TISteNvAWGhbsASwrL8MOFzQ93RngZk-5OEyUMlVngUCEKgEcSXhitKitjByn_ZzHS5WERXPOQ4JESlPARijMt7yphOFOIw_d6fewl1pUnBOvgs5wAV2uzF9m1Nmf_4yGqyvjp1TKoW6sqtxyRgpXY7H6KhijM9Gv8iSpBede4TUwtAI9Di7Ck5VLjIV8FYFEgB9B_1m00
\ No newline at end of file
diff --git a/docs/logical_data_model.puml b/docs/logical_data_model.puml
index 849cc77b94..7a55b54e44 100644
--- a/docs/logical_data_model.puml
+++ b/docs/logical_data_model.puml
@@ -541,7 +541,7 @@ class Imports{
* updatedAt : timestamp with time zone
fileMask : text
path : text
-!issue='column missing from model' postProcessingActions: jsonb
+ postProcessingActions : jsonb
}
class MailerLogs{
diff --git a/src/migrations/20241114202733-add-post-processing-to-import.js b/src/migrations/20241115203616-add-post-processing-to-import.js
similarity index 100%
rename from src/migrations/20241114202733-add-post-processing-to-import.js
rename to src/migrations/20241115203616-add-post-processing-to-import.js
diff --git a/src/models/import.js b/src/models/import.js
index 54d5b19fcb..3474d0abff 100644
--- a/src/models/import.js
+++ b/src/models/import.js
@@ -43,6 +43,10 @@ export default (sequelize, DataTypes) => {
type: DataTypes.JSONB,
allowNull: false,
},
+ postProcessingActions: {
+ type: DataTypes.JSONB,
+ allowNull: true,
+ },
}, {
sequelize,
modelName: 'Import',
From 0d408c2d1c46bb5f4833d91754c27f74d90524ab Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Mon, 18 Nov 2024 11:04:25 -0500
Subject: [PATCH 012/198] WIP adding ARO citations
---
docker-compose.yml | 20 -------
docs/logical_data_model.encoded | 2 +-
docs/logical_data_model.puml | 25 +++++++++
...add-activity-report-objective-citations.js | 53 +++++++++++++++++++
src/models/activityReportObjective.js | 9 ++++
src/models/activityReportObjectiveCitation.js | 39 ++++++++++++++
src/models/activityReportObjectiveTopic.js | 4 +-
src/models/monitoringStandard.js | 8 +++
8 files changed, 137 insertions(+), 23 deletions(-)
create mode 100644 src/migrations/20241118093025-add-activity-report-objective-citations.js
create mode 100644 src/models/activityReportObjectiveCitation.js
diff --git a/docker-compose.yml b/docker-compose.yml
index 1931e66c4e..0f9ca32645 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,8 +1,6 @@
services:
api-docs:
image: redocly/redoc
- profiles:
- - full_stack
ports:
- "5003:80"
volumes:
@@ -13,8 +11,6 @@ services:
image: postgres:15.6
container_name: postgres_docker
env_file: .env
- profiles:
- - minimal_required
ports:
- "5432:5432"
volumes:
@@ -22,8 +18,6 @@ services:
shm_size: 1g
minio:
image: minio/minio:RELEASE.2024-01-01T16-36-33Z
- profiles:
- - full_stack
env_file: .env
ports:
- "9000:9000"
@@ -33,16 +27,12 @@ services:
command: server /data --console-address ":9001"
aws-cli:
image: amazon/aws-cli
- profiles:
- - full_stack
env_file: .env
command: ["--endpoint-url", "http://minio:9000", "s3api", "create-bucket", "--bucket", "$S3_BUCKET"]
depends_on:
- minio
clamav-rest:
image: ajilaag/clamav-rest
- profiles:
- - full_stack
ports:
- "9443:9443"
environment:
@@ -50,8 +40,6 @@ services:
similarity_api:
build:
context: ./similarity_api
- profiles:
- - minimal_required
ports:
- "9100:8080"
env_file: .env
@@ -61,24 +49,18 @@ services:
- "./similarity_api/src:/app:rw"
redis:
image: redis:5.0.6-alpine
- profiles:
- - minimal_required
command: ['redis-server', '--requirepass', '$REDIS_PASS']
env_file: .env
ports:
- "6379:6379"
mailcatcher:
image: schickling/mailcatcher
- profiles:
- - full_stack
ports:
- "1025:1025"
- "1080:1080"
testingonly:
build:
context: .
- profiles:
- - full_stack
ports:
- "9999:9999"
depends_on:
@@ -91,8 +73,6 @@ services:
- NODE_ENV=development
sftp:
image: jmcombs/sftp:alpine
- profiles:
- - full_stack
volumes:
- ./test-sftp:/home/tta_ro/ProdTTAHome
- ./test-sftp/sshd_config:/etc/ssh/sshd_config
diff --git a/docs/logical_data_model.encoded b/docs/logical_data_model.encoded
index 93aff94573..09c2268096 100644
--- a/docs/logical_data_model.encoded
+++ b/docs/logical_data_model.encoded
@@ -1 +1 @@
-xLrjSzmsalxENy7IVaZYccnjaijLdMhlQcLPTbndoof9pjOcbL9193I3c0Hc0L2Eaij_lmB05m04IO3ao7Q2JzA0O7VpGQFHwCRBFnW7b5TbaQJfPocu9oXFzvJS5h1awNl4Tod0smBcQKR9UvRUGSYJD6Nl4Du32igqG1ZoXprC2UKxOZpA2i1O-bkIfcdwbD0SqbDI49h-vPjl_lFal_wcbL_lKFOEGyRIhvCq_LsASYhUXYn9Qped6KVtY3zSmWpj4SIS_PQaJrAKfySfmXnk8KsFqxzl8OOKS9Z_Jv9kAS3yfLdXykpvsUNP-UdPLNhqGZndHtz5UP8IleFq37F47qRUUDFMGh1-yKaAMidsX8xnEp4gjkzd50g1JtXwd79KGiP1kLVOYv_fhmJ3z3Z5vFtdNuZwrJxth_zxajZu2UOd-pqbzv1EWxZy-oe6wSXRRncaZuD5ciC2JjLEKaouU8V3NtYICWvunTBZ5EAg74Bpb1G5k2KKSFBdGdG0Z-UeW8-1mnqguCXRKemG77o_V-YxNn1GF4Umo2yXsnFCPe0WI7W38Igt7xWbeBY6vRu074eXBYWfzwFsp_t6OI1BQOr9tRL-_6aCnKl8I4MpHmMGrZn7tcx5EWn4YhXiYiRaA9Z4V_BdpGXKDyRhloKf1gN2NPOdajCT5OKrINzgV_-_BxySQwRXbfnkGKBH5c5GJBISqLPBBSSsWDUwRPmLXC6hYkqXpay95bsX7MpqPvSGC8ARWIU5E7y6vBWac2GfOlBBOF5mbYCEnQVS4b9085wv0mMJTIuv1tlG4X4AqOriikNQP-r0TmzejWFqyw-__VOBJMUQzmOwxPlRdw4Kw9HKT8xXQmbuDsRxHjTIalS8WLgu3pasjbBF6oJNWEbZ6pPR2UTR_JLW-GJjVn-vtY2WctzUY3ySN51oZtAO3um3KrGTWq7_bOcclYPxb7sOTkQ2zSZxIhGFqW7y1qiV8FgVldJO9BEu7dPI1-WR4KCmvFbgzZZRVk6f4lexI64tjWuEawpQ2w2kHIIa8INxMyjJwtOJG-byAnZUe9-WpHisTMUdh_xtj7tybi4yIK6U8uu81iKvuIASb2bT4DDyx7gXjPTks-Xl4YSiuNJh5e3njmh3fmz-q_sGm025MqJmQCEB2e_KYv-a8Xg5P4_inFawqJIyZllLVSpiVrOpjxqEg3ZNf5CEYgmIq-3yoeZtDDhIMGu10g6NfEZwUGuBT0_fmsl8ToJlYPPN573rOJdCK0cApx_PVFM5IrsxgN7Vto-5qiWgFpE2RuLwu6q5jZOMukK2Ra6cLCQKnQdp6bTSbRUGlhcx09IZ3Er5VBHha2JTWoqK2C5Q355YrBGbsLEofQ0KRUPwJ1lZst_mxmeoBfN0ihGYNcr0mwhR4Z6XErRZoGUSGnD93kWMyWko5mqk5Dp1Nhe8pWSSEFHFFBb7b5UWU5OhWqjBMtklnmk5AeLRUfs2jzKtmfIj6I2Zam4ftc5z8LqigIYJQ9HWpwx9o2pF8jZ8VmyAb3yVHUpxkifSgrTFiKsTVIBwmHvi0OTly6j8jz2OhdDQmGC5ipMGoxB2A9Cg_ufiVMUVcVNUwEJQhFeHpJTxedbyEMq6dD4Xiab67IeqB-hbW8FExeT_krz0Ph9JBGgT6A6d9-bU3FQCipVuxRuWm1c-Eco7EBk7c2kxU84Ami-1yXzE8UQGodRoK19eKHbfUfjX2oKsE8zGC4Lyb6oGATkvsxY5Udf2O1UfmRRnCrJwYAXvBM9mzlxmqxUNbsVdrpVNBr-VNLsVlBvmzJ_o7fk8kztf06WSRFiEtvvazeUrY7cRSq-J8qps5VeLcb_p3Zuuchy9IGWP1ZJ4pA86cp0HZW_DiMQM4p0wS0tu2o5PWayjorqW_EufoDwr-cMW7Pp5387gmsKg2Jvf9eINUbH7Fh5A5cSrG_EO8H0UCcxZ1aaVbqsmGaTihvZFqipcL2xJcRqKXLUeH0MWY3-yeAJQ-wKmm_NVOZFngbNxGA4u-4jML3dCNzwT4twIbmWx6ldt4R2cWReBJVJEyyBNnKKPz-y0Uudb318Cv5jx4ImBB3a8dku0tdhOWv3nPErizlonnuwNOVZ-oXqn9qK-yAGD2YaIGzwYV2334_reZ86xQ-3mxD7Dgvv3KxYffRde4AQNjPSYQfCbCv7WlT00L7NOKRrt9pxVI53i4ExOa7qJ36k2WRMkrwKotEV4jSlfDJ0S65rYC_J9Hirket1NjM4LElZFnUEJIE8uWPaQsFNHVEBbTbJlrNCrpNIJ728zqjP31BVPWcPAj0DiftVwMj9pCt2Y5xycERf3gpMYp3VL-O29bXwSkVkvI_ELNGMfBW25c8Uu4I5BmJycJxcMv7i4z7IJn_IklrxiEINbLGaePpi8-JK75mN0-fHdVBaOvUuy-z9bP1BvSvbtUeNmktLNzVitvROJ8tYa3GiyIlakuRu06IphVERYwAoJw4gnsaD6OMsvFPlV-j8fRHW2Xgho_4MGIgct4Dw-M5qJvplUjFj4lT-RGFVH3H_d3ZSXCrImwOdBOiq0umeKpokS8xnzJJ3VzKXcEN8Pw5gDNOywqpfi4gIMGdSuL3wxsiCCWG8nVbO2L4nLSX92M2Q5fJinAP5UsLhpL2VffqJgLNCAfUVdHzm8iOZWbEJEznq3YAR7fDf_a5ZmL9Z62CpyWQB1CWhcjXwEQKoGIGp3z40JDh_0bJ_x6QEdyBqkgji2PSPImnsWCFydaaRBt7xsKYQ7dW4EV1C_AftqV3mJ_xAj50owaU59ZzFFNZuxExz--VpbsRFFZe-Fv2pEalf6lNFaZv9SI83lI9SJCoPD56Q4vgUaKckHwYeisy4Iz1LR-rYFiAN_2WQ5z2JAuQi0o7iiKkKdNCm1zNLPwN7wjpfmb476XYluDzBS1kBxvwDjPA6_Ph_BuHt2I0zNoMGjnE2sW0CB3Exu_WfoZl3MUCVgmaA3ycf624wepMl0thKlxW7lG_4Qe09ISrNAhKYfSlSgC6tajfU5pKrxjsCB52E_d6VOwzkUf3-JMyUImYTYcPs8iIrsrYXaLS7RbOMioeGsQnPHHmnpY07Ee0zidABj5j9s6MrRI--L5issTm8EjuG-Q93FMBOZ8pTwwDL_NBXICDnLXHC9WX4d5E6jt2z_LPKbe0Xw3zN6ZDSrLcipywE_7cdl7Ad9m3s2xr_cRWATS9UoNaEbUUieQ82HSORJeDflVTCxdZlt06EKaF8DtGACVhLNzNwx-GzlVmkMH2sikL8R1tfaZ84DOSPfbOs_GQK5QDQVJdlW4g5dGRFrha6V8pcbylOBxrJzQKsf2JUgqhvtrAeiXDDKNgYAXjgAzuwwKN1aWEv9en9L3RNc_QEm1JMIKfr_QOOHA-RxCUWuON1wUfiHIg7KnsSGvAyGXii3PBK2Ut1lGuxwHjQ5clmTOkAtbo-uN9N3LL39zp2YYOzeY-yCcLgM7uSA-obR3Zca9SB0o94Q_H43AdPkt7UQvaZNhgXyUkEkw1VtTUrGaUtnx_9oIWxHCMPEt3yLJLv50zTMEjj8FfhANN60Sq3pntDAMCtHIsaRyJH5hL_-n9pXHzJDFvnPFAWzZ8PmKTiygjs8qv0u3N7CM5UqSlQC-V0sJaqlwi_nZaCjdjH_Oj4iLUyQbV23Z_HF7u9rewatAVzZ66wjDLRiE9OkF5Q_-UFuElhthEtgzB2Adavmo_XjWLvi45TjLR6ve5vUq3JP7st6PA_Rj29IchgIYcT4Nw2f3lRb76IxqeGfC3MW6j3m4GCqhdz2XqC7ZNdmkfQkDna_lAqB2Jgi6Yv0N2ItpKzTqai-lea0ucQ4rgyNRIkKYqpkBFtLDp2rZJtlU82FRMj_-F8eIx2bwvG7llEGQwkwJu6_y2iEzo7RSd3zpbZJrLK-hUquFJ3wMt48Tst5cNXHRiFfrR6Ru36EjlFMAVsuT_NpUivIZOoKUs5Ud2EVTZpkuozTdNTgunzd2UkC3OeVo-wlQslhfr7-0VP_zJ7rs-tbjqzBtzqrvu3JDJQ6BhW89BGN77hxcBOjUzvXgBDtdzqXolXfRH1zwhx45s9tWLozEbtxdF0hCSdmawF-DY_zzvHPLFKMMLJsbbXKzXLuS4lUq1uypPPq7LdNv9LcOXu7uiqxIlr2hDcGHptwXI_2Z3ZBoEW0UoMUVyINi5oe3JeNa3A2CIoMEkisFUCKQMYUbXFwXRsD8lK2Ti6mTxIMEcrrsUfYU7UdxFra-0CwTpyfm-DIPjc_NG_s5DwXNnndHT-vYflkwTxtLBU5QmVV8NOtptVMkW2MTVIi6w7sUDZHZMIE9_-2W8Pk7XkvE-n3cRQnZOPqzCZZ2YAhJEpe-EpuMFm6cJWxznxG1uItEW1RJzk9nyxdkrIdyvvHQanYXZOjh3kvBTt7B9CzRCzPji-JaT8-9Vb3kno_ERRBMUZSbrXHD8A7VqJ1VxjF2H2UavgZHVkMz2RKiKFgcrfjBo47vhc6JObV0Az49NIgfTlB4EVqU5_v478aySTmcOFl4Sw-t17hzjYQe-m2Y-FjkbT5uHpDR6UX-txb5nt0TymXooZQEtlrofxb5PHkfNRegE_Eof2zWQp7PSpTx5K-i1xt6TPxJH3UTdcbFpictiS2ppaBUCrZQcUVFTjzF2glCaAXF7yLPBjLhPm82trb-R4455_Rn-hMFj2DxQqn8LdlKeNWzaKSdXJGRolg8T8nFgT9F4bBo9Ywww7DpQdJUPkTl4rciDt7sttOZQZQ9ECzpcQOU-2STSZlFQ08I7VGtBlkxGO49eKEEIWEZtrlcqC8L_PhFdpcJx2eDqU_kOUKeXoktKUgZiN4AyqkxKCTy7aFYWhsCmSwQn-DbI_rLJ5W2pleja3V3X-v7TwItxU7EQhvm0UoTfoyWovVZhkc4hxDrnxA6e37D00xKiBB0z-Hj6Fo2XuwOCB4zEqLsQffuFgH6dUSgm3RBccS7gSdbvSd__hnfsPSaU3To5f7ubm0yGad5TyHAYF8ZfC3cezqtKVa30hj28RdpWEWZ1Is9ed8LmDyBm-iG8oZZ3Lho6-4Jt5JYZjNRaXjneGz5-T2tPI0lfCLn1WfJp9Eqh9TV7CZcrm55PBqtnKe47-u0OnLLAmjkSa1szqIGFCcNGWAnKrxm2b0MD1IIx2Ozgn9gEkV8RFdig3HrUO-cgMntANtsY2ct5LtDkeHJupxuUHPFIlq0AMRGXBQYqdySF8S5HnIlonyLMBdj5Ykm1gzpUA7arSdzYLc7b4puSxVQ3LaMLdS0DOhkdte5NL0JWmvekulaarLATMGHVeAY0v3omd7yFqD9Uzlp9a7AN97QJtDks5ewsY9L-OYZZelIYDCep0DB2Q9KFMXig8v4Q8-H6FtVaIyt7Wv8lyHAZTK4Q9HYBGLWjPzRH6j4RIYNVV7AXghCBpiI6K4QmIiYDCeJh7lUS4KuR7W6RagEW9a0sHTfH01KW2DtlmSGNA1507g0HHd9GvOdZrxvG597l1XkVLXuW9sWFCR3hQzsBEeB45iFstmczNMc1kKGxcrfRzyKZrtvk7K2UetpcJD13v1jsn7bN915W6o08e3c0EKaIko8q0DG36wFOLGdAQEW3D-DNJ4QG2dY94B8_Tbq16W1g3nMviZJZPEOuSSHBHijE8uvm5I1CS1JWJ51cNm0j-8JulFy7s_4IGNI14W5q0HbHugmxV1HeHQXABo348JKR5G6RnQDiBJmHEX6OGcmcB14dAzwvyEcP1hNFzqy4pUxHfnCd4Qk9KqmkI4oN7bPKHg12dt_OmHicd8eefDuXMroWSbnqKB4QaXfAAj2i8g6gb9e292WmYvx--BA9ae3LqE675ouHILoGEaED05GXNxtHHE4Q11W0RkROimkM6A45aGnVqVWH15U6nuXcznYm3f04LWB62cxc2BA1aenLMWKmX5P6nat7T9HN04ECwxgo8q4fh1YwiYFXe-KMyiNE9ngiF8RAEXSS4I9mrkO1ptCqLa6cGYA1OezerF4HM9ojGTKX4cDqmJrqn5sDomHOXiY4Rkgue8AOJwrrr5P0GoqVjneUAutYe2qm0pV3jMnCZ2A6BZm6RuJgw8KODH1CK2fNyXLCH5uEA-5ofYex2nRxhwK-EIV9QUC4Oo96qlIv94pjFJew-EqY-__F4ljmN8tcts1K1umtSeUuSXO__zzKzVVYchQ5k0XLpjhusyzZXHkyzyYTcdfwJ7wLOKRTIVxacIZDpzP74Jbyp1t6BtMveUuQEqTw1uUYokDpFBDYaOP9OuhdO9u3zrvr1wRF4MLJQEClErrQLgGyNU0hLl94DN9CmMoBuSfYTJMs4pzA1-yu2oDJqYL3BvtW7b6cx9QTaUeqtLp7E6SEf5Ix9v0ZLvj9uKElEcqYeSZMl-N6oRPwjnCotcs8VsQgycWxkFR5BQFLsDbxA1KzyfQwt6K-iNKuoxggjn7-xfDlEXVXt2krrebnwdR0yp10h4U5kwwUwkryfCCPBUxHKpHtBrxd5z2wpzP2FBdrDIzjvP947lq6dXlLXb6yEx41oMiUoT-2Gz9gjmPQRkbOFfSNrgllN0oTowNYAecHttTqYdpIpVz4gKEMbSXXLzZqp-D-bgX7FkM7GAgrWrYYr-9RqZyWDOUDn88EOWejP6_Hcll4WOHqv761EzbDLIEESeuGFFdPoewCFDChezxqzdOBbuePo6D5-nS6jxA8-1d6tN2l9dFRLRJjoPwHJCNENDXD9nf7QWaNQTN5xRQklHnbN8ag4JcTUT-EqbqKPxkhdT2jmd-BauXP9tIgX0RXvjQfRhCEelrUwnsuvMNqKAtUaQPjuvN4juDZoOIJnPFjrlA6zIGaLC2nH3qtpfs076q1R3t44eM9WXBshJywNcqUwflI9kAdqeKRlLbR1cAwzefPncQJxSFNCg1fMspGSjO30R_P3_zakONZgBhhHTW49SnwAo7IhoUMWO4xRMdQtxLp2r_W8CCRQ9aZSORjH_r--9eYwBP3T-Jv9U7DIVwT0vQSAB77ZKZwwkmjyyti3WRpuQu6-V4XqqBtAzt_dHrgF6eRQALnUZZMVhPPjUKSTxSVYgZfuxebR1z5i3L0VaLGSBfkA0XAgV-Gj5MVriJX7Pc8zDvCog-hCvahLd0pUJQFMhRg1mYlP63kLZ6KLBQREEvdmvAvp3EuHXtgbdeP91EzH6QU2BtfCatFiUZ5Ti-c6BkVJDDjpeJPdesIf_ijOfm5QU2vbFQnxXz4tQ6uq7Iwp3GKFiiVNcSSefscuXXVdJXqtt8_4uvs_MC62VhsWC1BtpkJRzX-sRZoDYUqgVIr2hIkE-o47BVOhT4e5QJRxh23e0gdLxVVwZRHRUMelbQG-I6D0XzN2ZmtK0APpSfxYkfAAjHh2n7ZEa42i9CTP1_7h6Ie8CjOIJjNucsIfDiEq8-hJ1Olu1_dJfufeuJzHQUshHaTuCJZXMb6JKKhbb6J5KIwMPR29R5lE5rILnYS0sJgcIaQF45aZWj1Nceu7-kgOK0pWu-Hg7EgqrMesQwxRYgbAu1ykcr8BTEcPdzGmIDahzKadpPVAJ0H-GPvTzFhUuDY48gtxIvS_3S-vw-17NPPlslouCwGYHhnXh9tRXPC1zaJo3iRgU7CcQwvCCRbryV7fzqaYaN5lyCKlKwOpynfsSNygl2xrgto_Zh_edwUUWB0reaPROWpEiDPUPsRuwaqwrabv0Pr4igB445xsOaB8STddIndpM245jUh6eH0DwVbfQ1VHgGlsyD9l7PI1DeJ5ofYdcyMeSV9swykPdqNEd0Uf2VM5KKfzJCTj8AQ453TiAiHcqpTHaHratwhFeTlZM11jmGoavXw66AkcFD-4Sj6EADDBxhdG6pSOVgSJVJReZ9LH4acuSNskjDgt_7r-XlPolStdn47F7UkBb4lWmNiPl6DljOZI7bX2OFFhB1-RmSdcD7P--rwZsHIpRKq69MgHaFGcctPtCqXAbRL4mjNO_RzqRwzqUWcAY3lMORIgOUDsq5AxbRIikpMgMzkehjLRdERXYooc3itPIjzzwEmMu-6TgNq0mLqFLQXQQhkdRUGDLpjufBV6JHHkfXlRpH8qOQ-mh4EIR5MLj-KBwzgH3vs5EDfRnaUlOAxuJQueBmFZRVIvsweai5gsTd506BkUrOT7tN4Ef-uL655m-0P5G_gwbL8fRVodL7DhsWfwEjl4adBOhIrmJxoU9GwcFjG9U0VN1oRXYCJ0F3-LYZZQAj3xOXDBhZa8HAzefL4ltMLiQJD0UMpVT23kBbiDeTcGNMqpN0GGjXtlhGTBiEb-uFUM-9jFV8Z6BHVjA4thylgNVX1gQaedji_We_LtZIJXty6p6oCOJO-Pd3En1zIVjy4wPptOjFQBbpStUpS5wY_PNOQElsJyG2o_SrcLfUaaiP6lRCkIM4BGMKDlXaWM4ZVpmggG7Gjx_H7n7gcOvjbdUArnL7YFOgiTDqRgCl9JQkNjlhZvaFbi30tIY3bXA_sLryeEfa7NP686hwnqbrNRNyp4tvkEUlsO-TpjB3i47s8tKdDu5EIg4QvVYN6jLYDb3lUZGcyReRZZMph5bBxyQtZ0bQZgduUwDL7Fx2_FV8pMNnS1ceQL6n0ndh-IcRFNj1_CwcBt4UhwngUFhmKriYwCQjfSYdqiF3sV0byBV137D_EPSGRKCO1ljz-_6AevOHmIbwx9LYGDIBFEog1v1dVjjYaWJx9nBHNrRalm3uhmgY-J_0G00
\ No newline at end of file
+xLrjRzqsblwkNo5uFhGDRWVJThl06hEBSHmd3JPn3DlfO5eK1YtHzxAHo3iavTJjzhylIEg5f2Y9b7ITfEKd-rBaEOSF3yd3S_Zo3yO1vLLP96dwMGhk2ShJFIMt1InP-XxnNGhmje1vcb7odgLt4F8aJTaxXFU0WZ8j48RyOGzJGlcE69-o0Z2M_fQaQPe-9JI7z9GKXAP_-UQR_ppvhxzfwTSxbBt3aB7qwoHDVvUYd8hIa2LfJUSau-WUyOTBcA4zWfYptvBqIOhoz3X5cCCjfEbn-lSjn9023lF_IT8j1PY_D3DSdZq_kpmxFpsxIezUYE_iwAyeJrB2Tv2UOOxumqY9Dsqk1Ek2JrBGIhOdT8pVYP6nBPzHA0G-uUbnob496GVbNc4lVgQ_4WpJunIJzvz_8V9N-zm__-z9OkCdc9_izvJSGpeDulBwLGpIaL9l6QIFWqMQmm9EvKvIJBWem-4lF4aP1xnYwN4ASLKEeNbAYWBS4Weu-NCXEe37SrJ0ny3X3XLmf9GK8mG7t_eFrVi50SLp18ly4h8zmMm628A4Dn0YSrl1BG75DIpt1U1G2d51IRaV7V_S9Gu3MKfhJEgkzUDFOoXUGaOgcZqgWBJcELAw5Uim42dYiYcQ8KV19FwJFsz6eBenYl-Mf1YK2dUPdaXETrGarYJzgVtz_xxySgoPXbjokWK9HLk4GJ7HSaPRBHqvjW6yrctZh20CNbLi3t5-Ih3e2kraet-xX80Ht4myASJvCo3d9S4aIH6JDOF5mrcEE1HLkIMaW40KkGC5atakEGTxq18H2j7DRBBbsc_jH7SFQBO3zFEll_ts2qrdcd83dVRDxSzG2dIAEE28uF2UsvBmRiwsHDTMDVSH4gy9HfnGXBTN7E0SqFo3pfKTSAIYSsQCLM2Jl4S8rC9z543DioxMWEbZipTQ2UTFbsl0yWbEd7taUhEdFc_oGlpXu8AIUvJ3V602dQBgc4NxHIQQ-fhiKVPXsfaBrYFbbMWVf0FuMna-GFIhljJU8BEu7dOr3EWRuNmnvFbgje7Irl1K2VqTfB0pIWRj2rRj1L3NmeNKa9BzhUMfTRi96sJ-A1ZUe9_Wc3Syr9sTllhVq_RpMmRp90LvZeGb1OfpmeKVBbEw8FmZs5MW7I_TOwF_a3Wn4awtRW4OVwimVFhWvoS9yQXGk1stC2zbXaVgnKzIuSP3iYTsudoTQ1fUEqphFcRsFwkPsru7b8DmSZK3YgmI_eFyod2KDThIZHm2148lIT7rynmMw1xIXzUGxqZU4oslgBJbnPe7PgW4nUVVRBxwmgMkdK-Xlr0gXTB8B3ypWc-5Uk1j1ROs5g9m0RSWKofZokBKUOqhhifRID_SNG3AKORsmh7SDKYIxi4M2eHWBOP8CNBQakmfsL9GYZPpVQRDuDl_yEyACYwLm1ffXQFVW8PLjoLYGdUinfCVE8ScaXpGBUGNP2yQN2YuWxqq4PmVE73ed_XoZoYlGF6iLWQNbhRZlUve5QeKFjBh5Bn7VSE_smu3P7Gn8CKxh2saYoKLPHAD4kpPDHbPvLa46_dlGO7ovwE8VLzdcJlgrRu9KsUVY7wm1rl0uHly6j8jDFmSd5RmG5403UIoh33AfCg_8blVrMTcbHjTN9jPFwBrPayqZvhJPi8PdIARADwXOXeYNmuuwCRzyBhxEMGcEzKYrudXitkIxiLappXxWz_k2mASVh_3TeYpUuYv6XSlK47uEL3smod475BPJXvA0Yqg8qrlCsoXn0Rd4GgcY8yIHZDbEtSFRg6MdX3OHMgmRVm6Adr6rAmMCJbxL_pqxUNbsVdrpVNBrsTNroUlBrpzZxp79k9kSpe06WVRli6t9vbTiHOnx-7SSyWHG_OL-XMQjNa77npDtxWaX0m36cA6duE7C17720rEPvOHC3fn3TWB8U6LJsrxG87mkwSWUzVgbu5sSFWt1AeF5-8b-AHP45xgNXtvnKfP70uE3G27G7X8luqR97rUDi4AdR6zV3vBGy9JGM1ds5SaNg4I5O0Y_l22achl5_iDrt-BpKHHA_U1N77mbwoeSfY_l3id_2Gl4NOqykyZO4q3THSQ8PzdXQ-BYnhSlmFi9POhI30GgEvvi2omv2HukWDuwp4UXCmdQsy3c3_tsFuo3DzVkJtcfCW7lhHE8KLYo5gK7wHxek-DCN1tJHoUFRefwZarXG-rD1STmhIozb94RR5a1XAyTsY0gdOxgd9telf6W6A7SCU6x1jdMECCh7MzJfNkFYUkNKolWU70w1AReKyqQdORXPkg3QlGoNyk7fz44CS9pDJ4hi_c4uNRMRsNpzKqqqroYF18cmyHt4OBcYNH3h2TtkbhISzDnucU_9haw0wjreWotbJ70oPQUd3bxcSkpLTsvQIw01HY7c96XEoX_vWivLgIxn7GmqqUoxl-Hhldb9HN9Q2SxI3arnnS503hIvxnVNQKk-FiI_VVIUJFPTpf5SBlrZqEuT-KsqwCu9aspF4evRk4-m9aiApZcOkziqwYA_bg3-c5jUNsR7whfbFQC0GCLUdvYo2LKgKGthvONHFd6zwqgwBUxqsW--Y6ptE7Mv0PAjZqn6M-Pe1n1Ohd5SuHthuccEzw9BCSkGReMerTdxgXTNX8a5eAtUvG-Urg3pC42iJuMGbGCPN9IGXX6HIMviIaH7fcQyzJ7QIV4gcNpIcK7fyVCIF49e9JaZlBEoOGJOz9jVyWlk0fDutXcFa3UOEb5ymzUZYMCK53OXYc1rgm-Jck_DdFc3s5xNLHtHOeCxOQxW25-J-H7BJCStilPN3e6U30D_2ZmadFnoFnBzjCmQ0R4PxqEFtivREpy-kNpr-UFVli-FX8hk8igMzKcID_bEH60dn7kfYOiMYYC2CqFoMLsejKPs7P3fQWhzXQPJtSblux61JIaoY7xm8Wxx48bPzmiGRKzsMbn-dVQiTH1XaRpk3VI7CRYE-VZxQHXVwQ_Iw7TmWZtLqbWxKGWzi23cmmkEFxAyWvmbkZZDM5XGRbrGmNd52Rru2zQmlk0Uz3yHgW0b9pLygj2AbozoumRUQsjuND3NktSmiKCxzSPzZxsvwaFvDRnvR29sARdV2nBJRMA6HLmTkTXQpAX3Ph8wAE6EOG0vn17zWuHTijfEqssZQNSPxAfDdsJi3Xbj073V8PstQavqPtlVhF2rSAXhkgS2BHdWAJ5mrSLMK9A8BUKxMnudNDrThA_EYlHrgP8xKOk4Vm_SlSJQ0JxbBcQwWqfOw29XZfPpWDoltMDxpZUUGHC4O9AT_GBS3eLtjNxxUR__2sjs19rCQQAtLme4V6cDWGPfnMsNW9AaU0Pl_fj0Kk4NeMj5hl6lGvaaigRxlmJkMVrfIQSACgRRcZLcKXdARoGcKqr5QKE-fcGQ-ZbRKqAb3LK6lA7uLjg96KwlXBCurOFj-FGSS9WzFBsunG2gK-FeCWVuKmNEuWgnNOWtlhSjHF02_KvEyG4x-zRC7bgXokWag-Xn5DVCIn-CwGgMNvSQYmdxBba4DQ8WoC6AdH7p6WukJ6VQTfZd3jYigdD-wATd5VrmxbsHwlbvTHSeYECNFX_gbeyYeQkBRIsOFaewbSUnd2enabhEOOT5U3fgXruK_C67v4tVp33Oi77G_ZGBYeRHvrt8ZBaBWCSQnOLnpbx1NpuSSujRoeg-FjXrgygF_4l5agtpKguGUVwPy-1Ej6aszI_iSOkBNMMB7ZMBhmMFtcZsFhwFLPszJfOHKjdU2MyJi2lNX0NBLLnU62Udj1qsH_j1MJlM_JyaXfwaegdX5zWwQw4PbpaErA4wR0q81gGCD63D2u_WqT3Xqqri7hZdIznVZXnxeGfyEgxWB4HN3RhEgINlBnBGJgYp1ReVnwqRP2kOZapj9V_G1JszZphWVus8xsXs-FiWHRkazve9w7t5lLLGN-mgyut8SSoy5rcR6cgxDyMiTnUc3qZ-8GxZgACr5G7iFfvR6Ru36kjlFMAVsUVFNpUivMZOoKUp6yE6S-xNhSnz-wEk_MnZ_E5jOP1nG_bjDVrzRHJoFz0vp_wcFgTzhBxvoMVhfhpWMdQnmCNV22IcWlEFJnCUrOzhpBK6UVFhj3bF7JUa3qoViINuZJ1NBtwdJkSyAln2B3JuwwRbxwxocpoEejiiZiBR78x2lmu96yeJDusbZITMHTarTQYNbSY3VlAlKBiMT3NlNevxu86yHPHaOds2ln_ID-lUP27T2vW9GHZ66nr5YtwHctI4FpjfpG6lOEYjGRs0N3tjLQwhRLPRkBuTsT6kydtH_GUVf93JmkPPRzs_LXJ-4T-iLnL_ITAxdfdkvpJtLRk4xm7U9qytnZhNDWMLVFkX5ghuSTFKZc-F8l26ZeuR6HHzS-rD0sTj2GZbvvN45afOaJ7PyT7ulV81Dtvzu3-e3mLWTWwpbsuh7x-IxLxRndb1gBcA532-ikRaltDSja3zjxbktxPAIqJqd-q6x7RwxjSYOwpoLM58tWuUzHy3_kKmA4vuJcQD6MajQ4kdP8rQdLzX9oODvhsAJuEV1AL4AdMhgj375FZ_UJ3o5N4dyCZpduNk7ionsnxeojESik676yJVUgA9opcMqiTFLNTj41RCSSieoYlRDNhsnPNa5fLcgNZlgUh0mv6yXwNCRCpLxc0-jnds5zrWJXRHTN-hDZudqjy9oxWDUyfdRstRDTvL5vbH4AvVsh8DkjEie9jz1NbXrBGF6rVQrkRGFTsDOQ4vBr7bK8Rb_4uKa1_OwY7Y8SutERn9EqX8ofjnxQs9owdRSvoTj41jjpl5t5RaJM9XZlSJV3t0ldh4j-xm55G9g3vjbrRpCWC2Xqo4bnUEvxsrn2kB9Ty-4ZVu95VJZwpJsa5ELmxJvITImcYZIdj0zrmUSzA2hOZnpeh7qqLXRggsB04NRG7O6-7RvoExmblsylSrJhWGyqsd7o3hfiEUvQIlWsNtifQm0Uqm3iI0al3_n6qex99tZem10cbc-lo5Oj0zVtrBZzN0NOTa7Zz3WzkRmy-TUFFpNpYWJlHkOw4lS2Y2gSL7n7gFGWEsuEQ2hfUe_8wHJQ4GpFdHT06I5iJH8HhmRuNbvOWHX76MlsaDy8c-Ac56-kt91w61FZkRWNwgO4z9tC8EP8UPDnafRjvfiRsUJAgP2a_wv0WVZ33M2iec9loKqEsEwM09etuK9GA6xQ1qu1meAMseB5j6zDG5slXeoUouH6LvltQ9RwSfU-QuEOCbVTqQX7l37kXvDdzQhG0vHk2KbepoVnmybpL718_QBmLOcTosAv0slqDOiVJbwTs8-OUNZDX3b_e-MGQMLn0rWlwVQXbzG1EJ7bYBo_I3PLfLH39kax83eCBICSm_StbBo_CLSUfCWTgVKoxuQXhQCbNfcREEguA8qmZS8qi9aaGjM7olBcH8Zw48sz-n7nSkFbyFn7gDnGHOX68Ufs2DPRdYDUddZPxzj5j4RGYlEWFrPnS0_3o_RiHh16m8euZU8K-vuNJ14U2vwHfxKZG3P0rjCJ4I05qEXBsH4a5qWHe1v0SLNYWENPirSUaEG15SkjYea7wm7ERpZOzdpr8hC4iQtQO9tLKQ8Rb4Dv5AY_tLHzTyOjrOtpDyxWrWG-GQ-IHvLoGHO1iW2A4vW3b94dnID03K0n6Z25K5ocZe0p_fHsn6a0fuYH2oFtuzaHe0QWyLUk8qusJcENGaIqRBJYFES1KWJ70Ku4nGPbyC7ZY4-Bp_3xo14a5qWH81T04PKUAiElvqQ4MeIYyWn24r6nK1byYZV2qy4JeHc49i9YmHBo7EwV3fcGPs__TF1CleKRSJ9n6dZ5DSBaXCdnH6X4QWGfztkM4R9foAAAJU8LjSe79SV3516f8QJYM1M4LHoa9e6A20qYv7xlCA9aeZJqc6B4oOLJL2OFaE945mXLx7TJE4M01W4QUESkmkI6AK9aGHJFVmH25E6nuHb-zIu2f0CKWR60cNgpBg9ae1HtWKuX596naN6_lHN14U0ut_gAq4Xe1X_wYlXe-4IziN29ryyE8xED0uqBbhXeS0FZUCyg8jCW4q6nGB5Vo8geI5IctjoACBbXcdYMAyBcXYr2P4CqyhvNH4WXr7yUB28Za8dMZmiLntDW4PW6cE7Nn2965aOH6mSsms-OHOmQZ28e5YZz1xGYBW8NzpjQ4Hk5ZVqos9yQb-Ipz8GnaY7rBacI97ATdXvzTPH--UMFVxajGFPkioy0mXk-Gzax31dxxwz_-l9BcKKx0YpaQdzjv3FHYJfzvalClZqbFKwt8ccZ_dHVacJahIoBctYK3-OMkz_Iz0WVfRqBnTDZyHoScRP9mI2pnF4KJW3_gJUQqcEBjyYoSQIPnLQNgdKLUmlKbaY6daYOBL3_CPsUL6sMpT27jkmXvLFNHC-HJfKye6nDZmsLJ9Oxe5p3cZOjssFed5ePpn1E5RGacoTG4-VhIQ4pRpAjmj5ujj75jlcs6ZTpH1-hQ2_tQZAu-yWcfTtNrNYfCNggdZBNQZorVZ75kCkw6lVXdcup7-hN8RpRZO3fSShsC4EWG8otgQ9HwnwZfPbY9BtNDMPQ6ghiSVsgh_rb8zEVKrBsoYoIe7Sfjh2khBIDOVq83ijOzcwaaXQTbRWoK_VEnlIuljNV-k1aRZr_4TJCZlT5f55ffczwAMeuQKo65MsFJFutQMg4S-vOT0ghM3MABNublIly0rXul9WWraWYrqRz6S-yI1X7p4aC9deggwLmhg729vuxkP3IfvjbT7lMdyx0Cl52EGrflcBXrlPG7WCnvQuLvCzwwRcSk3CIAvYvqfi5h-FAxy11sdMpUZrMMvyshaIM3DqOlUx2T2-9DjhJhJrNu3x1pyKfbBnJGWLoysbJiro7KNyhUO_TTZJxAbBeHjSmyyxWMiApuS5CuiboTqH3UPCIAdnke1XoGqZ33Z26jXZc3aB1mWnPKPkUBJMFTa_j5N5JwKE9sgskX3LRUSLHoZaipOUxDvoAIQLjyy4HC9YD_WX1MnEcfywYAySkG26kVMNP3XNvF3IC1LjhpjRzAvXQVuE8cDj4oXiCD-g_w_T4KPT9iXk_9qdFJ-lFTEWUDs553dpgUtzNuMzUls3mDpyDyBTliGvQ5xdATtQqzQangAtarSLe_UrwsgQNrF5UxBwglViEhrKm_NO0gWC6geC5Kt50GjNF_8MYh7usjvnipCycScPM_TiSoTgxYvj5jFfLDr0uHVkWYRAnZAIbj5b7SxwS5SxX7S8mxzIpKBxx7MgzDF15xqiIRdqF-YisVRV5NFfc6sxqfapqQ9e_MUkMOYiEXyolDOzm-YRjZSQ39TRbfI7ZiLNoEEKKRRUGmlpfmwRxaNZSGpXh6h3lVpGcWjxzOPl-nxRauuZOtYBe4jJAqfWlSj9otxAtHA1Ma-ywWWw0QjrUd_zecuMtrc9vseik1dH8VTpeS1r02YVtAQuhgMYh4Moinmmfn4f2Z7KGlvwnaY13BU5ahT-PjaeJR3k2Fcsmc7-0FLswk2RE6x_MNbfqf3U32uuL9HaTgTooZ0nKIwMPR29R5hF0wfAuH60RfrJ9o5Ld2oHmZXNcfu7-kgOK0pWu-HgNJgqrMuqErst6LQLmJvPDAfog_ihRXxBwVu_zGjQLHAj7ix2HwXVMcY-JdmpuW3oxIFYvnRLnXrZvaowM7nx_rrsXk4xRjFzrBb67ktv5lp-n4oUT_ula6VGRzkH8rjmhPN3hVFNJUwL6lFRQHfHPeaxl3jYuDmeivTUvWRNwIr-mNYNzpfYbpZNI4djmkjZcCZFRLsVIgLRISoujyair8y55pp-bHWuxFMdzJskumpOzlrYY0Jq_HAwSYpMXVeoSpUCo43BH63b39lDuDOiDdRfQMSoYvmv9r8dw9gsaljDZzY2bXDuwfdmsWxOP9kqewuFwBBeTlZN11jnGIivfgA5A-lDDE0VjIYUDzBvBdK6piKSgyNUJxaW95P6acyVNsjTEw__7TxClv-lSwZp4tB6tURa4FeoNyHl6ThiOpQ7j32QF_bQ6ERnSLcF7fsyrgZCaoywVKA8sJ5bVWcbtjzYqH6cR54ojFVjSzyRwP8oWcAWBK6OF5KmKDqSArrQx5RU9DTkCzLrZg_CSt3tcbC6PEwdRRxrJ2rpyCtL68PYheMer2qtNzAFFWIfddogN-CcaZLH3-tkYPemrpZW8yitkjZPz8NrxqQ7pC1SRo_Z8FVKLtudzpGLW_Eq-5wSQHKIDraPEA0ENyTgmw5kkeTJzN2CABX-7I2X_rxOknQs_hMiERNjpKKVR-19EsnMnhedt0zQXrCTQ0Q-0-k1YNB4OM8U7SZ5d6qLQdsn2wNNNnWZ78ONg605hKuC9sdFhffjXPz5o39eTsSKZfkkoXfR3lVKXQKxeBpmgSr-JwH_NcCMYBTAu7hzlwJUkXYPbugWnFaf_rtWIpXqycpcoiSIOMOs1yxh-f1tMAVCvEgPxjBovsITBMEy0mIfizFC31sBX3aOqeG3cVGPk5dElHbkNfMcYiP0pVikQRGb1QoUvxQjPGDW8bAzAUi3V_a_4TwHgzgMRwRt4LUKnFBswqnQjoyXBgPt76EVgG-PpHZ909sflgJIULYS_68TPbutXDRlUKL9jTpq3TsD-xXcBvdUtfUOGV83TICxfuw2ZGhYEQyuThGgnTAbtNthJ63qMoDe5lkpbLyCdnqHEfm_0VQ9iRdzX_eV5vh8OE3MrT10nVUahsKXR_Hp6l0vcxpMVFzZKpltW9hR5KGtRYy_FfOU7m-zBuMyyg6PmTQwWMlYn3JNuTsFbnApTaT9qsQge1IcMMLbKTz_EhPiYqdXQvr9HTKkIVq2SLsMH_3y0
\ No newline at end of file
diff --git a/docs/logical_data_model.puml b/docs/logical_data_model.puml
index e54c2637a7..c87f25cbee 100644
--- a/docs/logical_data_model.puml
+++ b/docs/logical_data_model.puml
@@ -86,6 +86,14 @@ class ActivityReportGoals{
timeframe : text
}
+class ActivityReportObjectiveCitations{
+ * id : integer :
+ * activityReportObjectiveId : integer : REFERENCES "ActivityReportObjectives".id
+ * citationId : integer : REFERENCES "MonitoringStandards".id
+ * createdAt : timestamp with time zone : now()
+ * updatedAt : timestamp with time zone : now()
+}
+
class ActivityReportObjectiveCourses{
* id : integer :
* activityReportObjectiveId : integer : REFERENCES "ActivityReportObjectives".id
@@ -1242,6 +1250,20 @@ class ZALActivityReportGoals{
session_sig : text
}
+class ZALActivityReportObjectiveCitations{
+ * id : bigint :
+ * data_id : bigint
+ * dml_as : bigint
+ * dml_by : bigint
+ * dml_timestamp : timestamp with time zone
+ * dml_txid : uuid
+ * dml_type : enum
+ descriptor_id : integer
+ new_row_data : jsonb
+ old_row_data : jsonb
+ session_sig : text
+}
+
class ZALActivityReportObjectiveCourses{
* id : bigint :
* data_id : bigint
@@ -2483,6 +2505,7 @@ Grants "1" --[#black,plain,thickness=2]-- "1" GrantNumberLinks : grant, grantNum
ActivityReportCollaborators "1" --[#black,dashed,thickness=2]--{ "n" CollaboratorRoles : collaboratorRoles, activityReportCollaborator
ActivityReportGoals "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportGoalFieldResponses : activityReportGoal, activityReportGoalFieldResponses
ActivityReportGoals "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportGoalResources : activityReportGoal, activityReportGoalResources
+ActivityReportObjectives "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportObjectiveCitations : activityReportObjective, activityReportObjectiveCitations
ActivityReportObjectives "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportObjectiveCourses : activityReportObjective, activityReportObjectiveCourses
ActivityReportObjectives "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportObjectiveFiles : activityReportObjective, activityReportObjectiveFiles
ActivityReportObjectives "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportObjectiveResources : activityReportObjective, activityReportObjectiveResources
@@ -2561,6 +2584,7 @@ MonitoringReviewStatusLinks "1" --[#black,dashed,thickness=2]--{ "n" Monitoring
MonitoringReviewStatusLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringReviews : monitoringReviews, statusLink
MonitoringStandardLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringFindingStandards : statusLink, monitoringFindingStandards
MonitoringStandardLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringStandards : monitoringStandardes, statusLink
+MonitoringStandards "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportObjectiveCitations : citation, activityReportObjectiveCitations
NationalCenters "1" --[#black,dashed,thickness=2]--{ "n" EventReportPilotNationalCenterUsers : nationalCenter, eventReportPilotNationalCenterUsers
NationalCenters "1" --[#black,dashed,thickness=2]--{ "n" NationalCenterUsers : nationalCenter, nationalCenterUsers
NationalCenters "1" --[#black,dashed,thickness=2]--{ "n" NationalCenters : mapsToNationalCenter, mapsFromNationalCenters
@@ -2621,6 +2645,7 @@ ActivityReportCollaborators "n" }--[#black,dotted,thickness=2]--{ "n" Roles : ro
ActivityReportGoals "n" }--[#black,dotted,thickness=2]--{ "n" Resources : resources, activityReportGoals
ActivityReportObjectives "n" }--[#black,dotted,thickness=2]--{ "n" Courses : courses, reportObjectives
ActivityReportObjectives "n" }--[#black,dotted,thickness=2]--{ "n" Files : files, reportObjectives
+ActivityReportObjectives "n" }--[#black,dotted,thickness=2]--{ "n" MonitoringStandards : citations, activityReportObjectives
ActivityReportObjectives "n" }--[#black,dotted,thickness=2]--{ "n" Resources : resources, activityReportObjectives
ActivityReportObjectives "n" }--[#black,dotted,thickness=2]--{ "n" Topics : topics, activityReportObjectives
ActivityReports "n" }--[#black,dotted,thickness=2]--{ "n" Files : files, reports
diff --git a/src/migrations/20241118093025-add-activity-report-objective-citations.js b/src/migrations/20241118093025-add-activity-report-objective-citations.js
new file mode 100644
index 0000000000..196e8866ab
--- /dev/null
+++ b/src/migrations/20241118093025-add-activity-report-objective-citations.js
@@ -0,0 +1,53 @@
+const { prepMigration } = require('../lib/migration');
+
+module.exports = {
+ up: async (queryInterface, Sequelize) => queryInterface.sequelize.transaction(
+ async (transaction) => {
+ await prepMigration(queryInterface, transaction, __filename);
+ // Create GrantReplacementTypes table
+ await queryInterface.createTable('ActivityReportObjectiveCitations', {
+ id: {
+ type: Sequelize.INTEGER,
+ autoIncrement: true,
+ primaryKey: true,
+ },
+ activityReportObjectiveId: {
+ type: Sequelize.INTEGER,
+ allowNull: false,
+ references: {
+ model: {
+ tableName: 'ActivityReportObjectives',
+ },
+ },
+ },
+ citationId: {
+ type: Sequelize.INTEGER,
+ allowNull: false,
+ references: {
+ model: {
+ tableName: 'MonitoringStandards',
+ },
+ },
+ },
+ createdAt: {
+ type: Sequelize.DATE,
+ allowNull: false,
+ defaultValue: Sequelize.fn('NOW'),
+ },
+ updatedAt: {
+ type: Sequelize.DATE,
+ allowNull: false,
+ defaultValue: Sequelize.fn('NOW'),
+ },
+ }, { transaction });
+ },
+ ),
+
+ down: async (queryInterface, Sequelize) => queryInterface.sequelize.transaction(
+ async (transaction) => {
+ await prepMigration(queryInterface, transaction, __filename);
+ await queryInterface.dropTable('ZALActivityReportObjectiveCitations', { transaction });
+ await queryInterface.dropTable('ActivityReportObjectiveCitations', { transaction });
+ },
+ ),
+};
diff --git a/src/models/activityReportObjective.js b/src/models/activityReportObjective.js
index 6428985046..4f7eabbcf1 100644
--- a/src/models/activityReportObjective.js
+++ b/src/models/activityReportObjective.js
@@ -16,6 +16,7 @@ export default (sequelize, DataTypes) => {
ActivityReportObjective.hasMany(models.ActivityReportObjectiveTopic, { foreignKey: 'activityReportObjectiveId', as: 'activityReportObjectiveTopics' });
ActivityReportObjective.hasMany(models.ActivityReportObjectiveResource, { foreignKey: 'activityReportObjectiveId', as: 'activityReportObjectiveResources' });
ActivityReportObjective.hasMany(models.ActivityReportObjectiveCourse, { foreignKey: 'activityReportObjectiveId', as: 'activityReportObjectiveCourses' });
+ ActivityReportObjective.hasMany(models.ActivityReportObjectiveCitation, { foreignKey: 'activityReportObjectiveId', as: 'activityReportObjectiveCitations' });
ActivityReportObjective.belongsToMany(models.File, {
through: models.ActivityReportObjectiveFile,
@@ -29,6 +30,14 @@ export default (sequelize, DataTypes) => {
otherKey: 'topicId',
as: 'topics',
});
+
+ ActivityReportObjective.belongsToMany(models.MonitoringStandard, {
+ through: models.ActivityReportObjectiveCitation,
+ foreignKey: 'activityReportObjectiveId',
+ otherKey: 'citationId',
+ as: 'citations',
+ });
+
ActivityReportObjective.belongsToMany(models.Resource, {
through: models.ActivityReportObjectiveResource,
foreignKey: 'activityReportObjectiveId',
diff --git a/src/models/activityReportObjectiveCitation.js b/src/models/activityReportObjectiveCitation.js
new file mode 100644
index 0000000000..911af97d7d
--- /dev/null
+++ b/src/models/activityReportObjectiveCitation.js
@@ -0,0 +1,39 @@
+const { Model } = require('sequelize');
+
+/**
+ * Junction table between ARO and Citations.
+ * @param {} sequelize
+ * @param {*} DataTypes
+ */
+export default (sequelize, DataTypes) => {
+ class ActivityReportObjectiveCitation extends Model {
+ static associate(models) {
+ ActivityReportObjectiveCitation.belongsTo(models.ActivityReportObjective, {
+ foreignKey: 'activityReportObjectiveId',
+ onDelete: 'cascade',
+ as: 'activityReportObjective',
+ });
+ ActivityReportObjectiveCitation.belongsTo(models.MonitoringStandard, { foreignKey: 'citationId', onDelete: 'cascade', as: 'citation' });
+ }
+ }
+ ActivityReportObjectiveCitation.init({
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: DataTypes.INTEGER,
+ },
+ activityReportObjectiveId: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ },
+ citationId: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ },
+ }, {
+ sequelize,
+ modelName: 'ActivityReportObjectiveCitation',
+ });
+ return ActivityReportObjectiveCitation;
+};
diff --git a/src/models/activityReportObjectiveTopic.js b/src/models/activityReportObjectiveTopic.js
index a5dd249a25..ca941b7758 100644
--- a/src/models/activityReportObjectiveTopic.js
+++ b/src/models/activityReportObjectiveTopic.js
@@ -25,11 +25,11 @@ export default (sequelize, DataTypes) => {
type: DataTypes.INTEGER,
},
activityReportObjectiveId: {
- type: DataTypes.STRING,
+ type: DataTypes.INTEGER,
allowNull: false,
},
topicId: {
- type: DataTypes.STRING,
+ type: DataTypes.INTEGER,
allowNull: false,
},
}, {
diff --git a/src/models/monitoringStandard.js b/src/models/monitoringStandard.js
index 1552682382..ae9d2523ec 100644
--- a/src/models/monitoringStandard.js
+++ b/src/models/monitoringStandard.js
@@ -13,6 +13,14 @@ export default (sequelize, DataTypes) => {
* status: standardId -< MonitoringStandardLink.standardId
*/
+ MonitoringStandard.hasMany(models.ActivityReportObjectiveCitation, { foreignKey: 'citationId', as: 'activityReportObjectiveCitations' });
+ MonitoringStandard.belongsToMany(models.ActivityReportObjective, {
+ through: models.ActivityReportObjectiveCitation,
+ foreignKey: 'citationId',
+ otherKey: 'activityReportObjectiveId',
+ as: 'activityReportObjectives',
+ });
+
models.MonitoringStandardLink.hasMany(
models.MonitoringStandard,
{
From 18835970532ed527012d4d8b2f141e370f5c0ce3 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Tue, 19 Nov 2024 10:15:39 -0500
Subject: [PATCH 013/198] fixes per Garrett
---
src/tools/createMonitoringGoals.js | 8 ++++----
src/tools/createMonitoringGoals.test.js | 8 ++++----
2 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/src/tools/createMonitoringGoals.js b/src/tools/createMonitoringGoals.js
index f907640c33..f70a101ae8 100644
--- a/src/tools/createMonitoringGoals.js
+++ b/src/tools/createMonitoringGoals.js
@@ -4,14 +4,14 @@ import {
} from '../models';
import { auditLogger } from '../logger';
-const createMonitoringGoals = async () => {
- const cutOffDate = '2024-10-01';
- const monitoringTemplateName = '(Monitoring) The recipient will develop and implement a QIP/CAP to address monitoring findings.';
+const createMonitoringGoals = async (monitoringGoalTemplateId = null) => {
+ const cutOffDate = '2024-10-01'; // TODO: Set this before we deploy to prod.
+ const monitoringTemplateId = monitoringGoalTemplateId || 24872;
// Verify that the monitoring goal template exists.
const monitoringGoalTemplate = await GoalTemplate.findOne({
where: {
- templateName: monitoringTemplateName,
+ id: monitoringTemplateId,
},
});
diff --git a/src/tools/createMonitoringGoals.test.js b/src/tools/createMonitoringGoals.test.js
index 1325bc47b8..15d4877081 100644
--- a/src/tools/createMonitoringGoals.test.js
+++ b/src/tools/createMonitoringGoals.test.js
@@ -1770,12 +1770,12 @@ describe('createMonitoringGoals', () => {
it('creates monitoring goals for grants that need them', async () => {
// 1st Run of the CRON job.
- await createMonitoringGoals();
- await assertMonitoringGoals();
+ await createMonitoringGoals(goalTemplate.id);
+ await assertMonitoringGoals(goalTemplate.id);
// 2nd Run of the CRON job.
// Run the job again to make sure we don't duplicate goals.
- await createMonitoringGoals();
- await assertMonitoringGoals();
+ await createMonitoringGoals(goalTemplate.id);
+ await assertMonitoringGoals(goalTemplate.id);
});
});
From 979bc1198b6c9002687dd68bee1101838449c670 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Tue, 19 Nov 2024 11:05:14 -0500
Subject: [PATCH 014/198] put back profiles
---
docker-compose.yml | 25 +++++++++++++++++++++----
1 file changed, 21 insertions(+), 4 deletions(-)
diff --git a/docker-compose.yml b/docker-compose.yml
index 0f9ca32645..0fa22811e4 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,6 +1,8 @@
services:
api-docs:
image: redocly/redoc
+ profiles:
+ - full_stack
ports:
- "5003:80"
volumes:
@@ -11,6 +13,8 @@ services:
image: postgres:15.6
container_name: postgres_docker
env_file: .env
+ profiles:
+ - minimal_required
ports:
- "5432:5432"
volumes:
@@ -18,6 +22,8 @@ services:
shm_size: 1g
minio:
image: minio/minio:RELEASE.2024-01-01T16-36-33Z
+ profiles:
+ - full_stack
env_file: .env
ports:
- "9000:9000"
@@ -27,12 +33,16 @@ services:
command: server /data --console-address ":9001"
aws-cli:
image: amazon/aws-cli
+ profiles:
+ - full_stack
env_file: .env
command: ["--endpoint-url", "http://minio:9000", "s3api", "create-bucket", "--bucket", "$S3_BUCKET"]
depends_on:
- minio
clamav-rest:
image: ajilaag/clamav-rest
+ profiles:
+ - full_stack
ports:
- "9443:9443"
environment:
@@ -40,6 +50,8 @@ services:
similarity_api:
build:
context: ./similarity_api
+ profiles:
+ - minimal_required
ports:
- "9100:8080"
env_file: .env
@@ -49,18 +61,24 @@ services:
- "./similarity_api/src:/app:rw"
redis:
image: redis:5.0.6-alpine
+ profiles:
+ - minimal_required
command: ['redis-server', '--requirepass', '$REDIS_PASS']
env_file: .env
ports:
- "6379:6379"
mailcatcher:
image: schickling/mailcatcher
+ profiles:
+ - full_stack
ports:
- "1025:1025"
- "1080:1080"
testingonly:
build:
context: .
+ profiles:
+ - full_stack
ports:
- "9999:9999"
depends_on:
@@ -73,6 +91,8 @@ services:
- NODE_ENV=development
sftp:
image: jmcombs/sftp:alpine
+ profiles:
+ - full_stack
volumes:
- ./test-sftp:/home/tta_ro/ProdTTAHome
- ./test-sftp/sshd_config:/etc/ssh/sshd_config
@@ -81,7 +101,4 @@ services:
command: ${ITAMS_MD_USERNAME:-tta_ro}:${ITAMS_MD_PASSWORD:-password}:1001
volumes:
dbdata: {}
- minio-data: {}
- yarn-cache: {}
- node_modules-backend: {}
- node_modules-frontend: {}
\ No newline at end of file
+ minio-data: {}
\ No newline at end of file
From 51cbb1b469d27e2b9a31315ff1cef7a01851837a Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Tue, 19 Nov 2024 13:34:27 -0500
Subject: [PATCH 015/198] hook up aro citation to links tables
---
docker-compose.yml | 20 +
docs/logical_data_model.encoded | 2 +-
docs/logical_data_model.puml | 4 +
...add-activity-report-objective-citations.js | 20 +-
src/models/activityReportObjectiveCitation.js | 51 ++-
src/models/monitoringFinding.js | 1 -
src/models/monitoringFindingLink.js | 1 +
src/models/monitoringReviewLinks.js | 1 +
src/models/monitoringStandard.js | 9 -
src/models/monitoringStandardLink.js | 7 +
.../activityReportObjectiveCitation.test.js | 361 ++++++++++++++++++
11 files changed, 462 insertions(+), 15 deletions(-)
create mode 100644 src/models/tests/activityReportObjectiveCitation.test.js
diff --git a/docker-compose.yml b/docker-compose.yml
index 0f9ca32645..1931e66c4e 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,6 +1,8 @@
services:
api-docs:
image: redocly/redoc
+ profiles:
+ - full_stack
ports:
- "5003:80"
volumes:
@@ -11,6 +13,8 @@ services:
image: postgres:15.6
container_name: postgres_docker
env_file: .env
+ profiles:
+ - minimal_required
ports:
- "5432:5432"
volumes:
@@ -18,6 +22,8 @@ services:
shm_size: 1g
minio:
image: minio/minio:RELEASE.2024-01-01T16-36-33Z
+ profiles:
+ - full_stack
env_file: .env
ports:
- "9000:9000"
@@ -27,12 +33,16 @@ services:
command: server /data --console-address ":9001"
aws-cli:
image: amazon/aws-cli
+ profiles:
+ - full_stack
env_file: .env
command: ["--endpoint-url", "http://minio:9000", "s3api", "create-bucket", "--bucket", "$S3_BUCKET"]
depends_on:
- minio
clamav-rest:
image: ajilaag/clamav-rest
+ profiles:
+ - full_stack
ports:
- "9443:9443"
environment:
@@ -40,6 +50,8 @@ services:
similarity_api:
build:
context: ./similarity_api
+ profiles:
+ - minimal_required
ports:
- "9100:8080"
env_file: .env
@@ -49,18 +61,24 @@ services:
- "./similarity_api/src:/app:rw"
redis:
image: redis:5.0.6-alpine
+ profiles:
+ - minimal_required
command: ['redis-server', '--requirepass', '$REDIS_PASS']
env_file: .env
ports:
- "6379:6379"
mailcatcher:
image: schickling/mailcatcher
+ profiles:
+ - full_stack
ports:
- "1025:1025"
- "1080:1080"
testingonly:
build:
context: .
+ profiles:
+ - full_stack
ports:
- "9999:9999"
depends_on:
@@ -73,6 +91,8 @@ services:
- NODE_ENV=development
sftp:
image: jmcombs/sftp:alpine
+ profiles:
+ - full_stack
volumes:
- ./test-sftp:/home/tta_ro/ProdTTAHome
- ./test-sftp/sshd_config:/etc/ssh/sshd_config
diff --git a/docs/logical_data_model.encoded b/docs/logical_data_model.encoded
index 09c2268096..ee385ec3b6 100644
--- a/docs/logical_data_model.encoded
+++ b/docs/logical_data_model.encoded
@@ -1 +1 @@
-xLrjRzqsblwkNo5uFhGDRWVJThl06hEBSHmd3JPn3DlfO5eK1YtHzxAHo3iavTJjzhylIEg5f2Y9b7ITfEKd-rBaEOSF3yd3S_Zo3yO1vLLP96dwMGhk2ShJFIMt1InP-XxnNGhmje1vcb7odgLt4F8aJTaxXFU0WZ8j48RyOGzJGlcE69-o0Z2M_fQaQPe-9JI7z9GKXAP_-UQR_ppvhxzfwTSxbBt3aB7qwoHDVvUYd8hIa2LfJUSau-WUyOTBcA4zWfYptvBqIOhoz3X5cCCjfEbn-lSjn9023lF_IT8j1PY_D3DSdZq_kpmxFpsxIezUYE_iwAyeJrB2Tv2UOOxumqY9Dsqk1Ek2JrBGIhOdT8pVYP6nBPzHA0G-uUbnob496GVbNc4lVgQ_4WpJunIJzvz_8V9N-zm__-z9OkCdc9_izvJSGpeDulBwLGpIaL9l6QIFWqMQmm9EvKvIJBWem-4lF4aP1xnYwN4ASLKEeNbAYWBS4Weu-NCXEe37SrJ0ny3X3XLmf9GK8mG7t_eFrVi50SLp18ly4h8zmMm628A4Dn0YSrl1BG75DIpt1U1G2d51IRaV7V_S9Gu3MKfhJEgkzUDFOoXUGaOgcZqgWBJcELAw5Uim42dYiYcQ8KV19FwJFsz6eBenYl-Mf1YK2dUPdaXETrGarYJzgVtz_xxySgoPXbjokWK9HLk4GJ7HSaPRBHqvjW6yrctZh20CNbLi3t5-Ih3e2kraet-xX80Ht4myASJvCo3d9S4aIH6JDOF5mrcEE1HLkIMaW40KkGC5atakEGTxq18H2j7DRBBbsc_jH7SFQBO3zFEll_ts2qrdcd83dVRDxSzG2dIAEE28uF2UsvBmRiwsHDTMDVSH4gy9HfnGXBTN7E0SqFo3pfKTSAIYSsQCLM2Jl4S8rC9z543DioxMWEbZipTQ2UTFbsl0yWbEd7taUhEdFc_oGlpXu8AIUvJ3V602dQBgc4NxHIQQ-fhiKVPXsfaBrYFbbMWVf0FuMna-GFIhljJU8BEu7dOr3EWRuNmnvFbgje7Irl1K2VqTfB0pIWRj2rRj1L3NmeNKa9BzhUMfTRi96sJ-A1ZUe9_Wc3Syr9sTllhVq_RpMmRp90LvZeGb1OfpmeKVBbEw8FmZs5MW7I_TOwF_a3Wn4awtRW4OVwimVFhWvoS9yQXGk1stC2zbXaVgnKzIuSP3iYTsudoTQ1fUEqphFcRsFwkPsru7b8DmSZK3YgmI_eFyod2KDThIZHm2148lIT7rynmMw1xIXzUGxqZU4oslgBJbnPe7PgW4nUVVRBxwmgMkdK-Xlr0gXTB8B3ypWc-5Uk1j1ROs5g9m0RSWKofZokBKUOqhhifRID_SNG3AKORsmh7SDKYIxi4M2eHWBOP8CNBQakmfsL9GYZPpVQRDuDl_yEyACYwLm1ffXQFVW8PLjoLYGdUinfCVE8ScaXpGBUGNP2yQN2YuWxqq4PmVE73ed_XoZoYlGF6iLWQNbhRZlUve5QeKFjBh5Bn7VSE_smu3P7Gn8CKxh2saYoKLPHAD4kpPDHbPvLa46_dlGO7ovwE8VLzdcJlgrRu9KsUVY7wm1rl0uHly6j8jDFmSd5RmG5403UIoh33AfCg_8blVrMTcbHjTN9jPFwBrPayqZvhJPi8PdIARADwXOXeYNmuuwCRzyBhxEMGcEzKYrudXitkIxiLappXxWz_k2mASVh_3TeYpUuYv6XSlK47uEL3smod475BPJXvA0Yqg8qrlCsoXn0Rd4GgcY8yIHZDbEtSFRg6MdX3OHMgmRVm6Adr6rAmMCJbxL_pqxUNbsVdrpVNBrsTNroUlBrpzZxp79k9kSpe06WVRli6t9vbTiHOnx-7SSyWHG_OL-XMQjNa77npDtxWaX0m36cA6duE7C17720rEPvOHC3fn3TWB8U6LJsrxG87mkwSWUzVgbu5sSFWt1AeF5-8b-AHP45xgNXtvnKfP70uE3G27G7X8luqR97rUDi4AdR6zV3vBGy9JGM1ds5SaNg4I5O0Y_l22achl5_iDrt-BpKHHA_U1N77mbwoeSfY_l3id_2Gl4NOqykyZO4q3THSQ8PzdXQ-BYnhSlmFi9POhI30GgEvvi2omv2HukWDuwp4UXCmdQsy3c3_tsFuo3DzVkJtcfCW7lhHE8KLYo5gK7wHxek-DCN1tJHoUFRefwZarXG-rD1STmhIozb94RR5a1XAyTsY0gdOxgd9telf6W6A7SCU6x1jdMECCh7MzJfNkFYUkNKolWU70w1AReKyqQdORXPkg3QlGoNyk7fz44CS9pDJ4hi_c4uNRMRsNpzKqqqroYF18cmyHt4OBcYNH3h2TtkbhISzDnucU_9haw0wjreWotbJ70oPQUd3bxcSkpLTsvQIw01HY7c96XEoX_vWivLgIxn7GmqqUoxl-Hhldb9HN9Q2SxI3arnnS503hIvxnVNQKk-FiI_VVIUJFPTpf5SBlrZqEuT-KsqwCu9aspF4evRk4-m9aiApZcOkziqwYA_bg3-c5jUNsR7whfbFQC0GCLUdvYo2LKgKGthvONHFd6zwqgwBUxqsW--Y6ptE7Mv0PAjZqn6M-Pe1n1Ohd5SuHthuccEzw9BCSkGReMerTdxgXTNX8a5eAtUvG-Urg3pC42iJuMGbGCPN9IGXX6HIMviIaH7fcQyzJ7QIV4gcNpIcK7fyVCIF49e9JaZlBEoOGJOz9jVyWlk0fDutXcFa3UOEb5ymzUZYMCK53OXYc1rgm-Jck_DdFc3s5xNLHtHOeCxOQxW25-J-H7BJCStilPN3e6U30D_2ZmadFnoFnBzjCmQ0R4PxqEFtivREpy-kNpr-UFVli-FX8hk8igMzKcID_bEH60dn7kfYOiMYYC2CqFoMLsejKPs7P3fQWhzXQPJtSblux61JIaoY7xm8Wxx48bPzmiGRKzsMbn-dVQiTH1XaRpk3VI7CRYE-VZxQHXVwQ_Iw7TmWZtLqbWxKGWzi23cmmkEFxAyWvmbkZZDM5XGRbrGmNd52Rru2zQmlk0Uz3yHgW0b9pLygj2AbozoumRUQsjuND3NktSmiKCxzSPzZxsvwaFvDRnvR29sARdV2nBJRMA6HLmTkTXQpAX3Ph8wAE6EOG0vn17zWuHTijfEqssZQNSPxAfDdsJi3Xbj073V8PstQavqPtlVhF2rSAXhkgS2BHdWAJ5mrSLMK9A8BUKxMnudNDrThA_EYlHrgP8xKOk4Vm_SlSJQ0JxbBcQwWqfOw29XZfPpWDoltMDxpZUUGHC4O9AT_GBS3eLtjNxxUR__2sjs19rCQQAtLme4V6cDWGPfnMsNW9AaU0Pl_fj0Kk4NeMj5hl6lGvaaigRxlmJkMVrfIQSACgRRcZLcKXdARoGcKqr5QKE-fcGQ-ZbRKqAb3LK6lA7uLjg96KwlXBCurOFj-FGSS9WzFBsunG2gK-FeCWVuKmNEuWgnNOWtlhSjHF02_KvEyG4x-zRC7bgXokWag-Xn5DVCIn-CwGgMNvSQYmdxBba4DQ8WoC6AdH7p6WukJ6VQTfZd3jYigdD-wATd5VrmxbsHwlbvTHSeYECNFX_gbeyYeQkBRIsOFaewbSUnd2enabhEOOT5U3fgXruK_C67v4tVp33Oi77G_ZGBYeRHvrt8ZBaBWCSQnOLnpbx1NpuSSujRoeg-FjXrgygF_4l5agtpKguGUVwPy-1Ej6aszI_iSOkBNMMB7ZMBhmMFtcZsFhwFLPszJfOHKjdU2MyJi2lNX0NBLLnU62Udj1qsH_j1MJlM_JyaXfwaegdX5zWwQw4PbpaErA4wR0q81gGCD63D2u_WqT3Xqqri7hZdIznVZXnxeGfyEgxWB4HN3RhEgINlBnBGJgYp1ReVnwqRP2kOZapj9V_G1JszZphWVus8xsXs-FiWHRkazve9w7t5lLLGN-mgyut8SSoy5rcR6cgxDyMiTnUc3qZ-8GxZgACr5G7iFfvR6Ru36kjlFMAVsUVFNpUivMZOoKUp6yE6S-xNhSnz-wEk_MnZ_E5jOP1nG_bjDVrzRHJoFz0vp_wcFgTzhBxvoMVhfhpWMdQnmCNV22IcWlEFJnCUrOzhpBK6UVFhj3bF7JUa3qoViINuZJ1NBtwdJkSyAln2B3JuwwRbxwxocpoEejiiZiBR78x2lmu96yeJDusbZITMHTarTQYNbSY3VlAlKBiMT3NlNevxu86yHPHaOds2ln_ID-lUP27T2vW9GHZ66nr5YtwHctI4FpjfpG6lOEYjGRs0N3tjLQwhRLPRkBuTsT6kydtH_GUVf93JmkPPRzs_LXJ-4T-iLnL_ITAxdfdkvpJtLRk4xm7U9qytnZhNDWMLVFkX5ghuSTFKZc-F8l26ZeuR6HHzS-rD0sTj2GZbvvN45afOaJ7PyT7ulV81Dtvzu3-e3mLWTWwpbsuh7x-IxLxRndb1gBcA532-ikRaltDSja3zjxbktxPAIqJqd-q6x7RwxjSYOwpoLM58tWuUzHy3_kKmA4vuJcQD6MajQ4kdP8rQdLzX9oODvhsAJuEV1AL4AdMhgj375FZ_UJ3o5N4dyCZpduNk7ionsnxeojESik676yJVUgA9opcMqiTFLNTj41RCSSieoYlRDNhsnPNa5fLcgNZlgUh0mv6yXwNCRCpLxc0-jnds5zrWJXRHTN-hDZudqjy9oxWDUyfdRstRDTvL5vbH4AvVsh8DkjEie9jz1NbXrBGF6rVQrkRGFTsDOQ4vBr7bK8Rb_4uKa1_OwY7Y8SutERn9EqX8ofjnxQs9owdRSvoTj41jjpl5t5RaJM9XZlSJV3t0ldh4j-xm55G9g3vjbrRpCWC2Xqo4bnUEvxsrn2kB9Ty-4ZVu95VJZwpJsa5ELmxJvITImcYZIdj0zrmUSzA2hOZnpeh7qqLXRggsB04NRG7O6-7RvoExmblsylSrJhWGyqsd7o3hfiEUvQIlWsNtifQm0Uqm3iI0al3_n6qex99tZem10cbc-lo5Oj0zVtrBZzN0NOTa7Zz3WzkRmy-TUFFpNpYWJlHkOw4lS2Y2gSL7n7gFGWEsuEQ2hfUe_8wHJQ4GpFdHT06I5iJH8HhmRuNbvOWHX76MlsaDy8c-Ac56-kt91w61FZkRWNwgO4z9tC8EP8UPDnafRjvfiRsUJAgP2a_wv0WVZ33M2iec9loKqEsEwM09etuK9GA6xQ1qu1meAMseB5j6zDG5slXeoUouH6LvltQ9RwSfU-QuEOCbVTqQX7l37kXvDdzQhG0vHk2KbepoVnmybpL718_QBmLOcTosAv0slqDOiVJbwTs8-OUNZDX3b_e-MGQMLn0rWlwVQXbzG1EJ7bYBo_I3PLfLH39kax83eCBICSm_StbBo_CLSUfCWTgVKoxuQXhQCbNfcREEguA8qmZS8qi9aaGjM7olBcH8Zw48sz-n7nSkFbyFn7gDnGHOX68Ufs2DPRdYDUddZPxzj5j4RGYlEWFrPnS0_3o_RiHh16m8euZU8K-vuNJ14U2vwHfxKZG3P0rjCJ4I05qEXBsH4a5qWHe1v0SLNYWENPirSUaEG15SkjYea7wm7ERpZOzdpr8hC4iQtQO9tLKQ8Rb4Dv5AY_tLHzTyOjrOtpDyxWrWG-GQ-IHvLoGHO1iW2A4vW3b94dnID03K0n6Z25K5ocZe0p_fHsn6a0fuYH2oFtuzaHe0QWyLUk8qusJcENGaIqRBJYFES1KWJ70Ku4nGPbyC7ZY4-Bp_3xo14a5qWH81T04PKUAiElvqQ4MeIYyWn24r6nK1byYZV2qy4JeHc49i9YmHBo7EwV3fcGPs__TF1CleKRSJ9n6dZ5DSBaXCdnH6X4QWGfztkM4R9foAAAJU8LjSe79SV3516f8QJYM1M4LHoa9e6A20qYv7xlCA9aeZJqc6B4oOLJL2OFaE945mXLx7TJE4M01W4QUESkmkI6AK9aGHJFVmH25E6nuHb-zIu2f0CKWR60cNgpBg9ae1HtWKuX596naN6_lHN14U0ut_gAq4Xe1X_wYlXe-4IziN29ryyE8xED0uqBbhXeS0FZUCyg8jCW4q6nGB5Vo8geI5IctjoACBbXcdYMAyBcXYr2P4CqyhvNH4WXr7yUB28Za8dMZmiLntDW4PW6cE7Nn2965aOH6mSsms-OHOmQZ28e5YZz1xGYBW8NzpjQ4Hk5ZVqos9yQb-Ipz8GnaY7rBacI97ATdXvzTPH--UMFVxajGFPkioy0mXk-Gzax31dxxwz_-l9BcKKx0YpaQdzjv3FHYJfzvalClZqbFKwt8ccZ_dHVacJahIoBctYK3-OMkz_Iz0WVfRqBnTDZyHoScRP9mI2pnF4KJW3_gJUQqcEBjyYoSQIPnLQNgdKLUmlKbaY6daYOBL3_CPsUL6sMpT27jkmXvLFNHC-HJfKye6nDZmsLJ9Oxe5p3cZOjssFed5ePpn1E5RGacoTG4-VhIQ4pRpAjmj5ujj75jlcs6ZTpH1-hQ2_tQZAu-yWcfTtNrNYfCNggdZBNQZorVZ75kCkw6lVXdcup7-hN8RpRZO3fSShsC4EWG8otgQ9HwnwZfPbY9BtNDMPQ6ghiSVsgh_rb8zEVKrBsoYoIe7Sfjh2khBIDOVq83ijOzcwaaXQTbRWoK_VEnlIuljNV-k1aRZr_4TJCZlT5f55ffczwAMeuQKo65MsFJFutQMg4S-vOT0ghM3MABNublIly0rXul9WWraWYrqRz6S-yI1X7p4aC9deggwLmhg729vuxkP3IfvjbT7lMdyx0Cl52EGrflcBXrlPG7WCnvQuLvCzwwRcSk3CIAvYvqfi5h-FAxy11sdMpUZrMMvyshaIM3DqOlUx2T2-9DjhJhJrNu3x1pyKfbBnJGWLoysbJiro7KNyhUO_TTZJxAbBeHjSmyyxWMiApuS5CuiboTqH3UPCIAdnke1XoGqZ33Z26jXZc3aB1mWnPKPkUBJMFTa_j5N5JwKE9sgskX3LRUSLHoZaipOUxDvoAIQLjyy4HC9YD_WX1MnEcfywYAySkG26kVMNP3XNvF3IC1LjhpjRzAvXQVuE8cDj4oXiCD-g_w_T4KPT9iXk_9qdFJ-lFTEWUDs553dpgUtzNuMzUls3mDpyDyBTliGvQ5xdATtQqzQangAtarSLe_UrwsgQNrF5UxBwglViEhrKm_NO0gWC6geC5Kt50GjNF_8MYh7usjvnipCycScPM_TiSoTgxYvj5jFfLDr0uHVkWYRAnZAIbj5b7SxwS5SxX7S8mxzIpKBxx7MgzDF15xqiIRdqF-YisVRV5NFfc6sxqfapqQ9e_MUkMOYiEXyolDOzm-YRjZSQ39TRbfI7ZiLNoEEKKRRUGmlpfmwRxaNZSGpXh6h3lVpGcWjxzOPl-nxRauuZOtYBe4jJAqfWlSj9otxAtHA1Ma-ywWWw0QjrUd_zecuMtrc9vseik1dH8VTpeS1r02YVtAQuhgMYh4Moinmmfn4f2Z7KGlvwnaY13BU5ahT-PjaeJR3k2Fcsmc7-0FLswk2RE6x_MNbfqf3U32uuL9HaTgTooZ0nKIwMPR29R5hF0wfAuH60RfrJ9o5Ld2oHmZXNcfu7-kgOK0pWu-HgNJgqrMuqErst6LQLmJvPDAfog_ihRXxBwVu_zGjQLHAj7ix2HwXVMcY-JdmpuW3oxIFYvnRLnXrZvaowM7nx_rrsXk4xRjFzrBb67ktv5lp-n4oUT_ula6VGRzkH8rjmhPN3hVFNJUwL6lFRQHfHPeaxl3jYuDmeivTUvWRNwIr-mNYNzpfYbpZNI4djmkjZcCZFRLsVIgLRISoujyair8y55pp-bHWuxFMdzJskumpOzlrYY0Jq_HAwSYpMXVeoSpUCo43BH63b39lDuDOiDdRfQMSoYvmv9r8dw9gsaljDZzY2bXDuwfdmsWxOP9kqewuFwBBeTlZN11jnGIivfgA5A-lDDE0VjIYUDzBvBdK6piKSgyNUJxaW95P6acyVNsjTEw__7TxClv-lSwZp4tB6tURa4FeoNyHl6ThiOpQ7j32QF_bQ6ERnSLcF7fsyrgZCaoywVKA8sJ5bVWcbtjzYqH6cR54ojFVjSzyRwP8oWcAWBK6OF5KmKDqSArrQx5RU9DTkCzLrZg_CSt3tcbC6PEwdRRxrJ2rpyCtL68PYheMer2qtNzAFFWIfddogN-CcaZLH3-tkYPemrpZW8yitkjZPz8NrxqQ7pC1SRo_Z8FVKLtudzpGLW_Eq-5wSQHKIDraPEA0ENyTgmw5kkeTJzN2CABX-7I2X_rxOknQs_hMiERNjpKKVR-19EsnMnhedt0zQXrCTQ0Q-0-k1YNB4OM8U7SZ5d6qLQdsn2wNNNnWZ78ONg605hKuC9sdFhffjXPz5o39eTsSKZfkkoXfR3lVKXQKxeBpmgSr-JwH_NcCMYBTAu7hzlwJUkXYPbugWnFaf_rtWIpXqycpcoiSIOMOs1yxh-f1tMAVCvEgPxjBovsITBMEy0mIfizFC31sBX3aOqeG3cVGPk5dElHbkNfMcYiP0pVikQRGb1QoUvxQjPGDW8bAzAUi3V_a_4TwHgzgMRwRt4LUKnFBswqnQjoyXBgPt76EVgG-PpHZ909sflgJIULYS_68TPbutXDRlUKL9jTpq3TsD-xXcBvdUtfUOGV83TICxfuw2ZGhYEQyuThGgnTAbtNthJ63qMoDe5lkpbLyCdnqHEfm_0VQ9iRdzX_eV5vh8OE3MrT10nVUahsKXR_Hp6l0vcxpMVFzZKpltW9hR5KGtRYy_FfOU7m-zBuMyyg6PmTQwWMlYn3JNuTsFbnApTaT9qsQge1IcMMLbKTz_EhPiYqdXQvr9HTKkIVq2SLsMH_3y0
\ No newline at end of file
+xLrjRzqsblwkNo5uFhGDRWVJThl06hEBSHmd3JPn3DlfO5eK1YtHzxAHo3iavTJjzhylIEg5f2Y9b7ITfEKd-_Bj73qy90_FuI7_mJYWl2gB94q_Iy4zGNcwf-IsW2LBtoE-Im7U5Z1FCqhUi_GEGfwaANk7y1vGKAO5mf0_x65AADyH8oih06FfRqcQfkbJGdD8Jqb1QFgNRxxvpvF_-fkMVhr3sJiC6Kk_JjBqTydAgQWDMPBMxPna7DqZ_d09CxH747FsMv8-IL6U7gS8SxY5D3rE_xw565B0OVu_IRgb0FENPe7Bi-TdbsVdfsTNwT4BaS-E_eho92Ly1-aPveW_ZDHuqvP2iAxu90KjPtj2n-YT60NR-Xc5Wk4JdXwd79MGSL3k5VRYP_ehmJ0zJZ5v__aNedorp_tB_vuaDdw2-Cb-JyazfDCWBl--Ya4wKkijW_Hni4XpMC2fqacA2HTrEFo59odZW5SiFKuXhieXD4z9KO1RGW4dVov41_1uZWhu63ZSeGAEr99iXE3W-_r3t_s2WE8vWaL-2TcUO3O31D64Dn2YSFk1MmAAQrZk2y2XPEA2ad8_El-vQnm6ifJMcDH3wyUVnb2yX8nKD7jK06bDSwHsozPX85ADowvefXm5a_XF_hmPWUh6gFvRac9GAjnbUY8ftL6HM3VftzhV_-_BByUwQRWdftkGKBH561GJFMVqrTAPOnj0Qzq-pWh2u5N5zX3dfmIBeT0kDdhU9GICu6RXII7Edm6vBWaYIOgO_39OF9pbY4FnARU45128AdUWOBBC9UTW3zf6X41qAvlikJQlNUZk0SrsW7x-zRVVVg79J5DlOD2-kVqPIePE4GSS4Hm-SjiclBkphT7rLKrzf8dN12DEACBRAmvm3cYkhISuHtZhLVcvAglD7lS8llUgUYcBVj0LkkikKj4vwzGgdILfZpHGC_TH03LFfra3fezDosavdBz8hm7B9xXSzv7hhTdwQiqByOU32qhkKGxpWMLxgbN7sjslCj7LrsIFiW_JproG7gclMFiW0y1Vj_41el_fIxKSCelgOVVWWBuH9X-3dwzcgqNj5vugfh-357QgqA1Mmwgz0EfQ6AwQfkHuQsbgmsv2yva_2uPtw2SuvmrFTMUdh_xtjCr-os2Uf23FaT3VG75EkB3-PKeMX5K6kogqywLxRlIV4YTcmkdUBG3ZRnM6Jn_yh3CXTXmAj-DwxQKiy4YZ-4agZ8T8ZiIEdQyJJS9hCsqznipnPtLpE-g0CajExg50aLN2Qf5V6UQvHbgwcKE0GEWbARfndiC2tKFwy1ho7SbxhEKLbJi8iTE26If1yVatcw--S8bhPydeRyzA8JIooC-Cu5iX7dXRWQsDXQXSm2t8bCYOl3XbNgEAg_8MqZTt5u1obA6zC6Tt7PAaUx250Y5Oio5IZ98s9tjAjXIKeizSsRIPVFiP_wuWur88h9bMk1SCQ5XrMo8cPCVgF9d1EJIHv81k8RyWUz7XGi0TvAMAuCpWm25_8kSUKLw1ubaj32ufRUTxtRmhH2byfjTRy9xr3QjoEmsGqSs0PE-mkf3EbXIKIJHAi6VNP6INPpNOy7yF2fG_7qNi-xh8t5cljeYOClj4Z8Czs08Et-3Na6wXOLtXj887yZre89TbXL4cHVuLsVhkFZEhtkZcsYpw4UqtUwnvV3bj5fpH9RBn7HxAaoBAvO23RjuF_tO_Wypae5faEZ4iXK_IlLZi6MTlyDjzGO3JsdZO0N5s3p5Ny-85AWW_1qf_E8MOGohxoKD8eDNAIDVR15ieiK5vf0MJnKTntfcydRidjr1DJp3iQcgmR_mcAdr5rBGMiJXxV_ZfsylBi_Fhc-kNhy-khazUNxZ67tcFJSJTwdG0D0woVOLlJpBxmrh4lATppob7JDbNw5TeViqx-E3e_iOa8NWOa13JwHqyXGdlIQXBFBEKWD65Qq1V277gV6g74Wg4tpq5sRjMlWoqWvtE8L1zECSomIVDXl1Ij-h872jbiHCwrEqS0k4XxJPkaFHrsGGhTCNsilaiTKHFkPwTpcQHU8LAL02A-CCBIggzdySvNFujCXDLhT87CiN1NxAYoc7-ykwSy9EyHDZHoR-FW3KDr5seNtwU5hukBcfo_mwmbrZk8C60UxndmRB0aelWwGxWhSTy4BAUh7tuOFxQOzlBC7n_vRQOao8Vo6cDGgg98MrHPaZpHDyQO-3kMZWyHtHJrNDf2fzgQIu6XAbbxIMBscR9T2Hudmi1LMrsLFNknESR3eeTmXqPic-COOqni9RrEbMQ-vouTZwz1eO3eqjiWZxUgDbz5AwgDgH25_-vU7mIJXmdC5CBkxwQJrHkL_LUELVNJ7V98TmZRJb4SEijQBP4Ey1s-g2lEJqt3ITwyckIepkqMYTAVrFj3vZewS6Tkv-wD5_ObP7g0b28UOWQ4N9o_sFIb6jElaT0TpTvRE_wn_kUKr9UbO1ojeEGNtDmKG2iRtd6J_THxlQpBptr9v4_btwdLmY_MpS8mxyhjnqRmIjjcE5HydS9zmN8O5dxCnTRPvsDhcIhFQGLrkRRi_kjBvsIXY5Wg4hFNmI9bAg5yFR5wf8uN_6ctIVguzu4kezk-3ftk0sP8OBDI5ncQmOOLw1uNicJjQy9vhjUarcEN1DqBSQkh6vg7PuIf9H2jpbKvbtjOOP0WHW_Am4gfWev6q9OEeMbEZ79aLxOElDKE-cdMEeLSpAb--T7r0XnYE2KvCxwZWL4asDIxJ-8BNZAJ6COPlv0rc1f1NFF7evjJD2K68RfWJPiVeOhV_8pLa-XUrrKjmNAZAE6Eq1X_aya9oqpMl2bT0uz0npuEdwKE-dvk2V-PLie67J3mvCUfv-zV7PsVlto-SkpPvyT7n_9NPp5zOsgS-IFaZn8WEz8vnCpDaqKPeJcfwHIPv7g8YpRmHBq1LlxM0-pfVy91eNq9ChXfm38UonSvITSx07rSrdfSVgtsd2KGSQ62_XtqjmEulldetraeR_ckClX7S98DrT9Pot4uBQ00mSCxlZ-2d8kmcxqEjOG5XAKLpDCS49iNWFshgsu1xmFnMk02aZDKoctmgNAtQV0j9NRFXOsr-tThYrGhFnods6VRtkI_ank7KSAduXZTiB6jbXPev1L1MzF5h8g4zAipOWwOfX33706VM3Z56staBR7QDjIHZILsMOxEW67Mq8VDCXd7DkHbXlTzEg_Bbmf6Ewgmeb46mCchpUugieIK0IzbsfZn-jQgxMT-T5VZxGwHrQCcgC1Eq2jQQMfTHIqm4WxmsbGwRU-xXtFG4l062MaV4Et0ADVxLEztsx-mrlVWYLH1skkLE10hnbZOKEOSLfbupMed06QywVJ6hX4w5dGRhqNhEYj5-g1aAEIKZVSY4fRxwWLMGYdMtcX2XfYA-eTTQjWI4ocSbGrhJNrZy8ErCYMQlzBiunODj-FGSO9WzFRsunG2gKyFeCWVuKmN6uWgmNOWtfhSjJZZXTAydU8YTzUkk1oKGuNGSNVmuWaFk9SV6Vmr9ByE4JOJzXoo24j4GQ6ZzGOZnXG4RbntqcQmrsxel3fdRkYNjoNjKDPjiU_oqifESJ0cBbn_rGaUHK9N5lex4doqKIklOBXqPGILbCCkY524zIwyYUc33-YRFxXfiM3peTn8bpKDuywRyHjIDo6E7QiAyvozexvy7ESsbxKd-DjXrgygV_4h5cgt3KguGTVwPy-1Cj6KswI_yUOkBNUM77WMBBmMEFcZwFhwD-pjghJmofgEi4zunS2lNX2NBLNHN11VRoWwR8_sex9FhTfMQGqSIKLpuX-G5FTmymvo7Qb2LEWQq1reA4Z1aXS_uOEXmmQ--1rPxhUPlpmSrs8qM5LSm7YmhbjVkgIKVBnRGJgwpTRfln6qRP2kV3apf9VFO2fxUnvrmDyRA7zuSUZBCQMdbCU-Cv3dgtgFWN-mgyut8U4vU2wpbZNrKK-hN2S7XZzK1p2NQ4u4nLb64wlZDq5Zt6rdRTCwRVpgv_NSuvgPAJSY-d5kV9fr-C-_j7LURSs_d6EiCu8KFnOeh-khT1F8_q3n7_rCVMBzlABqzAZk-PSSBf6OEY2Trj2Hy6XmyUqiEnvbw7EZZwxGvJnqtf2z2dx4b-8w09vUdMwz3dXLsAJuST7_MrUyk_9ioZgpR8exCsnAEpBy618tb0CdZQNT0DPb-ILTc8U5-BDkqhzGfpPa5Sz-falmYPnbf7H1VPAv7_4R-PSg02T2qWPmHWMIvLrkptZ5cbetfOJ-eMzG97w0BiWsBjQIzsskknqCRoxK_FzfFa3EgEVrCJZ4cOvlbqPUmhlq2-EkwBlNCdDy7J7FTLzuOhWEqHqytnbhJDWMLVFkX5ghuST3IHpvFmB0XeQF3RorDk7cjh60Wtfw9N75KHMceZe-EpuMFm6cRWxznxG1uItEW1RpxaJZztFTwbkvpsZrDZ430KjhBkvBTtNB9CzREzPjk-JaTm-9Vb3Uno_kRRBcUYCIwme6i73twBW7-vJ0eJdkEPeqTQIseIwTaXzKolj9UJ0lDUmIV4pu9MeX4wnT6iPufuUxoSVGgua_XWUS_2zmjcNEc9TZAsvoYwOSRnLzweedAkPhInqtw_qeWFeZZba6KLxvwXUMREyWbAjr2uTzOsi38aDP3qgO-fch_21vRXFi8urGNZR-PN-x9XuNmiyvoxWDQ-fdNttRFVngBpA28No_bMGxLQTvOHPw2jBZoMW-Dg-LhVMWMviROq9pdgFAWJtB-Bmf81-eQY7SCVut2Rn92qXO-gjXzOtfsxdRI9v6yzW6yVRVTYDADeuuptkPfXxuPnroUyze0X8TT2CNVTM0mATGeTS58VNlhVD9OGhwpKVtlCdIDJ1qM_k8QKeXskFaQeZCL6bYT8-r0MVzw2eODzpe9FsaLXPgI-A0I7ie0k1lXs-SZky9xzlBtDK-u4FDDjnyWwwT3dksahuTbzxAMi07jC0x4W9Bm_yHjAEoITuwC0K9hPlhyXMRGFNjzIuxLm5s7PEu_GuFRcyFFdNZpyrwue4xqQMEXBt0eXFEAduZb1fGNRS7D1yfEiz8gTHQ4SmF7TU06Mui3LEXByQu7jvOGLY76MiEKD-8tIAcrAykN93wcbCpEVYNgYR4j1tie1O8-LDnabQjeTlRcINAwD2alwx0WNY3pU0iOgAioKtEM2xMm9ethWBGQ6eQHcS0eH5M-e85jFo6eUw_XdIUouJ6bbjtgDPQijT-guDOafTTNkZ7l76k1zEdjMxGWzGkYqaeJsRn0ydpr718VNpmbSbTcwBvGciqTSiVZXvTM8_OkPXDHFg_8rMGgQLnGrWlQJRXrvI1-J4aYFA_YB9L9KI3Pcadu3eCBIKS0pVtr3o_iRSUP0YTwJMsxmRXhQEddXb7kAe_Q8qmZGAqy1XaWXL7ohBcXCXwaCqzkv7nCkDbyFo7w5oGnKX6eMeEo5O7tcEU7dYPRzl5z4QGIkkW_vOnSaz32_RiXl16W4huZIAKnnxNZ15U2nuHfxMZW3P05bFJqI0542ZBsL7a5mWHO1w0CLTYGEMPyzUUK2I1tnORdsOUB0UuFo6WstFLYyAIn3RJnlyPXLAT8DoA4E1-iTDLRxJRwbkdBznEhScy0bwbJoAb0ko2905K5p07A2CF2aU0Me0Yjw0AOJYDdK4dF6dj2DE0pX5Z5mOkH_FZW0r08s_SXrnid4QknGYecMZ5TSv3v0YE0vm8YWsA8CF7aTyMdo6tqUE8Bb0YW2v08gez50PVZqt8jGY58Ld49g8YeNAu5Ey4PuEdWZD83GH5WkMaETm_tJ8X3nx-wU3P_8jt8YJYTF0AwuH9YTCZYT28r8ZIBdViuoGJKCMHMmIhoXLFYWv7gQ8I0qX5UUg8ApY93K9L4He427tUuSH9HEbeSSK8qukdA8mUO0K5xf0gEAucy8f0ZG8qC0xTn4cDqmH8WkY6FyH2564nuPd-DMx290EK0J60cRgpRg8ae5IF0KwXL14naR6_VPM14U0utphAq8ZeHbyw2lYe-4JvSR29Lu_EupCDauqBbZYfi4DZECzguXCWqm4nGB5VY4heY9HcNfpAyBaXcdYMQu8cnks294DqSZxNX4XXL3_UR28Z40cQZykL1pFWKLW6c26NnEB65aOHMmSs0o_OHOnQZ2Ae5YWz1_GYhW8NDplQ4Li53Rsos1_QboIpz8JnaY6rBiaIPB8VNfwzDLH-kMNF_xbjW3Pkys-00Xl-0vbxp1axB-__kdBBsKHBGHOyDN-siXdeXCLVURRc7rxIdgSRaNIHltfSf9avAqiYvfub0_c5hdVqlG87wMz2yNJO_4Sd9csIS4WiSJn54u0_Qatcj9ZYxOeid6acTLMvwer5NiprDP8Xfv8cCrG_p6T7bTjaSrGHbltaFofQwBdSATg7f2tPiI6SYRh7P2lOKcRvkqnP4xjZ6SmfofQdCrZgF7pzRZGYJU5Lg5eFDle8jlahKRDdD77FhHNUZKUt6Daaz8kTpLQQewU-avPQZMQshYO8jmLNOtwy4YtYG-Dwn3QxK70z3XbUvWXK236srJHgVM8qT9CDfBUwnepDGrLzxX-rTVEsSZqvvIKlJgBEEXTYYsqAojjOsW_Wi8oahqRgQH6PoMkJDHziZ4zhc_rjpASnAtdBuAwsV6-Y5GgpVHDBqLD9utAiABkaSd_9bMDSCeTYKvW5Qb6iCMlf7U5Vm09ZnSJn1v9bDgeF-E55p7Zy3c9oKHFPJKNpZrK-C9p9pTI-dAphDsUzgSJi5IyK4f3agz8kAKzbCQ0Jxdh1VcptdekRzpfYHN4NEdDXjTnvNVW9EswMRsUgwsiZIkH9KCtHg-xCTtBHXlfwVQUAr2VeEUY5DgyaqC5SfErgKckGwX_bBn7RZjQV1KkTALhc7YdS2sXsT1WPkF9SdD4q_8c9LJOt40nP8QGYXrW36qnp1qD1JCpf4LjUhhKlBPxOgyQDuK-j56xrnMpPY-lE4ivfwNvSFUcKx6ej7q-E0C6az4V8MY_XEcfizgLSnUW4DUsico7YdmU6iQ6hLLdw_4Lt5h_W8XOsqJA6mmtwh_hzqIrBfFaC7vFafwTrf_eq3rkGeiSoEfRVrUXRrw_OD0timResxTO1-sBNFKxFThAQXHgAtbrDbfxkvwswILrj1URhshhliDg5Tn-Em7L3WCrm89RSK12rS_yXMfP_MnkETcOsIPoPb4zRmvdxTt5pIPQ_QexABn5Ez14wLX6qjAsMTVcV3uhdCCxX67UYMUXRFSxr5ffu8jUJXAkVG_QApPziyNSziq-tEXTcUZUDFzchLkABJySCdDh6k7jcsrlD1eiiIul3PcFhPh7AQTekuKKvayVDjsFnECUmbdBW7t_ex4GzEuFs_GpsvgFms9xYw2BKQj8exx8HSjzozuIXbf9lkiQT01Ko_Rwz4VxBBot5lFJNdGn84CgSwF3TG0kdDIdgAuaegr4iB4HCoGGAp4nDa3yHiPAZ0prn1ZfbTtYTQTFki5hwedx3hTLH6BRDUWZ4ulf1-ZtfOjguho1REsjJjVqPd03j28depEdcvUHYNCfDvDTPFDYLbYT4nSBREFaAXcvUcq11ywvoymQc3_Mv680Xn6wAqzRL3OwEMifsid0ZLnhBUMZQrbRS5Kkt9f4apee0rsrLNiZg__tE4nbtTUqkgiF6wb-Fh_vFFdnW0z8ipicS6vHRMn5LlcaE-WGVZX2jnFZEctJ_jVPYns-Z1MT8iHEdFICBE9dqAZQuoDPyo2PGQrpP4mV91jnQMYdT6QnEdrqOkC-HR6KNcSZrxvV6ihrJz8lr5bgUIHjOmzE--DSOcR_waGwqKfwbhT5Hhjc9hZe6U_Kp66d9wrnRWrRuQPdD4mqeEUV7r1gGDrG_m01cqTbm9-a4NBwa-RHQdO-UkjgcpEhFRU_faxKJx0bzOKdioSfnVJMiUIp6MsRFkIkPNrIdzLkXs_TO0DkQ4lEPQXfbVJNct0AsdrNMjFxBdL6cuq-Kec_ctH7IgWQIRfrVAr3_xJ-V_oe_tAwpdFWGyGT1x-NS-13U1czOMotYz4SEkjXqkYVyvX3owKrTdJwMgCAi5aMd4E9EarcvGIttLtnqpPAswJWrjh0y7eqrc53eiL4tGupUwfWehevKxZE8QvuXguvWwqVgLkTvk3HEYSFpj99tN_hXNHmySzK04SmLKFKQXAQhEbnuO4YPuVSI_naqaQAePsVg1LZZG0GqNnpzAzDNzY-lUdGQPYRZMLqPC11nNVYy7S167-xpqN8ft0tqbh92INWKbuR4dtDDRHrFzE8Wil7819AttNcJL7hdtcRqxPzygfhsyW9RzlbpgyRxtlpGriVwmMy0kg3ct55Oc0V7ah5N6rKq_fY4zfUTwUEC1nIg68iM9iQtA2zjcww5NPMMuv9isjsyOpfkicYfN1lPIJDzGKBuuZkwsRwiIopIBHL6jFZzstxZQcXIPc8myP7wUyQJSAvWsQpHZQQ63C_Oo2OsRzfXtRAVEBgT8kMDpKxx8RreYjM8QC7Lno8XDiPqRW2c7U0u2pYNfssBabJHMCX0P3Bkcr9WzOnUzjMiw1Xmsk_QjC3zlP_8huZLNDetqpl9QxAZs7frOssQ5r2NaBjZF6SAm-PJm1fW4pKrbAfFAtU_A0VPbqoXjNiUaT9jTtr5DoDcTrJ5iryjwRcO7o0t4Z9wLZPKIPSiQnpPwj2R5tgewdUROp6JB9sWoz7-TLmM9qewRG1kCFjveuNvXz9DvV4G4ZPqad4jAMlf2DjzI2LUGFCFeW_VxAfZVl1NMopepgsvv-VsnuUThylXR-ne9hErRcCQkx7rjNWFuoL4RCsHqhLPQky5gHOfcLHtNmxzzjK8usbTMuhwjkIv1yGvrLP9FyF
\ No newline at end of file
diff --git a/docs/logical_data_model.puml b/docs/logical_data_model.puml
index c87f25cbee..850cd3df81 100644
--- a/docs/logical_data_model.puml
+++ b/docs/logical_data_model.puml
@@ -90,6 +90,8 @@ class ActivityReportObjectiveCitations{
* id : integer :
* activityReportObjectiveId : integer : REFERENCES "ActivityReportObjectives".id
* citationId : integer : REFERENCES "MonitoringStandards".id
+ * findingId : integer : REFERENCES "MonitoringFindings".id
+ * reviewId : integer : REFERENCES "MonitoringReviews".id
* createdAt : timestamp with time zone : now()
* updatedAt : timestamp with time zone : now()
}
@@ -2574,6 +2576,7 @@ MonitoringFindingLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringFindi
MonitoringFindingStatusLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringFindingGrants : statusLink, monitoringFindingGrants
MonitoringFindingStatusLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringFindingStatuses : monitoringFindingStatuses, statusLink
MonitoringFindingStatusLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringFindings : monitoringFindings, statusLink
+MonitoringFindings "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportObjectiveCitations : finding, activityReportObjectiveCitationFinding
MonitoringGranteeLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringFindingGrants : granteeLink, monitoringFindingGrants
MonitoringGranteeLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringReviewGrantees : monitoringReviewGrantees, monitoringGranteeLink
MonitoringReviewLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringClassSummaries : monitoringReviewLink, monitoringClassSummaries
@@ -2582,6 +2585,7 @@ MonitoringReviewLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringReview
MonitoringReviewLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringReviews : monitoringReviews, monitoringReviewLink
MonitoringReviewStatusLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringReviewStatuses : monitoringReviewStatuses, statusLink
MonitoringReviewStatusLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringReviews : monitoringReviews, statusLink
+MonitoringReviews "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportObjectiveCitations : review, activityReportObjectiveCitationReview
MonitoringStandardLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringFindingStandards : statusLink, monitoringFindingStandards
MonitoringStandardLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringStandards : monitoringStandardes, statusLink
MonitoringStandards "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportObjectiveCitations : citation, activityReportObjectiveCitations
diff --git a/src/migrations/20241118093025-add-activity-report-objective-citations.js b/src/migrations/20241118093025-add-activity-report-objective-citations.js
index 196e8866ab..4cfda2d629 100644
--- a/src/migrations/20241118093025-add-activity-report-objective-citations.js
+++ b/src/migrations/20241118093025-add-activity-report-objective-citations.js
@@ -4,7 +4,7 @@ module.exports = {
up: async (queryInterface, Sequelize) => queryInterface.sequelize.transaction(
async (transaction) => {
await prepMigration(queryInterface, transaction, __filename);
- // Create GrantReplacementTypes table
+ // Create ActivityReportObjectiveCitations table
await queryInterface.createTable('ActivityReportObjectiveCitations', {
id: {
type: Sequelize.INTEGER,
@@ -20,6 +20,24 @@ module.exports = {
},
},
},
+ reviewId: {
+ type: Sequelize.INTEGER,
+ allowNull: false,
+ references: {
+ model: {
+ tableName: 'MonitoringReviews',
+ },
+ },
+ },
+ findingId: {
+ type: Sequelize.INTEGER,
+ allowNull: false,
+ references: {
+ model: {
+ tableName: 'MonitoringFindings',
+ },
+ },
+ },
citationId: {
type: Sequelize.INTEGER,
allowNull: false,
diff --git a/src/models/activityReportObjectiveCitation.js b/src/models/activityReportObjectiveCitation.js
index 911af97d7d..44264e8901 100644
--- a/src/models/activityReportObjectiveCitation.js
+++ b/src/models/activityReportObjectiveCitation.js
@@ -8,12 +8,49 @@ const { Model } = require('sequelize');
export default (sequelize, DataTypes) => {
class ActivityReportObjectiveCitation extends Model {
static associate(models) {
+ // ARO.
ActivityReportObjectiveCitation.belongsTo(models.ActivityReportObjective, {
foreignKey: 'activityReportObjectiveId',
onDelete: 'cascade',
as: 'activityReportObjective',
});
- ActivityReportObjectiveCitation.belongsTo(models.MonitoringStandard, { foreignKey: 'citationId', onDelete: 'cascade', as: 'citation' });
+
+ // Note: We join these to the 'link' tables,
+ // that inturn join to the monitoring tables via a GUID.
+ // Review.
+ ActivityReportObjectiveCitation.belongsTo(models.MonitoringReviewLink, { foreignKey: 'reviewId', as: 'review' });
+
+ /*
+ ActivityReportObjectiveCitation.belongsToMany(models.MonitoringReview, {
+ through: models.MonitoringReviewLink,
+ foreignKey: 'reviewId',
+ otherKey: 'reviewId',
+ as: 'reviews',
+ });
+ */
+
+ // Finding.
+ ActivityReportObjectiveCitation.belongsTo(models.MonitoringFindingLink, { foreignKey: 'findingId', as: 'finding' });
+
+ /*
+ ActivityReportObjectiveCitation.belongsToMany(models.MonitoringFinding, {
+ through: models.MonitoringFindingLink,
+ foreignKey: 'findingId',
+ otherKey: 'findingId',
+ as: 'findings',
+ });
+ */
+
+ // Citation (standard).
+ ActivityReportObjectiveCitation.belongsTo(models.MonitoringStandardLink, { foreignKey: 'citationId', as: 'citation' });
+ /*
+ ActivityReportObjectiveCitation.belongsToMany(models.MonitoringStandard, {
+ through: models.MonitoringStandardLink,
+ foreignKey: 'standardId',
+ otherKey: 'standardId',
+ as: 'citations',
+ });
+ */
}
}
ActivityReportObjectiveCitation.init({
@@ -24,11 +61,19 @@ export default (sequelize, DataTypes) => {
type: DataTypes.INTEGER,
},
activityReportObjectiveId: {
- type: DataTypes.STRING,
+ type: DataTypes.INTEGER,
+ allowNull: false,
+ },
+ reviewId: {
+ type: DataTypes.INTEGER,
+ allowNull: false,
+ },
+ findingId: {
+ type: DataTypes.INTEGER,
allowNull: false,
},
citationId: {
- type: DataTypes.STRING,
+ type: DataTypes.INTEGER,
allowNull: false,
},
}, {
diff --git a/src/models/monitoringFinding.js b/src/models/monitoringFinding.js
index 3f2d2d0639..c71b2b0be8 100644
--- a/src/models/monitoringFinding.js
+++ b/src/models/monitoringFinding.js
@@ -12,7 +12,6 @@ export default (sequelize, DataTypes) => {
* monitoringFindingLink: MonitoringFindingLink.statusId >- statusId
* status: statusId -< MonitoringFindingLink.statusId
*/
-
models.MonitoringFindingLink.hasMany(
models.MonitoringFinding,
{
diff --git a/src/models/monitoringFindingLink.js b/src/models/monitoringFindingLink.js
index 467a74b79c..23185008a7 100644
--- a/src/models/monitoringFindingLink.js
+++ b/src/models/monitoringFindingLink.js
@@ -24,6 +24,7 @@ export default (sequelize, DataTypes) => {
* monitoringClassSummaries: MonitoringClassSummary.findingId >- findingId
* monitoringFindingLink: findingId -< MonitoringClassSummary.findingId
*/
+ MonitoringFindingLink.hasMany(models.ActivityReportObjectiveCitation, { foreignKey: 'findingId', as: 'activityReportObjectiveCitationFinding' });
}
}
MonitoringFindingLink.init({
diff --git a/src/models/monitoringReviewLinks.js b/src/models/monitoringReviewLinks.js
index 458ad116d3..83648c87c8 100644
--- a/src/models/monitoringReviewLinks.js
+++ b/src/models/monitoringReviewLinks.js
@@ -24,6 +24,7 @@ export default (sequelize, DataTypes) => {
* monitoringClassSummaries: MonitoringClassSummary.reviewId >- reviewId
* monitoringReviewLink: reviewId -< MonitoringClassSummary.reviewId
*/
+ MonitoringReviewLink.hasMany(models.ActivityReportObjectiveCitation, { foreignKey: 'reviewId', as: 'activityReportObjectiveCitationReview' });
}
}
MonitoringReviewLink.init({
diff --git a/src/models/monitoringStandard.js b/src/models/monitoringStandard.js
index ae9d2523ec..231b743433 100644
--- a/src/models/monitoringStandard.js
+++ b/src/models/monitoringStandard.js
@@ -12,15 +12,6 @@ export default (sequelize, DataTypes) => {
* monitoringStandardLink: MonitoringStandardLink.standardId >- standardId
* status: standardId -< MonitoringStandardLink.standardId
*/
-
- MonitoringStandard.hasMany(models.ActivityReportObjectiveCitation, { foreignKey: 'citationId', as: 'activityReportObjectiveCitations' });
- MonitoringStandard.belongsToMany(models.ActivityReportObjective, {
- through: models.ActivityReportObjectiveCitation,
- foreignKey: 'citationId',
- otherKey: 'activityReportObjectiveId',
- as: 'activityReportObjectives',
- });
-
models.MonitoringStandardLink.hasMany(
models.MonitoringStandard,
{
diff --git a/src/models/monitoringStandardLink.js b/src/models/monitoringStandardLink.js
index 927b445e97..39bd6b10c3 100644
--- a/src/models/monitoringStandardLink.js
+++ b/src/models/monitoringStandardLink.js
@@ -10,6 +10,13 @@ import { Model } from 'sequelize';
export default (sequelize, DataTypes) => {
class MonitoringStandardLink extends Model {
static associate(models) {
+ MonitoringStandardLink.hasMany(models.ActivityReportObjectiveCitation, { foreignKey: 'citationId', as: 'activityReportObjectiveCitations' });
+ MonitoringStandardLink.belongsToMany(models.ActivityReportObjective, {
+ through: models.ActivityReportObjectiveCitation,
+ foreignKey: 'citationId',
+ otherKey: 'activityReportObjectiveId',
+ as: 'activityReportObjectives',
+ });
}
}
MonitoringStandardLink.init({
diff --git a/src/models/tests/activityReportObjectiveCitation.test.js b/src/models/tests/activityReportObjectiveCitation.test.js
new file mode 100644
index 0000000000..d23836939b
--- /dev/null
+++ b/src/models/tests/activityReportObjectiveCitation.test.js
@@ -0,0 +1,361 @@
+/* eslint-disable max-len */
+/* eslint-disable prefer-destructuring */
+import { REPORT_STATUSES } from '@ttahub/common';
+import { faker } from '@faker-js/faker';
+import { v4 as uuidv4 } from 'uuid';
+import db, {
+ User,
+ Recipient,
+ Grant,
+ Goal,
+ Objective,
+ ActivityReport,
+ ActivityReportGoal,
+ ActivityReportObjective,
+ ActivityReportObjectiveCitation,
+ MonitoringReviewGrantee,
+ MonitoringReview,
+ MonitoringReviewStatus,
+ MonitoringFindingHistory,
+ MonitoringFinding,
+ MonitoringFindingGrant,
+ MonitoringStandard,
+} from '..';
+import { captureSnapshot, rollbackToSnapshot } from '../../lib/programmaticTransaction';
+
+const mockUser = {
+ name: 'Tim Test',
+ role: ['TTAC'],
+ phoneNumber: '555-555-554',
+ hsesUserId: '65536',
+ hsesUsername: 'test50@test50.com',
+ hsesAuthorities: ['ROLE_FEDERAL'],
+ email: 'timtest50@test50.com',
+ homeRegionId: 1,
+ lastLogin: new Date('2021-02-09T15:13:00.000Z'),
+ permissions: [
+ {
+ regionId: 1,
+ scopeId: 1,
+ },
+ {
+ regionId: 2,
+ scopeId: 1,
+ },
+ ],
+ flags: [],
+};
+
+const mockGrant = {
+ regionId: 1,
+ status: 'Active',
+ startDate: new Date('2023-02-09T15:13:00.000Z'),
+ endDate: new Date('2023-02-09T15:13:00.000Z'),
+ cdi: false,
+ grantSpecialistName: null,
+ grantSpecialistEmail: null,
+ stateCode: 'NY',
+ annualFundingMonth: 'October',
+};
+
+const sampleReport = {
+ submissionStatus: REPORT_STATUSES.DRAFT,
+ calculatedStatus: REPORT_STATUSES.DRAFT,
+ oldApprovingManagerId: 1,
+ numberOfParticipants: 1,
+ deliveryMethod: 'method',
+ activityRecipientType: 'test',
+ creatorRole: 'COR',
+ topics: ['topic'],
+ participants: ['test'],
+ duration: 0,
+ endDate: '2020-01-01T12:00:00Z',
+ startDate: '2020-01-01T12:00:00Z',
+ requester: 'requester',
+ programTypes: ['type'],
+ reason: ['reason'],
+ ttaType: ['type'],
+ regionId: 2,
+ targetPopulations: ['target pop'],
+ author: {
+ fullName: 'Kiwi, GS',
+ name: 'Kiwi',
+ role: 'Grants Specialist',
+ homeRegionId: 1,
+ },
+ version: 2,
+};
+
+describe('activityReportObjectiveCitation', () => {
+ let snapShot;
+ let user;
+
+ let report;
+ let recipient;
+ let grant;
+ let goal;
+ let objective;
+ let activityReportObjective;
+
+ // Create Citations.
+ let citation1;
+ let citation2;
+ let citation3;
+
+ // To assign to citations.
+ const monitoringReviewId = uuidv4();
+ const monitoringFindingId = uuidv4();
+
+ beforeAll(async () => {
+ // Create a snapshot of the database.
+ snapShot = await captureSnapshot();
+
+ // Create mock user.
+ user = await User.create({ ...mockUser });
+
+ // Create recipient.
+ recipient = await Recipient.create({
+ id: 534935,
+ uei: 'NNA5N2KGHGM2',
+ name: 'IPD Recipient',
+ recipientType: 'IPD Recipient',
+ });
+
+ // Create grant.
+ const grantNumberToUse = faker.datatype.string(6);
+ grant = await Grant.create({
+ ...mockGrant,
+ id: 472968,
+ number: grantNumberToUse,
+ recipientId: recipient.id,
+ programSpecialistName: user.name,
+ programSpecialistEmail: user.email,
+ });
+
+ // Create goal.
+ goal = await Goal.create(
+ {
+ name: 'ipd citation goal 1',
+ grantId: grant.id,
+ },
+ );
+
+ // Create objective.
+ objective = await Objective.create(
+ {
+ title: 'IPD citation objective ',
+ goalId: goal.id,
+ status: 'Not Started',
+ },
+ );
+
+ // Create activity report.
+ report = await ActivityReport.create(sampleReport);
+
+ // Create activity report goal.
+ const activityReportGoal = await ActivityReportGoal.create({
+ activityReportId: report.id,
+ goalId: goal.id,
+ isActivelyEdited: false,
+ });
+
+ // Create activity report objective.
+ activityReportObjective = await ActivityReportObjective.create({
+ objectiveId: objective.id,
+ activityReportId: report.id,
+ ttaProvided: 'ipd aro Goal',
+ status: objective.status,
+ });
+
+ // Create monitoring data.
+ const monitoringGranteeId = uuidv4();
+
+ await MonitoringReviewGrantee.create({
+ id: faker.datatype.number({ min: 9999 }),
+ grantNumber: grantNumberToUse,
+ reviewId: monitoringReviewId,
+ granteeId: monitoringGranteeId,
+ createTime: new Date(),
+ updateTime: new Date(),
+ updateBy: 'Support Team',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ }, { individualHooks: true });
+
+ const monitoringStatusId = 1;
+ const monitoringContentId = uuidv4();
+
+ await MonitoringReview.create({
+ reviewId: monitoringReviewId,
+ contentId: monitoringContentId,
+ statusId: monitoringStatusId,
+ name: faker.random.words(3),
+ startDate: new Date(),
+ endDate: new Date(),
+ reviewType: 'AIAN-DEF',
+ reportDeliveryDate: new Date(),
+ reportAttachmentId: faker.datatype.uuid(),
+ outcome: faker.random.words(5),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ }, { individualHooks: true });
+
+ await MonitoringReviewStatus.create({
+ statusId: monitoringStatusId,
+ name: 'Complete',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ }, { individualHooks: true });
+
+ await MonitoringFindingHistory.create({
+ reviewId: monitoringReviewId,
+ findingHistoryId: uuidv4(),
+ findingId: monitoringFindingId,
+ statusId: monitoringStatusId,
+ narrative: faker.random.words(10),
+ ordinal: faker.datatype.number({ min: 1, max: 10 }),
+ determination: faker.random.words(5),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ sourceDeletedAt: null,
+ }, { individualHooks: true });
+
+ await MonitoringFinding.create({
+ findingId: monitoringFindingId,
+ statusId: monitoringStatusId,
+ findingType: faker.random.word(),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ }, { individualHooks: true });
+
+ await MonitoringFindingGrant.create({
+ // 1
+ findingId: monitoringFindingId,
+ granteeId: monitoringGranteeId,
+ statusId: monitoringStatusId,
+ findingType: faker.random.word(),
+ source: faker.random.word(),
+ correctionDeadLine: new Date(),
+ reportedDate: new Date(),
+ closedDate: null,
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ sourceDeletedAt: null,
+ }, { individualHooks: true });
+
+ // These are the actual citations.
+ const citations = await MonitoringStandard.bulkCreate([
+ {
+ standardId: faker.datatype.number({ min: 1 }),
+ contentId: monitoringContentId,
+ citation: 'citation 1',
+ text: 'This is the text for citation 1',
+ guidance: faker.lorem.paragraph(),
+ citable: faker.datatype.number({ min: 1, max: 10 }),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ standardId: faker.datatype.number({ min: 1 }),
+ contentId: monitoringContentId,
+ citation: 'citation 2',
+ text: 'This is the text for citation 2',
+ guidance: faker.lorem.paragraph(),
+ citable: faker.datatype.number({ min: 1, max: 10 }),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ standardId: faker.datatype.number({ min: 1 }),
+ contentId: monitoringContentId,
+ citation: 'citation 3',
+ text: 'This is the text for citation 3',
+ guidance: faker.lorem.paragraph(),
+ citable: faker.datatype.number({ min: 1, max: 10 }),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ ], { individualHooks: true });
+
+ // populate citations from citations into the citations.
+ citation1 = citations[0];
+ citation2 = citations[1];
+ citation3 = citations[2];
+ });
+
+ afterAll(async () => {
+ // Rollback to the snapshot.
+ await rollbackToSnapshot(snapShot);
+
+ // Close sequelize connection.
+ await db.sequelize.close();
+ });
+
+ it('aro citation', async () => {
+ // Get the monitoring review object where the citation will be assigned.
+ const monitoringReviewForLink = await MonitoringReview.findOne({
+ where: {
+ reviewId: monitoringReviewId,
+ },
+ });
+
+ // Get the monitoring finding object where the citation will be assigned.
+ const monitoringFindingForLink = await MonitoringFinding.findOne({
+ where: {
+ findingId: monitoringFindingId,
+ },
+ });
+
+ // Get the monitoring standard object where the citation will be assigned.
+ const monitoringStandardForLink = await MonitoringStandard.findOne({
+ where: {
+ citation: 'citation 1',
+ },
+ });
+
+ // Create aro citations.
+ const activityReportObjectiveCitation1 = await ActivityReportObjectiveCitation.create({
+ activityReportObjectiveId: activityReportObjective.id,
+ reviewId: monitoringReviewForLink.id,
+ findingId: monitoringFindingForLink.id,
+ citationId: monitoringStandardForLink.id,
+ }, { individualHooks: true });
+
+ const activityReportObjectiveCitation2 = await ActivityReportObjectiveCitation.create({
+ activityReportObjectiveId: activityReportObjective.id,
+ reviewId: monitoringReviewForLink.id,
+ findingId: monitoringFindingForLink.id,
+ citationId: citation2.id,
+ }, { individualHooks: true });
+
+ // Assert citations.
+ let activityReportObjectiveCitationLookUp = await ActivityReportObjectiveCitation.findAll({
+ where: {
+ id: [activityReportObjectiveCitation1.id, activityReportObjectiveCitation2.id],
+ },
+ });
+ expect(activityReportObjectiveCitationLookUp.length).toBe(2);
+
+ // Assert citation values regardless of order.
+ activityReportObjectiveCitationLookUp = activityReportObjectiveCitationLookUp.map((c) => c.get({ plain: true }));
+
+ // Citation 1.
+ const citation1LookUp = activityReportObjectiveCitationLookUp.find((c) => c.citationId === citation1.id);
+ expect(citation1LookUp).toBeDefined();
+ expect(citation1LookUp.reviewId).toBe(monitoringReviewForLink.id);
+ expect(citation1LookUp.findingId).toBe(monitoringFindingForLink.id);
+
+ // Citation 2.
+ const citation2LookUp = activityReportObjectiveCitationLookUp.find((c) => c.citationId === citation2.id);
+ expect(citation2LookUp).toBeDefined();
+ expect(citation2LookUp.reviewId).toBe(monitoringReviewForLink.id);
+ expect(citation2LookUp.findingId).toBe(monitoringFindingForLink.id);
+ });
+});
From abbaa718c467723292d634cd30781e472ffaa852 Mon Sep 17 00:00:00 2001
From: Adam Levin <84350609+AdamAdHocTeam@users.noreply.github.com>
Date: Tue, 19 Nov 2024 17:02:04 -0500
Subject: [PATCH 016/198] Update
src/migrations/20241115143738-AddMonitoringEnum.js
Co-authored-by: Matt Bevilacqua
---
src/migrations/20241115143738-AddMonitoringEnum.js | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/migrations/20241115143738-AddMonitoringEnum.js b/src/migrations/20241115143738-AddMonitoringEnum.js
index c1131af3a7..31cb2a0dc3 100644
--- a/src/migrations/20241115143738-AddMonitoringEnum.js
+++ b/src/migrations/20241115143738-AddMonitoringEnum.js
@@ -7,9 +7,9 @@ module.exports = {
await queryInterface.sequelize.transaction(async (transaction) => {
const sessionSig = __filename;
await prepMigration(queryInterface, transaction, sessionSig);
- return Promise.all(Object.values(GOAL_CREATED_VIA).map((action) => queryInterface.sequelize.query(`
- ALTER TYPE "enum_Goals_createdVia" ADD VALUE IF NOT EXISTS '${action}';
- `)));
+ return queryInterface.sequelize.query(`
+ ALTER TYPE "enum_Goals_createdVia" ADD VALUE IF NOT EXISTS 'monitoring';
+ `);
});
},
From 9d0830dd544ae8c80c8d1092d66f79f035f5c6e5 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Wed, 20 Nov 2024 12:33:54 -0500
Subject: [PATCH 017/198] WIP
---
src/policies/user.js | 11 ++
src/routes/citations/handlers.js | 40 +++++++
src/routes/citations/handlers.test.js | 151 ++++++++++++++++++++++++++
src/routes/citations/index.js | 12 ++
src/services/citations.js | 15 +++
src/services/citations.test.js | 0
6 files changed, 229 insertions(+)
create mode 100644 src/routes/citations/handlers.js
create mode 100644 src/routes/citations/handlers.test.js
create mode 100644 src/routes/citations/index.js
create mode 100644 src/services/citations.js
create mode 100644 src/services/citations.test.js
diff --git a/src/policies/user.js b/src/policies/user.js
index a71dfadbb3..708871bcb1 100644
--- a/src/policies/user.js
+++ b/src/policies/user.js
@@ -76,6 +76,17 @@ export default class Users {
return !_.isUndefined(permissions);
}
+ canViewCitationsInRegion(region) {
+ const permissions = this.user.permissions.find(
+ (permission) => (
+ (permission.scopeId === SCOPES.READ_WRITE_REPORTS
+ || permission.scopeId === SCOPES.READ_REPORTS
+ || permission.scopeId === SCOPES.APPROVE_REPORTS)
+ && permission.regionId === region),
+ );
+ return !_.isUndefined(permissions);
+ }
+
canWriteInAtLeastOneRegion() {
const permissions = this.user.permissions.find(
(permission) => (
diff --git a/src/routes/citations/handlers.js b/src/routes/citations/handlers.js
new file mode 100644
index 0000000000..f83cfc415e
--- /dev/null
+++ b/src/routes/citations/handlers.js
@@ -0,0 +1,40 @@
+/* eslint-disable import/prefer-default-export */
+import httpCodes from 'http-codes';
+import {
+ DECIMAL_BASE,
+} from '@ttahub/common';
+import handleErrors from '../../lib/apiErrorHandler';
+import User from '../../policies/user';
+import { currentUserId } from '../../services/currentUser';
+import { userById } from '../../services/users';
+import { getCitationsByGrantIds } from '../../services/citations';
+
+const namespace = 'SERVICE:CITATIONS';
+
+const logContext = { namespace };
+
+export const getCitationsByGrants = async (req, res) => {
+ try {
+ // Get the grant we need citations for.
+ const { regionId } = req.params;
+ const { grantIds } = req.query;
+
+ const userId = await currentUserId(req, res);
+ const user = await userById(userId);
+ const authorization = new User(user);
+ const regionNumber = parseInt(regionId, DECIMAL_BASE);
+ if (!authorization.canWriteInRegion(regionNumber)) {
+ res.sendStatus(403);
+ return;
+ }
+
+ // Get the citations for the grant.
+ const citations = await getCitationsByGrantIds(grantIds);
+
+ // Return the citations.
+ res.status(httpCodes.OK).send(citations);
+ } catch (error) {
+ // Handle any errors that occur.
+ await handleErrors(req, res, error, logContext);
+ }
+};
diff --git a/src/routes/citations/handlers.test.js b/src/routes/citations/handlers.test.js
new file mode 100644
index 0000000000..2551d06bad
--- /dev/null
+++ b/src/routes/citations/handlers.test.js
@@ -0,0 +1,151 @@
+import { getCitationsByGrants } from './handlers';
+import { currentUserId } from '../../services/currentUser';
+import { userById } from '../../services/users';
+import { getCitationsByGrantIds } from '../../services/citations';
+import User from '../../policies/user';
+import handleErrors from '../../lib/apiErrorHandler';
+
+// Mock these files.
+jest.mock('../../models');
+jest.mock('../../services/currentUser');
+jest.mock('../../services/users');
+jest.mock('../../policies/user');
+jest.mock('../../lib/apiErrorHandler');
+jest.mock('../../services/citations');
+
+describe('Citation handlers', () => {
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ afterAll(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('should get citations by grant id', async () => {
+ // Mock request.
+ const req = {
+ query: {
+ grantIds: [1],
+ },
+ params: {
+ regionId: 1,
+ },
+ };
+
+ // Mock response.
+ const res = {
+ sendStatus: jest.fn(),
+ status: jest.fn().mockReturnThis(),
+ send: jest.fn(),
+ };
+
+ // Mock the user.
+ const user = {
+ id: 1,
+ };
+
+ // Mock the response citations.
+ const citations = [
+ {
+ id: 1,
+ },
+ ];
+
+ // Mock the functions.
+ User.mockImplementation(() => ({
+ canWriteInRegion: () => true,
+ }));
+ currentUserId.mockResolvedValue(user.id);
+ userById.mockResolvedValue(user);
+ getCitationsByGrantIds.mockResolvedValue(citations);
+
+ await getCitationsByGrants(req, res);
+
+ expect(currentUserId).toHaveBeenCalledWith(req, res);
+ expect(userById).toHaveBeenCalledWith(user.id);
+ expect(getCitationsByGrantIds).toHaveBeenCalledWith(req.query.grantIds);
+ expect(res.status).toHaveBeenCalledWith(200);
+ expect(res.send).toHaveBeenCalledWith(citations);
+ });
+
+ it('should handle errors', async () => {
+ // Mock request.
+ const req = {
+ query: {
+ grantIds: [1],
+ },
+ params: {
+ regionId: 1,
+ },
+ };
+
+ // Mock response.
+ const res = {
+ sendStatus: jest.fn(),
+ status: jest.fn().mockReturnThis(),
+ send: jest.fn(),
+ };
+
+ // Mock the user.
+ const user = {
+ id: 1,
+ };
+
+ // Mock the functions.
+ User.mockImplementation(() => ({
+ canWriteInRegion: () => true,
+ }));
+ currentUserId.mockResolvedValue(user.id);
+ userById.mockResolvedValue(user);
+ getCitationsByGrantIds.mockRejectedValueOnce(new Error('Something went wrong!'));
+
+ // Mock the handleErrors function to return a 500 status code.
+ handleErrors.mockImplementation(() => {
+ res.sendStatus(500);
+ });
+
+ await getCitationsByGrants(req, res);
+
+ expect(currentUserId).toHaveBeenCalledWith(req, res);
+ expect(userById).toHaveBeenCalledWith(user.id);
+ expect(getCitationsByGrantIds).toHaveBeenCalledWith(req.query.grantIds);
+ expect(res.sendStatus).toHaveBeenCalledWith(500);
+ });
+
+ it('should return a 403 status code if the user cannot write in the region', async () => {
+ // Mock request.
+ const req = {
+ query: {
+ grantIds: [1],
+ },
+ params: {
+ regionId: 1,
+ },
+ };
+
+ // Mock response.
+ const res = {
+ sendStatus: jest.fn(),
+ status: jest.fn().mockReturnThis(),
+ send: jest.fn(),
+ };
+
+ // Mock the user.
+ const user = {
+ id: 1,
+ };
+
+ // Mock the functions.
+ User.mockImplementation(() => ({
+ canWriteInRegion: () => false,
+ }));
+ currentUserId.mockResolvedValue(user.id);
+ userById.mockResolvedValue(user);
+ getCitationsByGrantIds.mockResolvedValue([]);
+
+ await getCitationsByGrants(req, res);
+
+ expect(res.sendStatus).toHaveBeenCalledWith(403);
+ });
+});
diff --git a/src/routes/citations/index.js b/src/routes/citations/index.js
new file mode 100644
index 0000000000..97eb727ef8
--- /dev/null
+++ b/src/routes/citations/index.js
@@ -0,0 +1,12 @@
+import express from 'express';
+import transactionWrapper from '../transactionWrapper';
+import {
+ getCitationsByGrants,
+} from './handlers';
+
+const router = express.Router();
+
+// Citations by Region ID and Grant Ids.
+router.put('/:regionId', transactionWrapper(getCitationsByGrants));
+
+export default router;
diff --git a/src/services/citations.js b/src/services/citations.js
new file mode 100644
index 0000000000..2d7875fcdb
--- /dev/null
+++ b/src/services/citations.js
@@ -0,0 +1,15 @@
+/* eslint-disable import/prefer-default-export */
+import { sequelize } from '../models';
+
+/*
+ The purpose of this function is to get citations by grant id.
+ We then need to format the response for how it needs to be displayed on the FE.
+*/
+export async function getCitationsByGrantIds(grantIds) {
+ /* Utilize Garrett's suggestion here for getting citations for all grants in the grantIds array */
+ const citationsFromDB = await sequelize.query(
+ `
+ `,
+ );
+ return ['bad'];
+}
diff --git a/src/services/citations.test.js b/src/services/citations.test.js
new file mode 100644
index 0000000000..e69de29bb2
From 7212595260c652f184c7c92e0b1c4b423d5dc850 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Wed, 20 Nov 2024 15:57:21 -0500
Subject: [PATCH 018/198] add standard column per Garrett
---
docker-compose.yml | 20 -------
docs/logical_data_model.encoded | 2 +-
docs/logical_data_model.puml | 1 +
...23-create-standard-goal-template-column.js | 57 +++++++++++++++++++
src/tools/createMonitoringGoals.js | 5 +-
src/tools/createMonitoringGoals.test.js | 11 ++--
6 files changed, 66 insertions(+), 30 deletions(-)
create mode 100644 src/migrations/20241120031623-create-standard-goal-template-column.js
diff --git a/docker-compose.yml b/docker-compose.yml
index 0fa22811e4..e8885f5fd0 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,8 +1,6 @@
services:
api-docs:
image: redocly/redoc
- profiles:
- - full_stack
ports:
- "5003:80"
volumes:
@@ -13,8 +11,6 @@ services:
image: postgres:15.6
container_name: postgres_docker
env_file: .env
- profiles:
- - minimal_required
ports:
- "5432:5432"
volumes:
@@ -22,8 +18,6 @@ services:
shm_size: 1g
minio:
image: minio/minio:RELEASE.2024-01-01T16-36-33Z
- profiles:
- - full_stack
env_file: .env
ports:
- "9000:9000"
@@ -33,16 +27,12 @@ services:
command: server /data --console-address ":9001"
aws-cli:
image: amazon/aws-cli
- profiles:
- - full_stack
env_file: .env
command: ["--endpoint-url", "http://minio:9000", "s3api", "create-bucket", "--bucket", "$S3_BUCKET"]
depends_on:
- minio
clamav-rest:
image: ajilaag/clamav-rest
- profiles:
- - full_stack
ports:
- "9443:9443"
environment:
@@ -50,8 +40,6 @@ services:
similarity_api:
build:
context: ./similarity_api
- profiles:
- - minimal_required
ports:
- "9100:8080"
env_file: .env
@@ -61,24 +49,18 @@ services:
- "./similarity_api/src:/app:rw"
redis:
image: redis:5.0.6-alpine
- profiles:
- - minimal_required
command: ['redis-server', '--requirepass', '$REDIS_PASS']
env_file: .env
ports:
- "6379:6379"
mailcatcher:
image: schickling/mailcatcher
- profiles:
- - full_stack
ports:
- "1025:1025"
- "1080:1080"
testingonly:
build:
context: .
- profiles:
- - full_stack
ports:
- "9999:9999"
depends_on:
@@ -91,8 +73,6 @@ services:
- NODE_ENV=development
sftp:
image: jmcombs/sftp:alpine
- profiles:
- - full_stack
volumes:
- ./test-sftp:/home/tta_ro/ProdTTAHome
- ./test-sftp/sshd_config:/etc/ssh/sshd_config
diff --git a/docs/logical_data_model.encoded b/docs/logical_data_model.encoded
index 9e809d4546..804d6698c8 100644
--- a/docs/logical_data_model.encoded
+++ b/docs/logical_data_model.encoded
@@ -1 +1 @@
-xLrjSzmsalxENy7IVaZYccnjaijLdMhlQcLPTbndoof9pjOcbL9193I3c0Hc0L2Eaij_lmB05m04IO3ao7Q2JzA0O7VpGQFHwCRBFnW7b5TbaQJfPocu9oXFzvJS5h1awNl4Tod0smBcQKR9UvRUGSYJD6Nl4Du32igqG1ZoXprC2UKxOZpA2i1O-bkIfcdwbD0SqbDI49h-vPjl_lFal_wcbL_lKFOEGyRIhvCq_LsASYhUXYn9Qped6KVtY3zSmWpj4SIS_PQaJrAKfySfmXnk8KsFqxzl8OOKS9Z_Jv9kAS3yfLdXykpvsUNP-UdPLNhqGZndHtz5UP8IleFq37F47qRUUDFMGh1-yKaAMidsX8xnEp4gjkzd50g1JtXwd79KGiP1kLVOYv_fhmJ3z3Z5vFtdNuZwrJxth_zxajZu2UOd-pqbzv1EWxZy-oe6wSXRRncaZuD5ciC2JjLEKaouU8V3NtYICWvunTBZ5EAg74Bpb1G5k2KKSFBdGdG0Z-UeW8-1mnqguCXRKemG77o_V-YxNn1GF4Umo2yXsnFCPe0WI7W38Igt7xWbeBY6vRu074eXBYWfzwFsp_t6OI1BQOr9tRL-_6aCnKl8I4MpHmMGrZn7tcx5EWn4YhXiYiRaA9Z4V_BdpGXKDyRhloKf1gN2NPOdajCT5OKrINzgV_-_BxySQwRXbfnkGKBH5c5GJBISqLPBBSSsWDUwRPmLXC6hYkqXpay95bsX7MpqPvSGC8ARWIU5E7y6vBWac2GfOlBBOF5mbYCEnQVS4b9085wv0mMJTIuv1tlG4X4AqOriikNQP-r0TmzejWFqyw-__VOBJMUQzmOwxPlRdw4Kw9HKT8xXQmbuDsRxHjTIalS8WLgu3pasjbBF6oJNWEbZ6pPR2UTR_JLW-GJjVn-vtY2WctzUY3ySN51oZtAO3um3KrGTWq7_bOcclYPxb7sOTkQ2zSZxIhGFqW7y1qiV8FgVldJO9BEu7dPI1-WR4KCmvFbgzZZRVk6f4lexI64tjWuEawpQ2w2kHIIa8INxMyjJwtOJG-byAnZUe9-WpHisTMUdh_xtj7tybi4yIK6U8uu81iKvuIASb2bT4DDyx7gXjPTks-Xl4YSiuNJh5e3njmh3fmz-q_sGm025MqJmQCEB2e_KYv-a8Xg5P4_inFawqJIyZllLVSpiVrOpjxqEg3ZNf5CEYgmIq-3yoeZtDDhIMGu10g6NfEZwUGuBT0_fmsl8ToJlYPPN573rOJdCK0cApx_PVFM5IrsxgN7Vto-5qiWgFpE2RuLwu6q5jZOMukK2Ra6cLCQKnQdp6bTSbRUGlhcx09IZ3Er5VBHha2JTWoqK2C5Q355YrBGbsLEofQ0KRUPwJ1lZst_mxmeoBfN0ihGYNcr0mwhR4Z6XErRZoGUSGnD93kWMyWko5mqk5Dp1Nhe8pWSSEFHFFBb7b5UWU5OhWqjBMtklnmk5AeLRUfs2jzKtmfIj6I2Zam4ftc5z8LqigIYJQ9HWpwx9o2pF8jZ8VmyAb3yVHUpxkifSgrTFiKsTVIBwmHvi0OTly6j8jz2OhdDQmGC5ipMGoxB2A9Cg_ufiVMUVcVNUwEJQhFeHpJTxedbyEMq6dD4Xiab67IeqB-hbW8FExeT_krz0Ph9JBGgT6A6d9-bU3FQCipVuxRuWm1c-Eco7EBk7c2kxU84Ami-1yXzE8UQGodRoK19eKHbfUfjX2oKsE8zGC4Lyb6oGATkvsxY5Udf2O1UfmRRnCrJwYAXvBM9mzlxmqxUNbsVdrpVNBr-VNLsVlBvmzJ_o7fk8kztf06WSRFiEtvvazeUrY7cRSq-J8qps5VeLcb_p3Zuuchy9IGWP1ZJ4pA86cp0HZW_DiMQM4p0wS0tu2o5PWayjorqW_EufoDwr-cMW7Pp5387gmsKg2Jvf9eINUbH7Fh5A5cSrG_EO8H0UCcxZ1aaVbqsmGaTihvZFqipcL2xJcRqKXLUeH0MWY3-yeAJQ-wKmm_NVOZFngbNxGA4u-4jML3dCNzwT4twIbmWx6ldt4R2cWReBJVJEyyBNnKKPz-y0Uudb318Cv5jx4ImBB3a8dku0tdhOWv3nPErizlonnuwNOVZ-oXqn9qK-yAGD2YaIGzwYV2334_reZ86xQ-3mxD7Dgvv3KxYffRde4AQNjPSYQfCbCv7WlT00L7NOKRrt9pxVI53i4ExOa7qJ36k2WRMkrwKotEV4jSlfDJ0S65rYC_J9Hirket1NjM4LElZFnUEJIE8uWPaQsFNHVEBbTbJlrNCrpNIJ728zqjP31BVPWcPAj0DiftVwMj9pCt2Y5xycERf3gpMYp3VL-O29bXwSkVkvI_ELNGMfBW25c8Uu4I5BmJycJxcMv7i4z7IJn_IklrxiEINbLGaePpi8-JK75mN0-fHdVBaOvUuy-z9bP1BvSvbtUeNmktLNzVitvROJ8tYa3GiyIlakuRu06IphVERYwAoJw4gnsaD6OMsvFPlV-j8fRHW2Xgho_4MGIgct4Dw-M5qJvplUjFj4lT-RGFVH3H_d3ZSXCrImwOdBOiq0umeKpokS8xnzJJ3VzKXcEN8Pw5gDNOywqpfi4gIMGdSuL3wxsiCCWG8nVbO2L4nLSX92M2Q5fJinAP5UsLhpL2VffqJgLNCAfUVdHzm8iOZWbEJEznq3YAR7fDf_a5ZmL9Z62CpyWQB1CWhcjXwEQKoGIGp3z40JDh_0bJ_x6QEdyBqkgji2PSPImnsWCFydaaRBt7xsKYQ7dW4EV1C_AftqV3mJ_xAj50owaU59ZzFFNZuxExz--VpbsRFFZe-Fv2pEalf6lNFaZv9SI83lI9SJCoPD56Q4vgUaKckHwYeisy4Iz1LR-rYFiAN_2WQ5z2JAuQi0o7iiKkKdNCm1zNLPwN7wjpfmb476XYluDzBS1kBxvwDjPA6_Ph_BuHt2I0zNoMGjnE2sW0CB3Exu_WfoZl3MUCVgmaA3ycf624wepMl0thKlxW7lr7LF6BzGJORmLYpLqaLlaNeDK06fkMZbLcHSkNkHc3Qmswl6vcR5sw472iT0JaFivMzFqf_9hMF5uHEn2a-4u1RxSXIogk3jmaDMPG8vpM3gY64ECS0Pz86DKxJTGjgkicrhwPtMejbsNi3Xbj073V8PgtfaO4UFpFeF6rSAXhkgS291CEA48hoLs-clgh84547laToO4Bk6SctYVlJN8-sT8pLNk4Vm_SlSJQ0JxbBMQwWqRnq5ZJyo374QLFkjR_R7SsCVO8mIKhwXMu3Hh_QYtsyt_-7jZi6Ig7KtLsfVGO-CCUuXp3Yjil4tgBG0pVBKQGTT8lGiQ3BVDUXp96TStcxZdQe_hIaruKPLslUEMfM5S9hA2rNHK5lntb4tI8wCtprfL1wheTQyVnIstaPIgjCppIXZXOG_1Xqd34xFVZD2APJw-0o2_1N2S7dyh5PW3-vjH5BVvxfGaxz3JFpsSXMNgtAu2Yhv7aOqyH5jyPqXKylour1XFsNB8OUqH1WOALIZFs90nSQE-qxJ7DdS5PLFRzmLtUQ-hXqgy3t-NLvEoI4wninv_QUYoQjeuDfAPnl9HrEvAmvm3cZ-U4u9gpbwAMqZdgQezSkVc3FyYBhvXpEB1xM7qH0kQfj7jHjn6XAd8OuPwufMpfun7zx6gUb5_JdUSQZ5a_fFB9gbwbsZ4dxmaVxfGx1kDFKcvNyCmwsrneeTnzA5n-etVnnVH_-UjLsT7bRHqmbk6RyTqeiDuggjAlOUrAiFcYRxeysOx6MTLaHAKrTIyOpewpVLkR-SWsnNcZ05fWPK0ne-Oe36zG_mSEYWCGzUrzBr9kE7jtKXGOTLmmM8Yzot-QbBUid7Dn50t8pG-lMYROLoaScTfR_w_gQsiUUT7l2nxVmFNprb2RPq6VF0TnvowLNtAV0NViNXFcJN3gxV6MkwkigdjLkd1qP_fmxXhfqw4o_ANJgzEdOJFCR9TzuwfPy6htvUprcgaP5oByoBS-JJJizz_9Mjymvr_EEiS9snV_5ZsHnsRUqCDSe_mFQmFgP-cy7yjd7fcx_cd7-wnhxH1VU4XFQ2G-ykqtPjppihTVRcj3j46J-j3Q9FtPVuGcm-a8jRfskV4tub9Xb-SjJVvgN_dZABgjuYYyhU4elAlW8FxjWRsgAdsPAk8-kQlD9CpEEyl7adhL-8DHlo94Y_y4MO4MSP6HrWhyJpZ_YEbWkrV-S2KaOmXiLIPzscPnmo3St3T4A_iBUFf5xVBXZsJjvIXx4kMrqCx-wKlPyCVq3dvYXbU5pABFktwy6UmZlqY-CewJjNSTCztTkYwhPmRV0xnERd-SvQ5y2oJgPr8zIU7Zkwpypnv5yGqD2zPIDNYtqeqpQs3v6EdeXSGMILYGqVdnqVYzyW4qVhtWFwWF1M1s3hQJlnsF5zb-hmtZDAZKdCqAQ5TIUtfViKPhA7xJ7Czd4pKjgd97zeb-Ctftev2vtR5YkAHl3mZoZuxmOg0S9pKdEqQD-IteGwbKXzKwlj9UJ0FFQmAV4Bu9MeX4wrT7iPuXWVxniXGaui_XZkUl0zuTcNEsBT5bnpb5rWSRnTzweedAEPxInqtw_yeWFuZZba6KLxPwzUsREyWjAjrCuazTru6R9raFMuZBcRlSe7rkC-mtglYTYzrkQL_koOU9yQFEUeu3LFiPrzD6ttyQYyoWY5ydXMaErMjN8YBFILvSSIK7njdxfR-q0tjhV6X6IzEnU2snTnU590lvsgXqZ7-1msyIGj8MFgAeWsDwSExsqs-ZQPmNQRR_TXDw9gauptEBfXxdbqrXd0zu0Y8Dr1SskxjniGc1GwvA0uFFQzRSueNDcl-V2PFy6YtZlzvXvIYdAuTX-fEXOJhpIRk0zr_kSzA2hOpnpeh7qqLhxKLyM07UwWsm9zEtmYTtXBVj-UzQZc0n_8sd7o3hfyEUwQIlWsNtjcQm0Uqm3iI0al3to5qex96theW0aJqxTNPAkcWUj7QjnnhW9ikwHnUfoUN5wU_El7dvfnHO9t8siTYNC1n2USL7n7g8mWEquEQ3xITLsHCoYq8nYUEy-7CbBOcYGYNmtmlJpF0Z6ECjQi8RyHFiLDAQvakI6r6HFsN1mRTLC2-avM46EaFEWuISjsySsDR78LL4ZIVrUWGFpX1Z1MKR6sv2O7R7TB0CsRT24e53Vj0QS0OK5BBS5YshCceQv_XioUou96LvlhQfR6SfTUUuEOSbVTsQX7F53kXvDdzQpG0vHk2abeBoRnmybpL718_RBmLOcTqsAv0clqieiVJbwTs7USUKJDX3j_ezMGPMLn0rYlwFQXLzG1EJ7aYBg_I3PLfLH35kah83eCBISSm_StbBo_CMSUfCWTfVKqxuQXhQCbNfcBEEgyA8qmZS8qi9WaGjM7oehcH8Zw48rT2nBnSkFbYFn7gDnGHOX68T9M25hNkKMqHj2ATjqVgsYiml2o8vKHh16m8awZECM-vmLJXCU2P-GcwGYG3P1rqq84I04qUZ5q1Ca5KWIe1r2SbJXWUVRibGSaUS27vTQ7YVlP0SvlEDZsOi-YiWIn_RJ1RrPROsvG3kNMblxoIVNScOTJ1wpVE9Cr4_W4NS8TLSe5MGJ80YWEO0vGHcxGZW0r0CNezXH2SPew0ixupT4Hfm6S8aOkZDntHKU06e36BtMEEDauZHro4D6oqOhZd0T84Hm7E14K6vJ1-tqZFYq-mzS1Hv1S84M0N015L7ge35yM6n5g4OhACmXDHCL2PV3csGXF1q-4PX2Q28i5IyZrhdywP4AkSVtJmJDvjsl4oSHfu5RI29CJ9iULbn6f4QJSzpD6oAOXYoesYLVKAX-K71SjHAI6a8gsAWYhQAGcWOe83IBalhylecIYD7GvOCJ9XLDK9W-Gua0N25NiTrCuHO060Hgujot2v8OfGcH15FT_148KuR7X6Rx5BGAa0nI1iO2PkeCjecIW55U1JY4KaR6HSTyb5S4Hu3ZlhehGI6W6BwwA-6ZuHBsnS8d7gGuZius6nWLBd3IuWN7SpnIHQP29e5YWs3S-HLGaAbDtI4MOt31DN3CLONF35Y6o8HgvhoeYf13gNtSLaH78HE_7XOhZUgi8J0FCyErO4ICBeuYD0vlXExaYHWr64HGBbFw5K14NWOlxNgY8ZSB6lkdgJurBybbwmXZ9aBIzB4aIEKzFZxuwIh_yyiU_t1OWUxVP5m3X3T-Xx1s6ZFttrpzz-QMiecq15dArlpRoskD4xJxp9MQVdf8UfrjHj55_kYT9CdBtayLDN387SulTRsbwX8_Ite7YwR6utimiswHWa5dYkDebWF_KdKFfiSLRLDauoipNLfUg3HLx2zI-aGnTaJ1R8Fjoc9vCRONDq8FwpGFArlI8KCdaUmUKQxWbfsLxZ3PLCyyPmgaNBSdc2TJaqNfIwCoRIgjmDAxvSx5jdgt6pRIOOn_QgxsQ3Eu-iabfztGrNie6JdsdhBKQJwrVJ37kggx6VRYdsyo7-dK8xtQXNNgSiZtC42WGustffhkxNYipnKXwjrVC7CdLkyVrBR3sauqiVqv9sNjdaGIzGwU5zsALRGplGd1Onh9tufFqcAp2bPcwLmwcn_Mf-TO39tFhUecWPdRSto6TDhDyqojHvgHn6LRqFJBvtwIh4SwvOz4fh6BLABRubVIEo0zWuN4ZWPY3Y5eRzMUyyY9X73aTOKpqKbPBuPoZX0yyTtAYeWyto-Zsl3-TWUNYXN8Qqdp5mgtjeJm6ShPTAyYVzTHkEN5dfbCmSvKt4qh7aTg1HjfrSdbjgwr76rSXIuPEP5vtuhUNH1ljwUPsAt2VuEVY54hUAQ42kNcqgLckmwY_LBl7RZjQVHKfTAThc7ddS2tXsV1W9l5a-NI_eRn92HKnBL0CJVEbOmSOGriCSmUXO666lAXDpvUQHxkdzegugVIXHErMLyAQhRoYbNAQfVbmzyof65JQDX-qWC5iz4F-sozXUkaikj9s0Gfn7OlATgZ8vw5XJDXQTxRkNy7K-WimnDWcIjvWk57_NRydYheiajtuFKbwSL1_faFdf0alSk1HFxgw2d_pU0E3l_beWBzzIdJGlCZrVkT7MuyQXzegNbsCDfwjbsrwHHtlnU6hEdhkY5e5qsyDK1sGLnqic8e34gf-vYyKPVMpEKLaOpusaJEhwi_cIDQU3TnCejMlke72ATeREPIFPHGjfSqwclFbh70Exn26UQUUXKe6xL0RfO4lUa-ISEzxC5wnwOSjvj8tst2ZDsMYPwlyoLgd05jvBcG-hNc4qpTfRpGUBB4E1msnnzQRnocdQBk55EPF7pRTZyJZdBzPmu1zlg8n4FJEvzhq7xPlFew8xIbzBa6jAetx8WSjzobsImHgDVck8-W0gDRjzVcFjbbuQosMfpr8Oa27rCED3jS1f71od-Awaegs6i76UioGGgman5W7yUiPAmaor19ErlgPPAiqmROZwDC6Y_a7-DEbYslYF55hxQb5HtepEE5OKP9HI-MMPSHGBPLciebiMSmNLfV49GBREALAHeuIMo21qrQOZmRwwvfI3E3Wv6iSwhJMQ3LghjkAgqhX7YoRKWjswvYTrJD8s2ZrIoNDbybF1dn0dbts-TpYseGWhFf9bpyFpxdhuKTSbstQ_xemf2D4lcAidTY5am7tHV8CnkfwSYHhhayok7NnyUdrIIEHSctnnonHfpFo6tToVYc_BlIgVR-Cl-gVf9-3iZIWHLfY3ywmrbncPllgJ3hLIde1dKMneCOINFHYGifosEL96_DP8mIrwSMY4Wte-Mbf5T2h2VNpqcmUbu4qXSR8cQQOnwjny7NgofkVHS-T1gWBzOLHINrEnMmZfOGMD6mhn6RGDbEJ7MNTgC-Ys-5R4sp03QNa78OQggG_teHpq8ufqaZlkz8PD1j_f19_D-cEb50HIRfnVAsrshJ-Vto5zdA-pkN5GymTwucNI-33U1c_OMotYz4SMa9WykWl7vZ3o-KrTdpwNgFQ5x5iJmKbQf6IzIIOTdSoIqkKjaN1rDhzl7Llh7Tx28g9EjHZjgbWudNJKhYMjwsuDAjPsgklr5gTv-2ABQSCpjf9tNxhxHJWuf-fVG71N0rLgrbekgPlvmrKENkdjCHF5csa6jhF4pLYhB6lG91lLfIrvGlfsvCEdOSvsLZ6HwvZh_XEh2ak0EDlzxdOgIUoMBHsSa8Pk9pNXaNVSmsbxXSQKN3v14H2-RkMKYbk_QLKSsZR2tewsiQJSDgkB75Dl9ya3gK-rWfu1TG79-EAnS0yF9IBETeeqlfW4qglEmf5h6YdK2tTPsrfC45xRDrq8-miMWwZsP5TR35T1n2q7Ewj1qcpwtpXzfJxcan_YyGi5kqhJUZn-vP-4sfeIYQsp-AZz7UD9-7SmRCP8njFZ9cVCR07rP-qmpjbFjkrz8YMDpTxDmVhBTfVXOs-Plz0BBnmMvUbwIMnaArjov1RGj1QG6s7InOGDlB3gv8U27d_4_4TgPhbs6Pvht1LUOnWgnutHUioyb9gv-wzkVgG-MmD3D29Ec0f_PVLoG-cGjPbOmIkhdUKLDjTpyVSc8zx_fhvtEqiEGGVO3TISteNvAWGhbsASwrL8MOFzQ93RngZk-5OEyUMlVngUCEKgEcSXhitKitjByn_ZzHS5WERXPOQ4JESlPARijMt7yphOFOIw_d6fewl1pUnBOvgs5wAV2uzF9m1Nmf_4yGqyvjp1TKoW6sqtxyRgpXY7H6KhijM9Gv8iSpBede4TUwtAI9Di7Ck5VLjIV8FYFEgB9B_1m00
\ No newline at end of file
+xLrRSzqsadxdh-0g3vwuZcTpcMpLg3AfKMJ9SQqlAadEr2ILKa4a3WzY4ZW3W7AKRFxx5W1U010a0P97jXC-IGS4kviV6er6Dox_P1nGNXPv5CUFOd17K1hlARdDOS7YTuZlOe2p7APnGiax5cyXv54SixS8xm45HPmZ3Fdz7iQ4yXqn7YKvO2p-BOhZEFwMq1JIKv8J6d_ovPV_S_BtpxJownrAtcB8MFn55CU_Ir5EnTkGHN55voJXw0xn-qkOe3s2cBFVelXnZDBuE4QOmmpI-3Z-EeCOKi1X-aqKPnHW_joiS7bsVdPvzkhqx2e-UYQUiwE_eZIA2Tz1UeOvulS3RxnkQY5OlVWu1eraUqA7-9qO5Plt4mg5m2SyFeuvAY3ZeDWhxCNF-5U2ONmSO_BkBv-A-kK-zQ__UnJP-0dc9_iz9NUGZeCu__jA1kd0MsyOf0-3HPp278xLZc9Ck7Y7mrzu53CEUCdYunZYikX3yvJaERWb573ovqAq1uzpbCE7m66BSZZmbXGZ10VVx-_xxcS4vES8vkabP7k2sGGG10bl84JbvWCp0l9hMEnpmA4KueAIOZze_znl70Qoc5QOL5thnv-6KBo4Z9GqUL00QSLpuEr6hCD0f8ZBfgXDZe99_2T_tWn0TMEy_biKOr0et6Lv93bjgIYiIFfJ-_h_dZmPQgR-bXnlGK9HXZ38HrhEQAkvbYCBuBLi6oSvOV2gP7k8qrE2HLTe1XlzsIK4ZE1wu8aXJfy2EIu99b6CcFmocD-VfOX3yIctXEGGY5Us86UopYNdO0yQ8a8Xsf5Djbpph1dexW1DTe3-vOklllfKqvdwlPvE-_hsPoW5EYKbdMBu6WBUZTcyqRQKv3r0e1BkVOwDZTJJ1iabOFhGXir6mkbMVmbOFaBxtqMkCmZevf_duWy75vJSeHIcFUF065N7OD1zPQIfhyaU9U-ddRbWFV2-4kszT03_mT87YEwdpnqsoIIkXvtK0VfQn10CUJwVFCusNtYgXFuEKjYEhRt392lt0gXh4GcfISd-rl3Kkjt8K3hT2eOtw4Tey_fDtTdfY_yzRHw_OJ2DOf2cY2E2GVwAS15EIfGkY6a-ThtGs4ktRVGtYIEMSBnrSy3u6uNXj_T-q_sGm0256I9uiE55XGVgnOzZ4Or2iYUsudmHQHfUnVsgFcRsFwkPcru7L9nhr2a7ULAAQN1wPSHxQcrfBOS0WT1BahVzF8KvkeFq_YNaEv9sH4jB2hZwi1Gcg03vNxxSVFwfIrtRgN7Nto-4qiWgFpE2RuLwuAm4cOs5U3c76KYKofYoE7LUuuhBuXRIrziBG3bAq5x4Hzi6YU8zoA1025PYY1AZfYt9dfACWaAqcUinhOrlzy5_IiYuL0BBqefujG4DAsyBn8JkM8qd7t0EJIGve1da5sGl6bmek8EzT16S3pXmw9zuSeSeBq7-j586bvOszbwF5mfL2hVqEWLlgs-4AHipG0Od0rAym_f2kbXIKKRHAC2UNPE1MPv5i9R_3kGeFHz4xFkgobIhLqsnJPrzBFh17cI0mzVu9QGPDARhd9RmGC5i3UIYA35AfCg_89dlx3EntXkSN9jLFw3vPaSqZo-dzO0pE4JMoQX3XUP5lHHmqDhzy6_t2yYCTgfPeHCZzFGutel1dd7s6h_PvmGumrU7xH1dTn1pPHSlK4xuAw3yXrE8EQIodJoK15fK1fhUPjX0YGtE0nHC4Hya6sJAzifjt48zFI6mYrHls_WPAdr4rBmMCJXxV_ZfcylBirVNDzVFNvvTNP-yl73rF_8EcuYxtUa0Q1nikmxVdYJsXpM8UPjpJvCZJFOL-XMQN_CMtZkQlmv921bwDCJCeWQRC16E3ysn9fOJC3XmzVWB8Lc2ZotBdIFyzbkGl6tqoq0xE8iP0jNxorGIV59F22_rg8vyOfNCpcg6vZ5383natCODaZukcs25ZjXUCP-vcSsnNAOpUoaAhr21Sa0Hltz6IRdtIs66wx_5PkHLg_Q1Gd7mboGlKvW-txuY_2Gj47Oqyky3O8q3jGrDz4xpmfV5HHdtxm1xZkKC4lJaMpiHB0iiEGYUxW3EFEn1o7YoT3Px-jXZnyimV7VbZfYJFErvqWI558a-Rr4-aE69RZJ6mDqrS7ZswFfLpw4ftDJIL7G8qylQIn6roPAPIF1UQ03AAkoetjkAppSI53k4MpPaNmV3wc1WfUjLgGptEN5jSter30T6bzWCVR9Haziet5MjMOKE_bFn-2YIE4vWnWPsVJGVURaPrJlrN4qptYI7I8yqzH31JRQWdXBj0DlfdRwIz9KPED6BNv8KRT5YZIZpJLK-OAIbRLlyNuYn4lxZKKBoii2n3CVZBDvIKiG5IM7-AFuw4Ok_Zpz9l_hRbv_z_Hl60Ku1JR_-Rrd-ZPqIzVExflSQ5Usw01HY7kAFXAmf_v1SxagIxn7GyrOUyyJkmhDpIegh0b3ETX3oQmukSe3rkTFmkZFAtGdxkUlH9FAlP0Bh1S9lbzqUuD-KMIj6y90T5i6Kybt2VGuIM5I9qTbXNYVHXOA-Xmpdsd9xDh_rfMFQq0SCLUNfio2LKcyXd3rRN1EdU_2q_KIztvj0Tz4D7p0FDxCpLB3kYVE2rW3Z4kJd9KuHpbuIp7STaacAN0QwKcFNezQqzfi4gQNG7LULpwxsC44WHuoV5G3bOvMSX12MsMNfbunAf5Us5azLsVunaJhLd4BfYVu1VqIiHZWbgVDznm2YwJ4fz-ybg3mMxp4YF7-WiB8CfxcjXsC-56GImp0zqCpFBmBcptx2w2byhuloDaU9iMPn1oXC_uMaqJBtiJsNMQQdW0FV39BAIvqFf_IVDvM2GJTIHV5n_EZvqxDNry_FdvyzVNHyV2IdWvRKD-gT8t-Kv8e4l2LjSisOVL6O49gUab8jQgggi6G72z1LRErZFUAV_aiX5DAZA8Sl9o3liCYvdt0ncTGNUCN7yMTL12g1ZFLNy6-agGt4z-U3RMIXlsI_I-4MOQJ7lMJM5s9mcyEUbGfRlh-2d2ES6UzONLXeK5fEYEK9jNa9sDlcnJtWhRhk2UCNgiaGpiIQBrtq1gcN0EMGcgjVRaNgbhjNcsa3kxtyR6fwlHbwe70Cv3J4NaVtEFwPt3gMBZuMo_b4B6-sBqSXAtFUh61aPIAGqmocZXZc40ESG1_OE4LP1ccpNBUffNTQYsRJUG676Q7t6kGJbWV9294UulKVDguK3DioTo91CEA48fp9VVbNPL40Yg3tI6vCY5r3kJPnF_hhaVREaPeht27uxddg9j0ATodhrLJgjuw2nXyPXZWDglt6DutakKCGC4O9ALxJ363qgxrw-AlDtxpTCLWGjO3dIcrGw90nnZs46QTbClm6bHQWNkSwxg1BX9u6JURR1Zr7SiRbxJPkLVrnJQa8rwhIl7VKg2o4urIUgOg6sehtZjf1Sc3yxaYZ4c8rjVhzex25Ev9IbUUfHnahjlmXw3XXS7pscn1AeTJ7fn2aBn26y-DdbG9xS6yYalg6sPcQ_1rYuhUNgpXVbSDLKCbtFw293sYBjuPC3SiFGuNzb2s57D82OM3aOet-Y04LEqJkMarpf8KNL3wzSHTqcxkwTgZ0z_1tUJabX-WO4-Vr7uicBwA1IokTRIGVJEMkglpbmpabTSRKIsbhyJH5hLx-o9pX1zJDFvnPFAWzN0tXhBPvKBknJaFMQOXZnhgWbRDdp8UtSMfwKNsETnngzgJ-8ujcgNhNg83lVQJ_zZvOD-fwe_8_XE7MMcF1ZcFfmaFrcp-EhwD_pzAkJWUhrTC9RXc_hTQp3UAYhIesCLIh3-ec-wFDcEnbdLP4IbDNKlA4wEitrG72d8DaLvWm1QO6L0CQFcA0ndLF0d7ee34FNjRITIRZXxTr8K67HSC5Y8lSj_cfSth9XpSHGDoCqFhregn4gSZaJj9VVQDKqzZJZY7uq8uG25yzPGasTnbpptSUSkbL-odm5tx5uJva-m-ktXbgkghAnxNjfmD6TyiFuQujFoClokq-dJfs4pp44NhUEgKVAwz-NazPgf6GOY_CYtFaquxXVVYLhTDEX_pZh72Ti17oGzbbTcrjCJNAtyC-iowcVfl1_BPnwPk_fXoakiG6r1aNZeJsWaFlBjEsVqyxT7NovhGjeeoVLWRHf-vpV24s7yZ5hUErNn5-9IOPVd9Kt-Qb_vuoYwhU8elAtXABohu2z-xOwzkYfzcIjYFhchpGJCpZuBrfHxPVY3KRyaX9l_15c15d2HaSOA_4ym_uifSBjN_d0b96C8RvN6VTfcUSLWtDKtb2lx2pZwHQtouOTexmKgUwpbjT3BTRPUz7mn_GkJbA6HwNCil-xNgmft2BVd6SIRrr5TVTqxqjgceBrmQ_Gsn-dkwijG4iwuXRDqBDCRI3-ymSJ_uD0GtTMpPmHzdxCcr3-n3fuD7A9KHMcTZmyUZuMFm6YJYZzmxGUuGp7O3CqtRYeNC0BzKfm6UK6fEOlaqBoqvk2_TnpcGFc_ESxNDhfBJValI-MOvV7m_cB7HkMomf6i33FwBWlnke00XFIStHl7bBUXFgU2FrJOss5v03y_p4kiGlW5UW0ZhLqkrbYFE0lA-M23dw-4EuDCBtg6XVxeYrMt3DKNQ6nV5stSiyTuvcjhFGVh_oYmxWEwQG9HJjdRrwOS_o2agtKZlSr7VZPSZMGTRXCkHkTYeVM8-x3Akz9cBtMfDN-hDZudtyy9GpXvSy3dVo6RVjnwFYAY4KodDUGR9RrCg9iD3dbXzBGFw5VNnlvGFTsDuQ4v9rBcy8Rb_4uKaE_NQg7Y8TutFOnEEuW8ofEoMQt9mw1RZUw5jd1fjjlks7tOYgTJBUu-Y6gGVRMOS9tm6AW7G7pQtjtcv0O5Bea8DY_zZtfZxiS66_vlrd_WIBQk_qwpj8AKhXqdsaR5cCl59luZxKGQ9t82zXDtEWi_RHMFbILnK3ThY3TWlqRV_Htk4j-NrpQAIQ3xmdQyV9M-ZovRXhAk7RVCtXhm5uZ0Em8mMyFF1TJ3ich_gXFIPCJDzUa4nD0vUFrBZZN3d8sgHnV1oVN5wU_Fl7d-fnHO9t8siTYNC1n2USb7n7g8mWEquEg3_ITU-HCoYq8nY-SfuFPQImDKb4lXhWEtcU1M8S9QnUGtuZV8gRGjpjSaCgCoRikJWswgO0zAsi8CP8UM9o51VDufiRsUGgg135_ob1ZlZz3M2if69joKqAsEwM09hMwKAGAsxQ0qu1meAMMeB5j6TrGbt_39azbmMrhjRttioDvSwzFmSnvQwwir4FUA3S3oTFgrcX1oZj59BGJapYXvDpb7D8_RBmPO6TqsAv0clmremVJbwSs9UbUKJDX3j_ezMGPKL-0rYdwFQ-KzG2EJBaYBg_I3PbgLH35kah85eCBISSmtStbBoxCMSU94yTfTKqxuQXpD4IByn575LUbAum3SBKivXaXQeBbHNDMI7gGZHqlyYAbnilH-H_XSeDrGhH84JQ2a7hNhWLhG6qLhlkZzLghCBmiYELArW3OAqu3UAqzZqlc5PuB7W6hgHU0R81icvQM05I00tUCBs2v09f1QW3q3ehSS3ozEaB3qZIW0zBhGyJ5xq3dDvma7LYpwAo1B7vjDvlLbjZRb0AvTQMxl99zToPXrC7hDyuapKZ-0JTvhsgb0ko5I0Be7K0Su0oS1Nt2b0Dq1hT7aDedQOEW3Dy1VMAKmzEAqOkZDoNJgy0rG0DtmwzucJZD7J8mOgMZTOwpdc0j8BZ06S5nGQbztxV5Py6d_xhW5UGN22j0Ba0Mb7feD9_MUoAK8FGgZozqAoeMAZqtZoxmjF2iqBJ2yqAYmLBoFMkVpfaGgvn_TF1CtgtwugJYLFFhQGhJ4wO75TUhaWD8EM-dt65R1ooLSL6yLgroWSbnqKBAr9zIAssAWYhrKYD0hKYr8kG-_w-5IKJfRxB0LUSN3YjokG1qZhG1KArzhind5Q0UW7gkRTjXSaDqmgH1T6w_oCGeXKU6vx-MznMW7I0Lc0iOCRkO5jHCb1QLu6E8RGYOoFZlajg1KU0uxoxQeL6GjDxSTMAZuRFgZSMBkjnglt8PAkXSS6oJXhSm3ZkPrgHgP5PGR50iMzyMb6Hg8pT8RN2v8Pfv9gf5JOtR2j4DaG3zrMj22c4kjTThSW8P5RlnuMAutgh5PWQcFxRiLQCB8nMR1pOzD_9jM9KOxI2Oe7IlH1gnKNWuhuNQiL6OMFVTFMdnYLw59ymHX8aRI-BeaZEq-EZpu_Y9qz-_EGs1ydRpJu726_u3YLlCMJi7r_yzEI9hA9j0HPoDRysyjhZIEq-yoLcdvuI7yUPADgeF-sJA1gu-ydY9YwPWwd5xhSuFi97SUT0yVXON6zcbcrYqCiiSbpj4i1_xCwX-DZYBQfi76NcQujBrOIAlGLgteZwhaYOB91zEKn7fgR2fkz1zUO1PQjxHAXayhq3ojNVakEoFKIRgfdd1E7KYvPaqmIgysWTANJcTQL5E1hN_BdOriyMusPQpA4FxTHUJ0Pt7zd4j7sw62zb0yU-KzPQZAVMByOOjrKNuzxVqstcVVqwX7Upq2u-ZjaU9WZaYF6cTDFUNQ-LcM94dTih9exawjtZwXPO-yb6bfzJKdJUcIL1vz1fuRrOPHl3kn0Svh7idFWaFIQhS66cxfM3yN5rQhxrmCdSkbuYg9aTztT8fyqittHAb3cfNOO5VOz4_ZVgQeHpRbcq2gjOrOejVaMT8_83M7ZSI23c8ABMHlqPhZnB64TEHnWJlPHLKZZdAE43prPELFJXPXxTZlUdCx1Sl51EVPglMBYrFPH7JywsRGNvivhQpIVkp7GAPgxoUaBfE58xq4YxJYwlRBNrQECgP57VYSpBJlpsacYjFUtSRW7k8_nSd4B9EoLK8DUFDXNBPHXrbsfts6M3rHUHGdUw-fct3jSSdZKFPXAFva_tQygRc50Hqm9vtpHVEdQ0CPHXc3e81GlJvqLjEhxKDDeTjLV4JQKF9MgtkXBMREK5hPJJB4s7ksDEnQ3Ij7cl1Gncen_ottm9qqidqPMs2r28grXKiaDvEWqDOyBMkBDL_mgcrbs08SOsKV8Qmutwx_eTKTHbaUp6xqZKYOFwC-qx9qruaJsFkj7LHVYRnniO_Cr7FVpjKwA3vaMkzri_sdfKEzHMyU9YDF5ilMpJAMfvpmvVLT5pHzGgc7vZWAg0lEfWmLGSa53rCtscAAkVzIaY6_EnYPnPLNysHRBcRE1w4gr-LGqKJj7UoA5yBADeBctMqfmkPubJk4KOfgrwvYePj4DDb0O-wpo9mhtltdZ5fXksc4hVRCEDtfIHdgto9skT0cpbkP3vjE8HJj-alT5uiCWw73J47bjl7iUSeYo35ARVtc_Q7v77EN-JXW7xV4LZ8EYSpxNfFcpUV1WHcrFwN89QLMhtHGvQxbFivWZKQFAs9UW0gDBjvVaFjbbuQysMfpb8eaU7rCED3jS1f71o7-Ewa8es6i66UaoVGYman507yUiP2mdIrn9ErlgPP8iqGTOIz6c3sVm3_Ed2nJLodgYrzDIY9RtfdN0iACcWfNBBCc2evaepsaIsBEOBYqlY4e5jd5950qU9pP30QIlCHptzTKafUd1myZKETLfgj1eqrqt5HQLm3vPrgGMxTSpEuXaaR9JwfPAco-Gd0puWppRx_ChYMeSWhFfHcpyFpxdhuKTSbMsQ_zemf2D4lc2idTY5em7tMVBqnkewSaHBhayokBNnyV7rIGEHSctnnoHHnpFo6tToVYw_DlIgVR-Cl-gVf9-3CZUWHHfYzywmrbHcPllgJ5hLYZe1dLcneDeIN7HYGifosELH4_DP8WIrwSIY8WtewMbf5T2h27NpqcmQbvuqXSR8cQQOngjsy7NgofkVHC-T1gWAzOLHKNLEnMGZfOGMD6mhn2RGDbEJBMNTg4-Ys-9R4Qp13QNa78PgggGxteHpq8ufqaZlsz89D6j_f1H_D_6E550HYTfnVAcrshJwVtY5zdo-pkN5VSmTwuddI-33U1w_VcopsMZsBI4mUNIN3qpXvV8QMpvThr7jYvYtfu8IjKZ9UX9CkpsPfI5Acw9WQir-tgetLhiz1CL87UensLGmyRffALpBsnPScbKlxTLNQYtEitV5bbC6P-seQh_bTWfmyS_Ole1WheQgLImqNTEtymQgdBrJMk8dYpPI3UtdaHenbjXN8CZNAygQyfdqxKc7piESRCpZ8zUnbtmdrXJdWF4t-rJir1DPB5evEQ4CNCvhpQBhkOPIzmkDABXy0YAX_5rBgHItxbAgARJj1JqjRUD9E6rNbhWYtaqIXr8VQWMy0Egzat75ecSU7ih576qKQNsm2QNN7eKYLhHJg9RkiwOqcA0zjcxQ4NOIBOVHRCdEjfYk0mWQ3dVMWwZUTJvpUyfTHQQxHM8IY-QLElJuVKE_3ZNK9IFRP_5H_Bl6at3ku5cFaRKdnipFwDc3wa_QuPqod-rQUaJBcvkzcuDr5kqlmaPVityW5bwuBSlIz99OoDQsUSZD8MWjeBR39JC86_dXHKdtXBp_XVYEbFLoRBEyKxYeF4QmLO-RecKPUIcrS_VUL7r8VBO61kX45R2K_ilYv7jJ8SkoCG9NrpjAocpNyJ4tvkEElsO-TpjB3i5xs0tK7Dy5EIY4QvVYL6kLYzb3jUZGcyRWRZZMpB5bBx_gtp0bQZgduUwDL5Fx2_FV8pMLHSzceRb6n0ndB-IcPFhsWsaTp5vYFTtOLF7ru2QsnL4DsqcHJwM7-pFWS-6lWvXcVdEge5e6i8ds-tTZ5KUi8eBITSbKue3ayxmigaTGftwR8b87EwTIKTyMHV-4E2sBFFf_
\ No newline at end of file
diff --git a/docs/logical_data_model.puml b/docs/logical_data_model.puml
index 7a55b54e44..4fb96e0e5c 100644
--- a/docs/logical_data_model.puml
+++ b/docs/logical_data_model.puml
@@ -403,6 +403,7 @@ class GoalTemplates{
* updatedAt : timestamp with time zone
lastUsed : timestamp with time zone
source : varchar(255)
+!issue='column missing from model' standard: text
}
class Goals{
diff --git a/src/migrations/20241120031623-create-standard-goal-template-column.js b/src/migrations/20241120031623-create-standard-goal-template-column.js
new file mode 100644
index 0000000000..939d73f253
--- /dev/null
+++ b/src/migrations/20241120031623-create-standard-goal-template-column.js
@@ -0,0 +1,57 @@
+const { prepMigration } = require('../lib/migration');
+
+module.exports = {
+ up: async (queryInterface) => queryInterface.sequelize.transaction(
+ async (transaction) => {
+ await prepMigration(queryInterface, transaction, __filename);
+
+ // Set Monitoring goal back to Curated for use in the system.
+ await queryInterface.sequelize.query(/* sql */`
+ UPDATE "GoalTemplates"
+ SET "creationMethod" = 'Curated'
+ WHERE "creationMethod" = 'System Generated';
+ `, { transaction });
+
+ // Add a standard column that we populate with whats in () in the template name when curated.
+ await queryInterface.sequelize.query(/* sql */`
+ ALTER TABLE "GoalTemplates"
+ ADD COLUMN standard TEXT GENERATED ALWAYS AS (
+ CASE
+ WHEN "creationMethod" = 'Curated' THEN substring("templateName" from '(?:^[(]([^)]+)[)])')
+ ELSE NULL
+ END) STORED;
+ `, { transaction });
+
+ // Add a unique index on the standard column.
+ await queryInterface.sequelize.query(/* sql */`
+ CREATE UNIQUE INDEX unique_standard_non_null
+ ON "GoalTemplates"(standard)
+ WHERE standard IS NOT NULL;
+ `, { transaction });
+ },
+ ),
+
+ down: async (queryInterface) => queryInterface.sequelize.transaction(
+ async (transaction) => {
+ await prepMigration(queryInterface, transaction, __filename);
+
+ // Set creationMethod back to 'System Generated' for the monitoring goal template.
+ await queryInterface.sequelize.query(/* sql */`
+ UPDATE "GoalTemplates"
+ SET "creationMethod" = 'System Generated'
+ WHERE "standard" = 'Monitoring';
+ `, { transaction });
+
+ // Remove the unique index on the standard column.
+ await queryInterface.sequelize.query(/* sql */`
+ DROP INDEX unique_standard_non_null;
+ `, { transaction });
+
+ // Drop the standard column.
+ await queryInterface.sequelize.query(/* sql */`
+ ALTER TABLE "GoalTemplates"
+ DROP COLUMN standard;
+ `, { transaction });
+ },
+ ),
+};
diff --git a/src/tools/createMonitoringGoals.js b/src/tools/createMonitoringGoals.js
index f70a101ae8..c5e536fc51 100644
--- a/src/tools/createMonitoringGoals.js
+++ b/src/tools/createMonitoringGoals.js
@@ -4,14 +4,13 @@ import {
} from '../models';
import { auditLogger } from '../logger';
-const createMonitoringGoals = async (monitoringGoalTemplateId = null) => {
+const createMonitoringGoals = async () => {
const cutOffDate = '2024-10-01'; // TODO: Set this before we deploy to prod.
- const monitoringTemplateId = monitoringGoalTemplateId || 24872;
// Verify that the monitoring goal template exists.
const monitoringGoalTemplate = await GoalTemplate.findOne({
where: {
- id: monitoringTemplateId,
+ standard: 'Monitoring',
},
});
diff --git a/src/tools/createMonitoringGoals.test.js b/src/tools/createMonitoringGoals.test.js
index 15d4877081..5d1ddac60c 100644
--- a/src/tools/createMonitoringGoals.test.js
+++ b/src/tools/createMonitoringGoals.test.js
@@ -29,7 +29,6 @@ describe('createMonitoringGoals', () => {
let recipientForMergeCase11;
const startingReportDeliveryDate = new Date('2023-12-01');
- // const goalTemplateId = 18172;
const goalTemplateName = '(Monitoring) The recipient will develop and implement a QIP/CAP to address monitoring findings.';
let goalTemplate;
@@ -1619,7 +1618,7 @@ describe('createMonitoringGoals', () => {
], { individualHooks: true });
// Retrieve the goal template.
- goalTemplate = await GoalTemplate.findOne({ where: { templateName: goalTemplateName } });
+ goalTemplate = await GoalTemplate.findOne({ where: { standard: 'Monitoring' } });
// Create a goal for grantThatAlreadyHasMonitoringGoal2.
await Goal.create({
@@ -1770,12 +1769,12 @@ describe('createMonitoringGoals', () => {
it('creates monitoring goals for grants that need them', async () => {
// 1st Run of the CRON job.
- await createMonitoringGoals(goalTemplate.id);
- await assertMonitoringGoals(goalTemplate.id);
+ await createMonitoringGoals();
+ await assertMonitoringGoals();
// 2nd Run of the CRON job.
// Run the job again to make sure we don't duplicate goals.
- await createMonitoringGoals(goalTemplate.id);
- await assertMonitoringGoals(goalTemplate.id);
+ await createMonitoringGoals();
+ await assertMonitoringGoals();
});
});
From 735abff6cf956a89c6f78e98156f33abe03b5caa Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Wed, 20 Nov 2024 16:50:35 -0500
Subject: [PATCH 019/198] minio remove
---
docker-compose.yml | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/docker-compose.yml b/docker-compose.yml
index e8885f5fd0..7e5f632df8 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -80,5 +80,4 @@ services:
- "${SFTP_EXPOSED_PORT:-22}:22"
command: ${ITAMS_MD_USERNAME:-tta_ro}:${ITAMS_MD_PASSWORD:-password}:1001
volumes:
- dbdata: {}
- minio-data: {}
\ No newline at end of file
+ dbdata: {}
\ No newline at end of file
From e114b7e75228a1ec889e024fc127ce5d67676692 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Wed, 20 Nov 2024 18:26:02 -0500
Subject: [PATCH 020/198] add missing model entry
---
docs/logical_data_model.encoded | 2 +-
docs/logical_data_model.puml | 2 +-
src/models/goalTemplate.js | 4 ++++
3 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/docs/logical_data_model.encoded b/docs/logical_data_model.encoded
index 804d6698c8..b5984b107b 100644
--- a/docs/logical_data_model.encoded
+++ b/docs/logical_data_model.encoded
@@ -1 +1 @@
-xLrRSzqsadxdh-0g3vwuZcTpcMpLg3AfKMJ9SQqlAadEr2ILKa4a3WzY4ZW3W7AKRFxx5W1U010a0P97jXC-IGS4kviV6er6Dox_P1nGNXPv5CUFOd17K1hlARdDOS7YTuZlOe2p7APnGiax5cyXv54SixS8xm45HPmZ3Fdz7iQ4yXqn7YKvO2p-BOhZEFwMq1JIKv8J6d_ovPV_S_BtpxJownrAtcB8MFn55CU_Ir5EnTkGHN55voJXw0xn-qkOe3s2cBFVelXnZDBuE4QOmmpI-3Z-EeCOKi1X-aqKPnHW_joiS7bsVdPvzkhqx2e-UYQUiwE_eZIA2Tz1UeOvulS3RxnkQY5OlVWu1eraUqA7-9qO5Plt4mg5m2SyFeuvAY3ZeDWhxCNF-5U2ONmSO_BkBv-A-kK-zQ__UnJP-0dc9_iz9NUGZeCu__jA1kd0MsyOf0-3HPp278xLZc9Ck7Y7mrzu53CEUCdYunZYikX3yvJaERWb573ovqAq1uzpbCE7m66BSZZmbXGZ10VVx-_xxcS4vES8vkabP7k2sGGG10bl84JbvWCp0l9hMEnpmA4KueAIOZze_znl70Qoc5QOL5thnv-6KBo4Z9GqUL00QSLpuEr6hCD0f8ZBfgXDZe99_2T_tWn0TMEy_biKOr0et6Lv93bjgIYiIFfJ-_h_dZmPQgR-bXnlGK9HXZ38HrhEQAkvbYCBuBLi6oSvOV2gP7k8qrE2HLTe1XlzsIK4ZE1wu8aXJfy2EIu99b6CcFmocD-VfOX3yIctXEGGY5Us86UopYNdO0yQ8a8Xsf5Djbpph1dexW1DTe3-vOklllfKqvdwlPvE-_hsPoW5EYKbdMBu6WBUZTcyqRQKv3r0e1BkVOwDZTJJ1iabOFhGXir6mkbMVmbOFaBxtqMkCmZevf_duWy75vJSeHIcFUF065N7OD1zPQIfhyaU9U-ddRbWFV2-4kszT03_mT87YEwdpnqsoIIkXvtK0VfQn10CUJwVFCusNtYgXFuEKjYEhRt392lt0gXh4GcfISd-rl3Kkjt8K3hT2eOtw4Tey_fDtTdfY_yzRHw_OJ2DOf2cY2E2GVwAS15EIfGkY6a-ThtGs4ktRVGtYIEMSBnrSy3u6uNXj_T-q_sGm0256I9uiE55XGVgnOzZ4Or2iYUsudmHQHfUnVsgFcRsFwkPcru7L9nhr2a7ULAAQN1wPSHxQcrfBOS0WT1BahVzF8KvkeFq_YNaEv9sH4jB2hZwi1Gcg03vNxxSVFwfIrtRgN7Nto-4qiWgFpE2RuLwuAm4cOs5U3c76KYKofYoE7LUuuhBuXRIrziBG3bAq5x4Hzi6YU8zoA1025PYY1AZfYt9dfACWaAqcUinhOrlzy5_IiYuL0BBqefujG4DAsyBn8JkM8qd7t0EJIGve1da5sGl6bmek8EzT16S3pXmw9zuSeSeBq7-j586bvOszbwF5mfL2hVqEWLlgs-4AHipG0Od0rAym_f2kbXIKKRHAC2UNPE1MPv5i9R_3kGeFHz4xFkgobIhLqsnJPrzBFh17cI0mzVu9QGPDARhd9RmGC5i3UIYA35AfCg_89dlx3EntXkSN9jLFw3vPaSqZo-dzO0pE4JMoQX3XUP5lHHmqDhzy6_t2yYCTgfPeHCZzFGutel1dd7s6h_PvmGumrU7xH1dTn1pPHSlK4xuAw3yXrE8EQIodJoK15fK1fhUPjX0YGtE0nHC4Hya6sJAzifjt48zFI6mYrHls_WPAdr4rBmMCJXxV_ZfcylBirVNDzVFNvvTNP-yl73rF_8EcuYxtUa0Q1nikmxVdYJsXpM8UPjpJvCZJFOL-XMQN_CMtZkQlmv921bwDCJCeWQRC16E3ysn9fOJC3XmzVWB8Lc2ZotBdIFyzbkGl6tqoq0xE8iP0jNxorGIV59F22_rg8vyOfNCpcg6vZ5383natCODaZukcs25ZjXUCP-vcSsnNAOpUoaAhr21Sa0Hltz6IRdtIs66wx_5PkHLg_Q1Gd7mboGlKvW-txuY_2Gj47Oqyky3O8q3jGrDz4xpmfV5HHdtxm1xZkKC4lJaMpiHB0iiEGYUxW3EFEn1o7YoT3Px-jXZnyimV7VbZfYJFErvqWI558a-Rr4-aE69RZJ6mDqrS7ZswFfLpw4ftDJIL7G8qylQIn6roPAPIF1UQ03AAkoetjkAppSI53k4MpPaNmV3wc1WfUjLgGptEN5jSter30T6bzWCVR9Haziet5MjMOKE_bFn-2YIE4vWnWPsVJGVURaPrJlrN4qptYI7I8yqzH31JRQWdXBj0DlfdRwIz9KPED6BNv8KRT5YZIZpJLK-OAIbRLlyNuYn4lxZKKBoii2n3CVZBDvIKiG5IM7-AFuw4Ok_Zpz9l_hRbv_z_Hl60Ku1JR_-Rrd-ZPqIzVExflSQ5Usw01HY7kAFXAmf_v1SxagIxn7GyrOUyyJkmhDpIegh0b3ETX3oQmukSe3rkTFmkZFAtGdxkUlH9FAlP0Bh1S9lbzqUuD-KMIj6y90T5i6Kybt2VGuIM5I9qTbXNYVHXOA-Xmpdsd9xDh_rfMFQq0SCLUNfio2LKcyXd3rRN1EdU_2q_KIztvj0Tz4D7p0FDxCpLB3kYVE2rW3Z4kJd9KuHpbuIp7STaacAN0QwKcFNezQqzfi4gQNG7LULpwxsC44WHuoV5G3bOvMSX12MsMNfbunAf5Us5azLsVunaJhLd4BfYVu1VqIiHZWbgVDznm2YwJ4fz-ybg3mMxp4YF7-WiB8CfxcjXsC-56GImp0zqCpFBmBcptx2w2byhuloDaU9iMPn1oXC_uMaqJBtiJsNMQQdW0FV39BAIvqFf_IVDvM2GJTIHV5n_EZvqxDNry_FdvyzVNHyV2IdWvRKD-gT8t-Kv8e4l2LjSisOVL6O49gUab8jQgggi6G72z1LRErZFUAV_aiX5DAZA8Sl9o3liCYvdt0ncTGNUCN7yMTL12g1ZFLNy6-agGt4z-U3RMIXlsI_I-4MOQJ7lMJM5s9mcyEUbGfRlh-2d2ES6UzONLXeK5fEYEK9jNa9sDlcnJtWhRhk2UCNgiaGpiIQBrtq1gcN0EMGcgjVRaNgbhjNcsa3kxtyR6fwlHbwe70Cv3J4NaVtEFwPt3gMBZuMo_b4B6-sBqSXAtFUh61aPIAGqmocZXZc40ESG1_OE4LP1ccpNBUffNTQYsRJUG676Q7t6kGJbWV9294UulKVDguK3DioTo91CEA48fp9VVbNPL40Yg3tI6vCY5r3kJPnF_hhaVREaPeht27uxddg9j0ATodhrLJgjuw2nXyPXZWDglt6DutakKCGC4O9ALxJ363qgxrw-AlDtxpTCLWGjO3dIcrGw90nnZs46QTbClm6bHQWNkSwxg1BX9u6JURR1Zr7SiRbxJPkLVrnJQa8rwhIl7VKg2o4urIUgOg6sehtZjf1Sc3yxaYZ4c8rjVhzex25Ev9IbUUfHnahjlmXw3XXS7pscn1AeTJ7fn2aBn26y-DdbG9xS6yYalg6sPcQ_1rYuhUNgpXVbSDLKCbtFw293sYBjuPC3SiFGuNzb2s57D82OM3aOet-Y04LEqJkMarpf8KNL3wzSHTqcxkwTgZ0z_1tUJabX-WO4-Vr7uicBwA1IokTRIGVJEMkglpbmpabTSRKIsbhyJH5hLx-o9pX1zJDFvnPFAWzN0tXhBPvKBknJaFMQOXZnhgWbRDdp8UtSMfwKNsETnngzgJ-8ujcgNhNg83lVQJ_zZvOD-fwe_8_XE7MMcF1ZcFfmaFrcp-EhwD_pzAkJWUhrTC9RXc_hTQp3UAYhIesCLIh3-ec-wFDcEnbdLP4IbDNKlA4wEitrG72d8DaLvWm1QO6L0CQFcA0ndLF0d7ee34FNjRITIRZXxTr8K67HSC5Y8lSj_cfSth9XpSHGDoCqFhregn4gSZaJj9VVQDKqzZJZY7uq8uG25yzPGasTnbpptSUSkbL-odm5tx5uJva-m-ktXbgkghAnxNjfmD6TyiFuQujFoClokq-dJfs4pp44NhUEgKVAwz-NazPgf6GOY_CYtFaquxXVVYLhTDEX_pZh72Ti17oGzbbTcrjCJNAtyC-iowcVfl1_BPnwPk_fXoakiG6r1aNZeJsWaFlBjEsVqyxT7NovhGjeeoVLWRHf-vpV24s7yZ5hUErNn5-9IOPVd9Kt-Qb_vuoYwhU8elAtXABohu2z-xOwzkYfzcIjYFhchpGJCpZuBrfHxPVY3KRyaX9l_15c15d2HaSOA_4ym_uifSBjN_d0b96C8RvN6VTfcUSLWtDKtb2lx2pZwHQtouOTexmKgUwpbjT3BTRPUz7mn_GkJbA6HwNCil-xNgmft2BVd6SIRrr5TVTqxqjgceBrmQ_Gsn-dkwijG4iwuXRDqBDCRI3-ymSJ_uD0GtTMpPmHzdxCcr3-n3fuD7A9KHMcTZmyUZuMFm6YJYZzmxGUuGp7O3CqtRYeNC0BzKfm6UK6fEOlaqBoqvk2_TnpcGFc_ESxNDhfBJValI-MOvV7m_cB7HkMomf6i33FwBWlnke00XFIStHl7bBUXFgU2FrJOss5v03y_p4kiGlW5UW0ZhLqkrbYFE0lA-M23dw-4EuDCBtg6XVxeYrMt3DKNQ6nV5stSiyTuvcjhFGVh_oYmxWEwQG9HJjdRrwOS_o2agtKZlSr7VZPSZMGTRXCkHkTYeVM8-x3Akz9cBtMfDN-hDZudtyy9GpXvSy3dVo6RVjnwFYAY4KodDUGR9RrCg9iD3dbXzBGFw5VNnlvGFTsDuQ4v9rBcy8Rb_4uKaE_NQg7Y8TutFOnEEuW8ofEoMQt9mw1RZUw5jd1fjjlks7tOYgTJBUu-Y6gGVRMOS9tm6AW7G7pQtjtcv0O5Bea8DY_zZtfZxiS66_vlrd_WIBQk_qwpj8AKhXqdsaR5cCl59luZxKGQ9t82zXDtEWi_RHMFbILnK3ThY3TWlqRV_Htk4j-NrpQAIQ3xmdQyV9M-ZovRXhAk7RVCtXhm5uZ0Em8mMyFF1TJ3ich_gXFIPCJDzUa4nD0vUFrBZZN3d8sgHnV1oVN5wU_Fl7d-fnHO9t8siTYNC1n2USb7n7g8mWEquEg3_ITU-HCoYq8nY-SfuFPQImDKb4lXhWEtcU1M8S9QnUGtuZV8gRGjpjSaCgCoRikJWswgO0zAsi8CP8UM9o51VDufiRsUGgg135_ob1ZlZz3M2if69joKqAsEwM09hMwKAGAsxQ0qu1meAMMeB5j6TrGbt_39azbmMrhjRttioDvSwzFmSnvQwwir4FUA3S3oTFgrcX1oZj59BGJapYXvDpb7D8_RBmPO6TqsAv0clmremVJbwSs9UbUKJDX3j_ezMGPKL-0rYdwFQ-KzG2EJBaYBg_I3PbgLH35kah85eCBISSmtStbBoxCMSU94yTfTKqxuQXpD4IByn575LUbAum3SBKivXaXQeBbHNDMI7gGZHqlyYAbnilH-H_XSeDrGhH84JQ2a7hNhWLhG6qLhlkZzLghCBmiYELArW3OAqu3UAqzZqlc5PuB7W6hgHU0R81icvQM05I00tUCBs2v09f1QW3q3ehSS3ozEaB3qZIW0zBhGyJ5xq3dDvma7LYpwAo1B7vjDvlLbjZRb0AvTQMxl99zToPXrC7hDyuapKZ-0JTvhsgb0ko5I0Be7K0Su0oS1Nt2b0Dq1hT7aDedQOEW3Dy1VMAKmzEAqOkZDoNJgy0rG0DtmwzucJZD7J8mOgMZTOwpdc0j8BZ06S5nGQbztxV5Py6d_xhW5UGN22j0Ba0Mb7feD9_MUoAK8FGgZozqAoeMAZqtZoxmjF2iqBJ2yqAYmLBoFMkVpfaGgvn_TF1CtgtwugJYLFFhQGhJ4wO75TUhaWD8EM-dt65R1ooLSL6yLgroWSbnqKBAr9zIAssAWYhrKYD0hKYr8kG-_w-5IKJfRxB0LUSN3YjokG1qZhG1KArzhind5Q0UW7gkRTjXSaDqmgH1T6w_oCGeXKU6vx-MznMW7I0Lc0iOCRkO5jHCb1QLu6E8RGYOoFZlajg1KU0uxoxQeL6GjDxSTMAZuRFgZSMBkjnglt8PAkXSS6oJXhSm3ZkPrgHgP5PGR50iMzyMb6Hg8pT8RN2v8Pfv9gf5JOtR2j4DaG3zrMj22c4kjTThSW8P5RlnuMAutgh5PWQcFxRiLQCB8nMR1pOzD_9jM9KOxI2Oe7IlH1gnKNWuhuNQiL6OMFVTFMdnYLw59ymHX8aRI-BeaZEq-EZpu_Y9qz-_EGs1ydRpJu726_u3YLlCMJi7r_yzEI9hA9j0HPoDRysyjhZIEq-yoLcdvuI7yUPADgeF-sJA1gu-ydY9YwPWwd5xhSuFi97SUT0yVXON6zcbcrYqCiiSbpj4i1_xCwX-DZYBQfi76NcQujBrOIAlGLgteZwhaYOB91zEKn7fgR2fkz1zUO1PQjxHAXayhq3ojNVakEoFKIRgfdd1E7KYvPaqmIgysWTANJcTQL5E1hN_BdOriyMusPQpA4FxTHUJ0Pt7zd4j7sw62zb0yU-KzPQZAVMByOOjrKNuzxVqstcVVqwX7Upq2u-ZjaU9WZaYF6cTDFUNQ-LcM94dTih9exawjtZwXPO-yb6bfzJKdJUcIL1vz1fuRrOPHl3kn0Svh7idFWaFIQhS66cxfM3yN5rQhxrmCdSkbuYg9aTztT8fyqittHAb3cfNOO5VOz4_ZVgQeHpRbcq2gjOrOejVaMT8_83M7ZSI23c8ABMHlqPhZnB64TEHnWJlPHLKZZdAE43prPELFJXPXxTZlUdCx1Sl51EVPglMBYrFPH7JywsRGNvivhQpIVkp7GAPgxoUaBfE58xq4YxJYwlRBNrQECgP57VYSpBJlpsacYjFUtSRW7k8_nSd4B9EoLK8DUFDXNBPHXrbsfts6M3rHUHGdUw-fct3jSSdZKFPXAFva_tQygRc50Hqm9vtpHVEdQ0CPHXc3e81GlJvqLjEhxKDDeTjLV4JQKF9MgtkXBMREK5hPJJB4s7ksDEnQ3Ij7cl1Gncen_ottm9qqidqPMs2r28grXKiaDvEWqDOyBMkBDL_mgcrbs08SOsKV8Qmutwx_eTKTHbaUp6xqZKYOFwC-qx9qruaJsFkj7LHVYRnniO_Cr7FVpjKwA3vaMkzri_sdfKEzHMyU9YDF5ilMpJAMfvpmvVLT5pHzGgc7vZWAg0lEfWmLGSa53rCtscAAkVzIaY6_EnYPnPLNysHRBcRE1w4gr-LGqKJj7UoA5yBADeBctMqfmkPubJk4KOfgrwvYePj4DDb0O-wpo9mhtltdZ5fXksc4hVRCEDtfIHdgto9skT0cpbkP3vjE8HJj-alT5uiCWw73J47bjl7iUSeYo35ARVtc_Q7v77EN-JXW7xV4LZ8EYSpxNfFcpUV1WHcrFwN89QLMhtHGvQxbFivWZKQFAs9UW0gDBjvVaFjbbuQysMfpb8eaU7rCED3jS1f71o7-Ewa8es6i66UaoVGYman507yUiP2mdIrn9ErlgPP8iqGTOIz6c3sVm3_Ed2nJLodgYrzDIY9RtfdN0iACcWfNBBCc2evaepsaIsBEOBYqlY4e5jd5950qU9pP30QIlCHptzTKafUd1myZKETLfgj1eqrqt5HQLm3vPrgGMxTSpEuXaaR9JwfPAco-Gd0puWppRx_ChYMeSWhFfHcpyFpxdhuKTSbMsQ_zemf2D4lc2idTY5em7tMVBqnkewSaHBhayokBNnyV7rIGEHSctnnoHHnpFo6tToVYw_DlIgVR-Cl-gVf9-3CZUWHHfYzywmrbHcPllgJ5hLYZe1dLcneDeIN7HYGifosELH4_DP8WIrwSIY8WtewMbf5T2h27NpqcmQbvuqXSR8cQQOngjsy7NgofkVHC-T1gWAzOLHKNLEnMGZfOGMD6mhn2RGDbEJBMNTg4-Ys-9R4Qp13QNa78PgggGxteHpq8ufqaZlsz89D6j_f1H_D_6E550HYTfnVAcrshJwVtY5zdo-pkN5VSmTwuddI-33U1w_VcopsMZsBI4mUNIN3qpXvV8QMpvThr7jYvYtfu8IjKZ9UX9CkpsPfI5Acw9WQir-tgetLhiz1CL87UensLGmyRffALpBsnPScbKlxTLNQYtEitV5bbC6P-seQh_bTWfmyS_Ole1WheQgLImqNTEtymQgdBrJMk8dYpPI3UtdaHenbjXN8CZNAygQyfdqxKc7piESRCpZ8zUnbtmdrXJdWF4t-rJir1DPB5evEQ4CNCvhpQBhkOPIzmkDABXy0YAX_5rBgHItxbAgARJj1JqjRUD9E6rNbhWYtaqIXr8VQWMy0Egzat75ecSU7ih576qKQNsm2QNN7eKYLhHJg9RkiwOqcA0zjcxQ4NOIBOVHRCdEjfYk0mWQ3dVMWwZUTJvpUyfTHQQxHM8IY-QLElJuVKE_3ZNK9IFRP_5H_Bl6at3ku5cFaRKdnipFwDc3wa_QuPqod-rQUaJBcvkzcuDr5kqlmaPVityW5bwuBSlIz99OoDQsUSZD8MWjeBR39JC86_dXHKdtXBp_XVYEbFLoRBEyKxYeF4QmLO-RecKPUIcrS_VUL7r8VBO61kX45R2K_ilYv7jJ8SkoCG9NrpjAocpNyJ4tvkEElsO-TpjB3i5xs0tK7Dy5EIY4QvVYL6kLYzb3jUZGcyRWRZZMpB5bBx_gtp0bQZgduUwDL5Fx2_FV8pMLHSzceRb6n0ndB-IcPFhsWsaTp5vYFTtOLF7ru2QsnL4DsqcHJwM7-pFWS-6lWvXcVdEge5e6i8ds-tTZ5KUi8eBITSbKue3ayxmigaTGftwR8b87EwTIKTyMHV-4E2sBFFf_
\ No newline at end of file
+xLtjSzqcrl-klqBhViWsSxDFNxRFp7hQpZgEdMOsSJosqvrjf-F14jQbaU0k8ATkd_xlpm1w0OGaa7ITfDKd-o8uv_3ZS81pUFaduu3oiiYZE7uKWpk2qdX7oKqE2nQ_GtmR0vpbC8qJIjwn-1P27iKnUulm3b1Gn3d2aD_lO4metnBZKv83nkBVeZYEuwU4ff0UavpG-9Cll_pVetzyfgLVRL7o5aF6uY-YEFvTYd8iIaCMnHMTeuIZEyJlBs22TWXYpdwBuiSnIkF3664ECqZZm_Zh36980OVfjr6SKO3v2pF3nSdfoSN9sV79PNpmN7nd1tz3QHGJleNq17F4xmTAl6vo8LWLV1o3Hh8TeKFyZeoCJVc4GY7m4OyFOuuAo3WeTWfxyJF-bM0O7yQOlFlhfo9_kKlzy_yUHJP-WldHRaV97QJZ8CwlNyaW7IZrXa7wC5Z4ASpXMEueneIBCXp-mgEOSS1B5X_649T57vh79C_13Q60apyNeZrudA8SFWGEjoY70wMa624u-DQ_hzxF2SXF4SpJ2yXs1BC980WIta28p4q5CmBoArZiSi2X5EASac8tQF_S9VO3MKmhJAgkzUDFGoXUG4PAcZmeWBJYEL2w4Mi_42dYiYbgawDW4l_9dzS3K5qRnN-BOWmAXRjCZoIdMoeIAvB-rBx-loTFXfgfloN7Mv1Gb24Cyf6MCzfghkN80dWjsyP9JXYyBDaEulIOOD4Lci56_tO10IEu7dXYYDDdG8vBWaaK8oPh1lFxanHnAAfoGqWEWIXo2t8cyrnm3dQW8I8KeXtPPCirtvg1xWxGP0leNx_uwgjFDPsfo_LqsjStFqCfq34fwHJ3rn3mRiocHDjIalS0WKguzpasDb9F6oINWEb36pOR2QTR_ILW-GZjVnUvpY2Wct-UYpySd5DoXrAOzem3OrGTWq6to4XJNv4TIjvFEtF1Ug2y4kszT03_mT87Y6xLvmwRP1BN_Ixg07qjV0Y6F9zF7cUR6XuheR-35BPZQ-ymoSgT0EfAU49gKhA_DJorRZSob4wtWk4D-X7Ql7vJTtByyb_7pUSt3APH3D8KSKGmoCy85-waHEc2MEPZLu76Nhfjexz97B64uwsR0yRVA0oVtlijzqCS0nHcYE31XXSP7gWNFuw5Dmh9djYAyqMaQNZbzwlwcDd_h6PkUXrGVgxHIJd8ar8iWzDBmzzJQqjhEGG8X5wGlEtdASpH7QJtho3VahGZMbfIm5N5KfYW0kH__NBp-QSkTMsNnjtvVY6KHcRvcH5y8zG3PoN8R2n4uHnc854gOyhYr7cDAowB6qXVtvu3ob62Ti8_sZP85Ez01WL2i193f1WvRKbsb6GK5ABDN9zfRNox7_vRGiQb4bYMLlYh3MXOULCW9dJ7QZpvWNDe8ia1pI0_9plHu4B17Upb5C7vWGE7_ealTuZo4kJFAcLmQMbZxtL_eL2bS4klK_2s-eQRiZ43P70n8CNRxrn85qigoYWQ1TYnApAmohC8rlBVWHobfuE8VLzbcMhrgYMsgREFHJ_OWGnm-1g_WZI3ndATqnBkoPcjWLmK9KQ9LFcN9FDTVIPMkS75jPdr0rflTaJpg3cj1vp18R8EHktBDI_ofO23rkw7rztF8JFQgMQvJWpNq-Du9mPxnjbh_6QN4-1qN-stGvpSGSoLNJn7EU5dGCqVZY7caCfsyb0IQ54PQ7gROGCbDZWFK315V91ea2dRcTkuXSxqXC0kKhrjusUez15HcxM8mTcxuiTlBYvEpgwkhrwyEhcyEdfrxkf_v1qs4NTFfmEWSR3jJlXs9B4hrY3cRSuzJOumsPVeLsZMvYsyTpH_xP8GClHeY1b53JPXmXqV6cDDh4d0uC3LCry8YH9yR57k7E6tJq7ojj4l0-tWA6O8LEylKadmKRqWlD2zElABbJ8xgXaQnmo1S9z-6pT8-hXiWXKwOdlvVEP6DiTYcSxefKYyH0NA0KNy_ZabvSulX1ck_nQRaQ9MxZsvuk0lILwcC7rnUqRuKLeWx6ZatmV06WVgDZJGEsqMlYmkohX_1zWBB5UGe2VUsl5WMM39GV1u2t3cOGz2nfEbezdJnnxRNuRXkqktnvpaQS-NnYcaIFJD5kK7wPxeUcCCt5q3XsUFhejw3atXffQceaEOiwoT555RB9c11E-D1g2it8wgtAselX6Ws2BSiY6xsXbMEmChNQzIfRlFYMiRqwjXs3Ewn6Re4usQt4RXKzMQLUZWFnMFZoA8umHcR63NH_EHmisetiddQfhn9jb7UAJ9XmXkeWNracWBs5Rlz9MaPwRZH4z-HL9qYnPhH1blgk818orjs-A_8CPA-CsZXEHbWMEv8Jmba8czj741UBAD2vB2NDKZ_iitHl57Ods8Kq05wwyJiKdyy9Fqgxz_-Ta_lXNvl_uVcVQjsSiyzdzFxbFA5tOEACGyPBOIikFyGvgGLv9yZu0UrV9OHNQtfPrIKbmMW7AsXP1VSN2U0wolh8PtfL7k3kVFtQqcaJ-JuQsN2BzTzl20VrDchHZ10tgOVrFATm5tEKXWKOLDPpk1dKGNSWdlqzbho-tP_5PDnxIX3nYgqjFdGIgbIY6SFLzS4wVFxsbRHRtVcq1tqGqVJlUtxZDKiEw9yrpQ0ECIvASbJX7ENX7CjnsIIOfS1hfIOzUdrhJscmIffT3jTPNVBdSmGI17Z9yK0ELZcPo449QnPpbh6PL8psnjhQhs0eo9rCdc54hV1Z0mMn5x5Ov9wYpl640qFIRbxhsudCUyGOR_vm_aT9REDhVrSAoMWanY6APxMXVw-SVysKyOFSNSwxoyoL5Yx6ZS0WhJVoDfjCnJRYza3EePuC0tlgIYLgV30VyNJQPWq8r0KdmOFthnxEJiwiNfYvDdZmuF3-HYqPBwMfKvy4T9xbV0jwGDlHbhCmeJGjDZKgejJrKMbcnX0RgAhLNMmplglr528JsASlXc0_8EYuZy4HTBJNtxLtmOVrRvfnB8M7y6_qQQsa1yTphOHXRwI_Qx5DuYZ7Ilc8m9GmnkSjYpZ-6MxouXvmXdHXch2miDIii9dZwXDgy0UziKt07UgkkEC7wkcWpXJEm8qqLla7e5K0wfkJVcHWHskNilcZQmsvlJfWRVcw47Yec1N8ROk-mUnp-J6yV6niTYiv_mzCsUor78ofmt-njPcGZaD8Ef8uOvn00dq0SsJb6MGTfivcrowPtMejbqNi1XHkYz1lc4pKFIVwGxWFs7ZKi5mssPEv4W67524SxagVtBiYW0HT1x93SMnAwXN5ku7_pjG2jpeDOATmY-Uv6w2RJ2NSXyDPMwr47G-2FT4CT1kT_u1faypmy2nYWXnMkQ0On-jVSMVxNvk_Tq31P4RU-vLCSNEcICSGzXnccP39UWqWBKks379-WIeMT1Y_csGK-HN75vJnjlLVvnJQa8rwhIb3jeL1R2SIhFLKP3RKMvWtQG71Y_EzAeS5bDhIv_4BQjHrAgea3rA6DvdZy47ISCZgz-4q8fbFhu38Bq9S9mll-iKc0Fxer7KZzEErEJlqDC_FQQAivNvN0LLFBT3sZY0zhYMqEcXkM7eS9-ofP23cc1C30YjKP_n82AyoDthQOvWiOBAf_Uk0jEcxkwjY_7zt2zl2Ob--WO4zVr7uicBwA1IokTRIGVJEKkgtZcmqrAwWYgbzBMuccAMXN-o2R33wYRVpYpk5Txk1h2MMtpe7PZNOQion37YdL1AsUlcFTluzJoeggEjnzgzgB-8_DcgNnNg83lVQH_UX-i6tM-KVaVmd1hhR5Wnx5quQ7wpHzdhgFNP-cfJWUhTQOJt39-1x5d6yH5MrKSSQXs7zHDzaTRCTdREYsFbAQkfEG9qVTlgkk5EGV9jZ1X2WmDg0OqveW16jUz9yIXWyOwU5bBrrcCxz_MXJWT5dKN82vYtsPLv_II3syXWBZfeVNhHLc9KX79dQI_-Z6gfh6dNLBmeLqm4BvxoX9ixZRcddhkS-jLMnNu2x_aS1ToE0XNPOQQhihoSHqAwe3HFH14kEj0q4X1UHYeayTkWeUuOCzRf_HBvzNFgobRDH9Ix8HvSOvysYMz3x-pTThTaN-S7QuJZab-A6VGhgm3g4RwUpY5riNKxp2Otp4k_V3Nb6jMbpYUEkCvaj1UiE_JPThXy6bNiau-d7QBAEF7bK7qoVi2dul31t9pwtZkCy8lnIB3JuwgRbxwxocpoEejiiZiBR78x2jmxuczlYDwcYrfExAkoGkjn3okvPjseVkvsBCXBvpqSr-46N6MaC41zb9y_u2VWZbNvtVE8MK4OvZFdQpRz8oxhA5vPqze3JldIAhzxO9Xvu8lTUVhlDt5yFPMsVjHzmTq7evIXiUbpB9_kpxiABn5lpXkhBvr9TVTqtqigceBrm6_VHn-dZvPQnrOrWMuReIQIse7pvavllm70XfwjcpW1RFtPTg6pY7ImOkNIuYiD77Wuz7XeVWD4d63xnsWzmXdEW2Pf-t4GxS4NgWxWayeDHOnVOSMbbtS5kxBdiaEDhSzsxVQI6cValI-sOvVNnxcJ7GUMomf6i33twFWVnge00XF2StHl2abPnFgyYDLfqRR2yY1URjYdUBdm2jG0PrgwPvbYDk2l1-U2BaO-KDuKiBt8cbVxeYrHt3DKNQ6nV6UkfTvxXpDxCb2MtyrBzh3t365BA5ev-pL2yyiBw6SLcfNd_eUl0mvEiWwN8QccvtCUzRZBiFgh8aulTR4bVxic7XVJZpbHk8bBykTV0DkMtbKN2Q4GdaBB4YsYrevuHRwt9h74f1_OL_kMys1hktP6XEITPvs26vVnE5H3lrEgkuZ7EDpsSJZk82CgHSdcjYSkYEuFkXRHmQROxvjnQv5rZeOxt5rmzHhxwohXky0HK1E1ysZxTxJWC2Wq3u7nVwnxqrz5UB2yvht7_aJBAgpqw_l8AKeXKltaR9bCLAaFiIzhsj6xq1UmcxaG6Vjex6oK5SL0qwuW_O8z4t_nT_XBVbzSwMaMW-y9sl7oLlecijnrrB2j_cQgs43y1W7O4iAUB7dbvgsJ3u4H7jCc5Y-lIIPMWOkxwbn-xboaBL1u_WmFhguEFhFZp_Luue4xrhcEX3t0eWg79LyIwZq83jk3gWhwNeLoUaKsX8CpvptGnaXR4qI4Qy6-4xUPu4OHmchzv1VYpd5TI5UVhacbHaJzbnS6tLT0FfMPX1ZfBnFEOgBPlFDjMnovLI8ul-M84Vy_XemLbAnj-Gw1MnxGm1Dwt0Xo1KtxGEd0651Ist1OjgtEg2kLqD6psL2hQlrgpXpVRbpNzT1J4rNtT6e1xnGxeUZPzMgq0EKTWb9QCSdySFHASevf7vH-BB0pcMnt84r-AR63qSlZ-odr3oyPc8wVwtba6b5VWrO9-dclfFK0Zaov8YylqWsPQbKGoRfEo1Q3Ar9E8RlhYbvTsskF4YUEbAhPTurGvcZ9LwQcpYgk2XTOHg4gMSpoGfL5ohBcxD2r8LewFMH5IytNWxF_mgL6weLea68j1s2rgjnAre3QArstH-hrLY5uMLxAbUm1i1QSHh4QUrxNZ2jy5Zm33nOlG9a0sIzjB02f00QV7zw1Sa5qWfG1w1rLUA0vTcpbnwGf04LIwsAYMVW0KwlESWwlcUHMOBOLAstpkfep3LAeJpQqjq-gRuxut3gWlLhPx1c19-WjxvNbN91jW9a0RGEO0vG1XxgNW6g0LgzssC8hSlK0N267wXUSUh1QSN8XP7xIUeLe1gWuRUZLvnid8OkELZHij5QnpaFa5Pm7E2ieDYWzBxlk-BJuDF_7F0AaWka5I0NG2ig7LJw7ujT4MeGMfMd5veL5GkLtfTdLtXQU5PeUg5POR5W2NdUzK-7JCWphdywU4RVTbrnCd7gUPLqXSa9a-F9onMf0QJSxpEka8r3jghOA5whLlcWv7XeOKMg3wdLLWLXjHgfKQ1MX7eHSh_Vjw9aejJtEE2AamkdLPayG5e7ka2erdutXjCAK2z0FU-shJ2vOLeXiY1Qpty4GhJ2OyFJ_uhR2f0Eq0gC1SpO6smhIWPAwoxGCOHMP6nat6z9hU08S9nlLwtGI6Xw7wvQyT7mMVN6mcMzJhKVcMmrD2vOraM36nZ7-qmhCZKoAoWMAFQBJoig4fMnDwHMcDmmZJmpjSBcXcs5o8PeuBsg5I65KFUvgnKPWSmw-t6XudZUQWMc1gR_TQmL8mkZ5Hi7DlrlSgqOrHYje5YWT1-4Md4HkBZVXLgnKTZOBrtzgN697iL7Z944ITlBeYYIw_JuuFE3-CcJ7p-voK7oThFB0S8RlaN9MmmP--QBdvuyaLcq8y12hkRNHZvx7AdJPxx4xDjJuiCu4qaRzQVz4aK3RpzPtCGZSp2rsFqMnmVu8EvSA1uVYkVDpBHD37gPHOwdNGBuZxqv51zQl4MMZIFDBBQuL8shs5kWfaJKzqJ2P87ins4wCZKPDdqVgf9xbAtk4Q6IoN9xbAk_9STbUuWsLIlF2SAfWfPaqmIgusWTANJcTQP5E1hF_BdOrjyMusPQpA6ATd8l9eExZsnYsdvTZ9UoWMFrbBaMuwdrYp76RTR5kFVtT9jvNxyEuTrij8iFOxQ32G8vOhn9tRHlrsjffXXHv_MAYIEvAdjOlG9hVzd8z5DAYawPMH9qBfqMinShRMFCBWI7UImxDpx96PELk31Jzyx1-B2wDT_wu6HkFIyHrCoEzplafCrCttHIb3cffmmBpZqI-T_eL2ESSykYLLZ5gbPiyIlgN94VmCBZHGAn1X6vDkhFSEPPmZXYEiAOwAciaiAvHWaVU6xbGaKVRkRHxSp-T0RM52-KKv_cAnPkRG_bqHEpRLj1_goMTlE9EoETWfahlLvGEgxK3ZJIhflBArjjVTgu4bdKToFpiWk_dIHQQq-xjbk0-mW_5wTGyau95SZrfqt5SXb6tQtQ7NRPOFP5fD0dNVtCsmdN79ur3sOIZ-OlzslAcvXGuTC2UTyoNqxi06Cemp1r40eMfcyMjUdCfQRHxgYz8cugVIXHkzMLi6OhBsYbd6PfEjmTSoe6bQRDUorWC1lzal-c9anFdKHNsYv08QvyKSaMvUaqD8m5MpEThVfNCBNk1WmnjecKrnXkr7zNxuYYBfDaDtv7eeuVr9zftprfOXRPywdhNLs4ltczWS7V_DJ3txubEcXTv2hVpgEjJ_M4safUN8p6dwsdRJf9BU_PxglAU-xmMWNJRmnGvP1N7IoOYWCIgdwcpnLbzRE-HMHZVZQHCxFgtwP8rjmDN2-YrQ-gWS8fsWSvr1pBI9fBcxKqfyjPuXJU8WpJLZrp5GpQeiPB0vztlaJXi-zUVyMcwxQOIjzimutUb94UhV8dQzq2RCMvaFcruX5EtwIzqNYmoBeSDCGUMoKFOqvHba4AqwVtc_P7v77FtwHXmBvVqHW8ElTpRReVcv8F8s9pIvzBK2jBFNsHWvRx5hiv0ZMQVDiIT01KwVRo_0VRBBorPilJN2JHeqFguqQ7QmzIEBcFSLr8HHkDO4CzfayX5X9Yg8FuzOo51Ebh2ITh_KwoHPeWQmdwjC5i_W7pwSB5DNBUgBNqqg8blUbTS2meoQ2bSiioOAZcIZFQHBOiPe759N69m3PEgQA1gyHcIE2q5UQddlwwf1GzE3Zv6fSwBJLQ3Phh9kEYKhW7ophLWjswPcVn218sKlr2ITDbyfC1xv5d6tq-ixZMBWWhVzJ6pyDZxhhx4LTfc-Q_rWtfS94lw6idTkAemtsMFBs-keuSqPBxamnkhNzyV7_IWATSs_mnITJnYFp67PoVowzrlMhrb_vNlKhwFIXp7Lga8NRlT6jDPMQshywaLQseuz2PvGkgBS5vnoUaB8UT7ZK-doLSK9kEBohIW9uUbbOElOfGDoxDfd7f2LDeZ9nXYccywWc-JbtDvYVHSuS1gaHzOrHKtLCnsKWfeGKDsmgn2RJDq6HBMJVga-Ys-5O4Qt13AJc7ePegwOutuHoqQueqqhisTGBDUZvIYh-RU4iAg8X4xJY-r5vjMlq_l4DxFb_dzk8-vexznFCby64yZry_jjbiDFks49Wykji7fl1opHfRFjrPepeNCUzC1ILgavBr9PXsUpDBGvGsIS7KclEyLM-iJdi8Yf6wr6EogM2Yk6afNDlR5joQLM_jrNTgBSwpTyUMKmPdxgXgl-LE2d3npzYvW62kXgfLB3HTqu_p1ggSVL9QuYUJDb8DxU-H6Z6ME5SWoDSxofhqcVJjHOVEmrnipECZzx6NV2ls5EU0yJUxL9pK4r4iMaroGXcu9gyrY-xCDPIwdsj4mUNZ40dbxzfAfRXrjr9DeMrlw6bf6r_2QRkYn1NnRf0ubFfGA-05KEzTZ2iMFUlmK2xcQAD8wuDDABtsAXInevr2jNMVDQd11EsnTTEEi9DeEOncItQsnNHTGD1mkBC-HFMfyvd7K-ugCjqZ4fDOD4Lgr-Ft3VtQrL2dZ1oVnGVnxnfFmhc3Ppr6rZCnPbQZvmogFsd7x9Jv7DVIFLhUtEpJ7AopQNyHDlgU_JsnyC9jMPRcB8c576sRXzmSWjO6QjjTCeEm4HzUb7IFo_lVY6z9MM_7DjCxYOlAOpXRTRyfMfQHbr8xJ-zDrOV4vsXWW4vK3aivVrcSV6yeP5asXkFgQaj9cMqxSyoD-RapNpE_koTB3i5xs0ta73y5EIY4wvJYL6kLYzL3jUdGcyRWQJZMhB5bBx_iFp0bSZgNuUw3L5Fx2_C_8pMLHSzXeRb6n7HdBpHDoVJZ1zCwcBt5UhgngVphmKriYwCQjXCYdqeFzqV0vy9VEp7D-EPKGRK8OEFjz-_6oezOUGJbr2LJWGEIpvjbKTs3EhLkYaZjx9nAHTKjYlu9SLeMUVJ_
\ No newline at end of file
diff --git a/docs/logical_data_model.puml b/docs/logical_data_model.puml
index 4fb96e0e5c..d76624f3db 100644
--- a/docs/logical_data_model.puml
+++ b/docs/logical_data_model.puml
@@ -403,7 +403,7 @@ class GoalTemplates{
* updatedAt : timestamp with time zone
lastUsed : timestamp with time zone
source : varchar(255)
-!issue='column missing from model' standard: text
+!issue='column type does not match model: text != varchar(255)' standard : text
}
class Goals{
diff --git a/src/models/goalTemplate.js b/src/models/goalTemplate.js
index 9bddc4ed64..e009940308 100644
--- a/src/models/goalTemplate.js
+++ b/src/models/goalTemplate.js
@@ -73,6 +73,10 @@ export default (sequelize, DataTypes) => {
return this.source === null;
},
},
+ standard: {
+ allowNull: true,
+ type: DataTypes.STRING,
+ },
}, {
sequelize,
modelName: 'GoalTemplate',
From 4612aa077e8eb7f3f76f363ba53098697729ebaf Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Thu, 21 Nov 2024 09:19:06 -0500
Subject: [PATCH 021/198] push model changes again
---
docs/logical_data_model.encoded | 2 +-
docs/logical_data_model.puml | 2 +-
src/models/goalTemplate.js | 8 ++++----
3 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/docs/logical_data_model.encoded b/docs/logical_data_model.encoded
index b5984b107b..ab0b54ed12 100644
--- a/docs/logical_data_model.encoded
+++ b/docs/logical_data_model.encoded
@@ -1 +1 @@
-xLtjSzqcrl-klqBhViWsSxDFNxRFp7hQpZgEdMOsSJosqvrjf-F14jQbaU0k8ATkd_xlpm1w0OGaa7ITfDKd-o8uv_3ZS81pUFaduu3oiiYZE7uKWpk2qdX7oKqE2nQ_GtmR0vpbC8qJIjwn-1P27iKnUulm3b1Gn3d2aD_lO4metnBZKv83nkBVeZYEuwU4ff0UavpG-9Cll_pVetzyfgLVRL7o5aF6uY-YEFvTYd8iIaCMnHMTeuIZEyJlBs22TWXYpdwBuiSnIkF3664ECqZZm_Zh36980OVfjr6SKO3v2pF3nSdfoSN9sV79PNpmN7nd1tz3QHGJleNq17F4xmTAl6vo8LWLV1o3Hh8TeKFyZeoCJVc4GY7m4OyFOuuAo3WeTWfxyJF-bM0O7yQOlFlhfo9_kKlzy_yUHJP-WldHRaV97QJZ8CwlNyaW7IZrXa7wC5Z4ASpXMEueneIBCXp-mgEOSS1B5X_649T57vh79C_13Q60apyNeZrudA8SFWGEjoY70wMa624u-DQ_hzxF2SXF4SpJ2yXs1BC980WIta28p4q5CmBoArZiSi2X5EASac8tQF_S9VO3MKmhJAgkzUDFGoXUG4PAcZmeWBJYEL2w4Mi_42dYiYbgawDW4l_9dzS3K5qRnN-BOWmAXRjCZoIdMoeIAvB-rBx-loTFXfgfloN7Mv1Gb24Cyf6MCzfghkN80dWjsyP9JXYyBDaEulIOOD4Lci56_tO10IEu7dXYYDDdG8vBWaaK8oPh1lFxanHnAAfoGqWEWIXo2t8cyrnm3dQW8I8KeXtPPCirtvg1xWxGP0leNx_uwgjFDPsfo_LqsjStFqCfq34fwHJ3rn3mRiocHDjIalS0WKguzpasDb9F6oINWEb36pOR2QTR_ILW-GZjVnUvpY2Wct-UYpySd5DoXrAOzem3OrGTWq6to4XJNv4TIjvFEtF1Ug2y4kszT03_mT87Y6xLvmwRP1BN_Ixg07qjV0Y6F9zF7cUR6XuheR-35BPZQ-ymoSgT0EfAU49gKhA_DJorRZSob4wtWk4D-X7Ql7vJTtByyb_7pUSt3APH3D8KSKGmoCy85-waHEc2MEPZLu76Nhfjexz97B64uwsR0yRVA0oVtlijzqCS0nHcYE31XXSP7gWNFuw5Dmh9djYAyqMaQNZbzwlwcDd_h6PkUXrGVgxHIJd8ar8iWzDBmzzJQqjhEGG8X5wGlEtdASpH7QJtho3VahGZMbfIm5N5KfYW0kH__NBp-QSkTMsNnjtvVY6KHcRvcH5y8zG3PoN8R2n4uHnc854gOyhYr7cDAowB6qXVtvu3ob62Ti8_sZP85Ez01WL2i193f1WvRKbsb6GK5ABDN9zfRNox7_vRGiQb4bYMLlYh3MXOULCW9dJ7QZpvWNDe8ia1pI0_9plHu4B17Upb5C7vWGE7_ealTuZo4kJFAcLmQMbZxtL_eL2bS4klK_2s-eQRiZ43P70n8CNRxrn85qigoYWQ1TYnApAmohC8rlBVWHobfuE8VLzbcMhrgYMsgREFHJ_OWGnm-1g_WZI3ndATqnBkoPcjWLmK9KQ9LFcN9FDTVIPMkS75jPdr0rflTaJpg3cj1vp18R8EHktBDI_ofO23rkw7rztF8JFQgMQvJWpNq-Du9mPxnjbh_6QN4-1qN-stGvpSGSoLNJn7EU5dGCqVZY7caCfsyb0IQ54PQ7gROGCbDZWFK315V91ea2dRcTkuXSxqXC0kKhrjusUez15HcxM8mTcxuiTlBYvEpgwkhrwyEhcyEdfrxkf_v1qs4NTFfmEWSR3jJlXs9B4hrY3cRSuzJOumsPVeLsZMvYsyTpH_xP8GClHeY1b53JPXmXqV6cDDh4d0uC3LCry8YH9yR57k7E6tJq7ojj4l0-tWA6O8LEylKadmKRqWlD2zElABbJ8xgXaQnmo1S9z-6pT8-hXiWXKwOdlvVEP6DiTYcSxefKYyH0NA0KNy_ZabvSulX1ck_nQRaQ9MxZsvuk0lILwcC7rnUqRuKLeWx6ZatmV06WVgDZJGEsqMlYmkohX_1zWBB5UGe2VUsl5WMM39GV1u2t3cOGz2nfEbezdJnnxRNuRXkqktnvpaQS-NnYcaIFJD5kK7wPxeUcCCt5q3XsUFhejw3atXffQceaEOiwoT555RB9c11E-D1g2it8wgtAselX6Ws2BSiY6xsXbMEmChNQzIfRlFYMiRqwjXs3Ewn6Re4usQt4RXKzMQLUZWFnMFZoA8umHcR63NH_EHmisetiddQfhn9jb7UAJ9XmXkeWNracWBs5Rlz9MaPwRZH4z-HL9qYnPhH1blgk818orjs-A_8CPA-CsZXEHbWMEv8Jmba8czj741UBAD2vB2NDKZ_iitHl57Ods8Kq05wwyJiKdyy9Fqgxz_-Ta_lXNvl_uVcVQjsSiyzdzFxbFA5tOEACGyPBOIikFyGvgGLv9yZu0UrV9OHNQtfPrIKbmMW7AsXP1VSN2U0wolh8PtfL7k3kVFtQqcaJ-JuQsN2BzTzl20VrDchHZ10tgOVrFATm5tEKXWKOLDPpk1dKGNSWdlqzbho-tP_5PDnxIX3nYgqjFdGIgbIY6SFLzS4wVFxsbRHRtVcq1tqGqVJlUtxZDKiEw9yrpQ0ECIvASbJX7ENX7CjnsIIOfS1hfIOzUdrhJscmIffT3jTPNVBdSmGI17Z9yK0ELZcPo449QnPpbh6PL8psnjhQhs0eo9rCdc54hV1Z0mMn5x5Ov9wYpl640qFIRbxhsudCUyGOR_vm_aT9REDhVrSAoMWanY6APxMXVw-SVysKyOFSNSwxoyoL5Yx6ZS0WhJVoDfjCnJRYza3EePuC0tlgIYLgV30VyNJQPWq8r0KdmOFthnxEJiwiNfYvDdZmuF3-HYqPBwMfKvy4T9xbV0jwGDlHbhCmeJGjDZKgejJrKMbcnX0RgAhLNMmplglr528JsASlXc0_8EYuZy4HTBJNtxLtmOVrRvfnB8M7y6_qQQsa1yTphOHXRwI_Qx5DuYZ7Ilc8m9GmnkSjYpZ-6MxouXvmXdHXch2miDIii9dZwXDgy0UziKt07UgkkEC7wkcWpXJEm8qqLla7e5K0wfkJVcHWHskNilcZQmsvlJfWRVcw47Yec1N8ROk-mUnp-J6yV6niTYiv_mzCsUor78ofmt-njPcGZaD8Ef8uOvn00dq0SsJb6MGTfivcrowPtMejbqNi1XHkYz1lc4pKFIVwGxWFs7ZKi5mssPEv4W67524SxagVtBiYW0HT1x93SMnAwXN5ku7_pjG2jpeDOATmY-Uv6w2RJ2NSXyDPMwr47G-2FT4CT1kT_u1faypmy2nYWXnMkQ0On-jVSMVxNvk_Tq31P4RU-vLCSNEcICSGzXnccP39UWqWBKks379-WIeMT1Y_csGK-HN75vJnjlLVvnJQa8rwhIb3jeL1R2SIhFLKP3RKMvWtQG71Y_EzAeS5bDhIv_4BQjHrAgea3rA6DvdZy47ISCZgz-4q8fbFhu38Bq9S9mll-iKc0Fxer7KZzEErEJlqDC_FQQAivNvN0LLFBT3sZY0zhYMqEcXkM7eS9-ofP23cc1C30YjKP_n82AyoDthQOvWiOBAf_Uk0jEcxkwjY_7zt2zl2Ob--WO4zVr7uicBwA1IokTRIGVJEKkgtZcmqrAwWYgbzBMuccAMXN-o2R33wYRVpYpk5Txk1h2MMtpe7PZNOQion37YdL1AsUlcFTluzJoeggEjnzgzgB-8_DcgNnNg83lVQH_UX-i6tM-KVaVmd1hhR5Wnx5quQ7wpHzdhgFNP-cfJWUhTQOJt39-1x5d6yH5MrKSSQXs7zHDzaTRCTdREYsFbAQkfEG9qVTlgkk5EGV9jZ1X2WmDg0OqveW16jUz9yIXWyOwU5bBrrcCxz_MXJWT5dKN82vYtsPLv_II3syXWBZfeVNhHLc9KX79dQI_-Z6gfh6dNLBmeLqm4BvxoX9ixZRcddhkS-jLMnNu2x_aS1ToE0XNPOQQhihoSHqAwe3HFH14kEj0q4X1UHYeayTkWeUuOCzRf_HBvzNFgobRDH9Ix8HvSOvysYMz3x-pTThTaN-S7QuJZab-A6VGhgm3g4RwUpY5riNKxp2Otp4k_V3Nb6jMbpYUEkCvaj1UiE_JPThXy6bNiau-d7QBAEF7bK7qoVi2dul31t9pwtZkCy8lnIB3JuwgRbxwxocpoEejiiZiBR78x2jmxuczlYDwcYrfExAkoGkjn3okvPjseVkvsBCXBvpqSr-46N6MaC41zb9y_u2VWZbNvtVE8MK4OvZFdQpRz8oxhA5vPqze3JldIAhzxO9Xvu8lTUVhlDt5yFPMsVjHzmTq7evIXiUbpB9_kpxiABn5lpXkhBvr9TVTqtqigceBrm6_VHn-dZvPQnrOrWMuReIQIse7pvavllm70XfwjcpW1RFtPTg6pY7ImOkNIuYiD77Wuz7XeVWD4d63xnsWzmXdEW2Pf-t4GxS4NgWxWayeDHOnVOSMbbtS5kxBdiaEDhSzsxVQI6cValI-sOvVNnxcJ7GUMomf6i33twFWVnge00XF2StHl2abPnFgyYDLfqRR2yY1URjYdUBdm2jG0PrgwPvbYDk2l1-U2BaO-KDuKiBt8cbVxeYrHt3DKNQ6nV6UkfTvxXpDxCb2MtyrBzh3t365BA5ev-pL2yyiBw6SLcfNd_eUl0mvEiWwN8QccvtCUzRZBiFgh8aulTR4bVxic7XVJZpbHk8bBykTV0DkMtbKN2Q4GdaBB4YsYrevuHRwt9h74f1_OL_kMys1hktP6XEITPvs26vVnE5H3lrEgkuZ7EDpsSJZk82CgHSdcjYSkYEuFkXRHmQROxvjnQv5rZeOxt5rmzHhxwohXky0HK1E1ysZxTxJWC2Wq3u7nVwnxqrz5UB2yvht7_aJBAgpqw_l8AKeXKltaR9bCLAaFiIzhsj6xq1UmcxaG6Vjex6oK5SL0qwuW_O8z4t_nT_XBVbzSwMaMW-y9sl7oLlecijnrrB2j_cQgs43y1W7O4iAUB7dbvgsJ3u4H7jCc5Y-lIIPMWOkxwbn-xboaBL1u_WmFhguEFhFZp_Luue4xrhcEX3t0eWg79LyIwZq83jk3gWhwNeLoUaKsX8CpvptGnaXR4qI4Qy6-4xUPu4OHmchzv1VYpd5TI5UVhacbHaJzbnS6tLT0FfMPX1ZfBnFEOgBPlFDjMnovLI8ul-M84Vy_XemLbAnj-Gw1MnxGm1Dwt0Xo1KtxGEd0651Ist1OjgtEg2kLqD6psL2hQlrgpXpVRbpNzT1J4rNtT6e1xnGxeUZPzMgq0EKTWb9QCSdySFHASevf7vH-BB0pcMnt84r-AR63qSlZ-odr3oyPc8wVwtba6b5VWrO9-dclfFK0Zaov8YylqWsPQbKGoRfEo1Q3Ar9E8RlhYbvTsskF4YUEbAhPTurGvcZ9LwQcpYgk2XTOHg4gMSpoGfL5ohBcxD2r8LewFMH5IytNWxF_mgL6weLea68j1s2rgjnAre3QArstH-hrLY5uMLxAbUm1i1QSHh4QUrxNZ2jy5Zm33nOlG9a0sIzjB02f00QV7zw1Sa5qWfG1w1rLUA0vTcpbnwGf04LIwsAYMVW0KwlESWwlcUHMOBOLAstpkfep3LAeJpQqjq-gRuxut3gWlLhPx1c19-WjxvNbN91jW9a0RGEO0vG1XxgNW6g0LgzssC8hSlK0N267wXUSUh1QSN8XP7xIUeLe1gWuRUZLvnid8OkELZHij5QnpaFa5Pm7E2ieDYWzBxlk-BJuDF_7F0AaWka5I0NG2ig7LJw7ujT4MeGMfMd5veL5GkLtfTdLtXQU5PeUg5POR5W2NdUzK-7JCWphdywU4RVTbrnCd7gUPLqXSa9a-F9onMf0QJSxpEka8r3jghOA5whLlcWv7XeOKMg3wdLLWLXjHgfKQ1MX7eHSh_Vjw9aejJtEE2AamkdLPayG5e7ka2erdutXjCAK2z0FU-shJ2vOLeXiY1Qpty4GhJ2OyFJ_uhR2f0Eq0gC1SpO6smhIWPAwoxGCOHMP6nat6z9hU08S9nlLwtGI6Xw7wvQyT7mMVN6mcMzJhKVcMmrD2vOraM36nZ7-qmhCZKoAoWMAFQBJoig4fMnDwHMcDmmZJmpjSBcXcs5o8PeuBsg5I65KFUvgnKPWSmw-t6XudZUQWMc1gR_TQmL8mkZ5Hi7DlrlSgqOrHYje5YWT1-4Md4HkBZVXLgnKTZOBrtzgN697iL7Z944ITlBeYYIw_JuuFE3-CcJ7p-voK7oThFB0S8RlaN9MmmP--QBdvuyaLcq8y12hkRNHZvx7AdJPxx4xDjJuiCu4qaRzQVz4aK3RpzPtCGZSp2rsFqMnmVu8EvSA1uVYkVDpBHD37gPHOwdNGBuZxqv51zQl4MMZIFDBBQuL8shs5kWfaJKzqJ2P87ins4wCZKPDdqVgf9xbAtk4Q6IoN9xbAk_9STbUuWsLIlF2SAfWfPaqmIgusWTANJcTQP5E1hF_BdOrjyMusPQpA6ATd8l9eExZsnYsdvTZ9UoWMFrbBaMuwdrYp76RTR5kFVtT9jvNxyEuTrij8iFOxQ32G8vOhn9tRHlrsjffXXHv_MAYIEvAdjOlG9hVzd8z5DAYawPMH9qBfqMinShRMFCBWI7UImxDpx96PELk31Jzyx1-B2wDT_wu6HkFIyHrCoEzplafCrCttHIb3cffmmBpZqI-T_eL2ESSykYLLZ5gbPiyIlgN94VmCBZHGAn1X6vDkhFSEPPmZXYEiAOwAciaiAvHWaVU6xbGaKVRkRHxSp-T0RM52-KKv_cAnPkRG_bqHEpRLj1_goMTlE9EoETWfahlLvGEgxK3ZJIhflBArjjVTgu4bdKToFpiWk_dIHQQq-xjbk0-mW_5wTGyau95SZrfqt5SXb6tQtQ7NRPOFP5fD0dNVtCsmdN79ur3sOIZ-OlzslAcvXGuTC2UTyoNqxi06Cemp1r40eMfcyMjUdCfQRHxgYz8cugVIXHkzMLi6OhBsYbd6PfEjmTSoe6bQRDUorWC1lzal-c9anFdKHNsYv08QvyKSaMvUaqD8m5MpEThVfNCBNk1WmnjecKrnXkr7zNxuYYBfDaDtv7eeuVr9zftprfOXRPywdhNLs4ltczWS7V_DJ3txubEcXTv2hVpgEjJ_M4safUN8p6dwsdRJf9BU_PxglAU-xmMWNJRmnGvP1N7IoOYWCIgdwcpnLbzRE-HMHZVZQHCxFgtwP8rjmDN2-YrQ-gWS8fsWSvr1pBI9fBcxKqfyjPuXJU8WpJLZrp5GpQeiPB0vztlaJXi-zUVyMcwxQOIjzimutUb94UhV8dQzq2RCMvaFcruX5EtwIzqNYmoBeSDCGUMoKFOqvHba4AqwVtc_P7v77FtwHXmBvVqHW8ElTpRReVcv8F8s9pIvzBK2jBFNsHWvRx5hiv0ZMQVDiIT01KwVRo_0VRBBorPilJN2JHeqFguqQ7QmzIEBcFSLr8HHkDO4CzfayX5X9Yg8FuzOo51Ebh2ITh_KwoHPeWQmdwjC5i_W7pwSB5DNBUgBNqqg8blUbTS2meoQ2bSiioOAZcIZFQHBOiPe759N69m3PEgQA1gyHcIE2q5UQddlwwf1GzE3Zv6fSwBJLQ3Phh9kEYKhW7ophLWjswPcVn218sKlr2ITDbyfC1xv5d6tq-ixZMBWWhVzJ6pyDZxhhx4LTfc-Q_rWtfS94lw6idTkAemtsMFBs-keuSqPBxamnkhNzyV7_IWATSs_mnITJnYFp67PoVowzrlMhrb_vNlKhwFIXp7Lga8NRlT6jDPMQshywaLQseuz2PvGkgBS5vnoUaB8UT7ZK-doLSK9kEBohIW9uUbbOElOfGDoxDfd7f2LDeZ9nXYccywWc-JbtDvYVHSuS1gaHzOrHKtLCnsKWfeGKDsmgn2RJDq6HBMJVga-Ys-5O4Qt13AJc7ePegwOutuHoqQueqqhisTGBDUZvIYh-RU4iAg8X4xJY-r5vjMlq_l4DxFb_dzk8-vexznFCby64yZry_jjbiDFks49Wykji7fl1opHfRFjrPepeNCUzC1ILgavBr9PXsUpDBGvGsIS7KclEyLM-iJdi8Yf6wr6EogM2Yk6afNDlR5joQLM_jrNTgBSwpTyUMKmPdxgXgl-LE2d3npzYvW62kXgfLB3HTqu_p1ggSVL9QuYUJDb8DxU-H6Z6ME5SWoDSxofhqcVJjHOVEmrnipECZzx6NV2ls5EU0yJUxL9pK4r4iMaroGXcu9gyrY-xCDPIwdsj4mUNZ40dbxzfAfRXrjr9DeMrlw6bf6r_2QRkYn1NnRf0ubFfGA-05KEzTZ2iMFUlmK2xcQAD8wuDDABtsAXInevr2jNMVDQd11EsnTTEEi9DeEOncItQsnNHTGD1mkBC-HFMfyvd7K-ugCjqZ4fDOD4Lgr-Ft3VtQrL2dZ1oVnGVnxnfFmhc3Ppr6rZCnPbQZvmogFsd7x9Jv7DVIFLhUtEpJ7AopQNyHDlgU_JsnyC9jMPRcB8c576sRXzmSWjO6QjjTCeEm4HzUb7IFo_lVY6z9MM_7DjCxYOlAOpXRTRyfMfQHbr8xJ-zDrOV4vsXWW4vK3aivVrcSV6yeP5asXkFgQaj9cMqxSyoD-RapNpE_koTB3i5xs0ta73y5EIY4wvJYL6kLYzL3jUdGcyRWQJZMhB5bBx_iFp0bSZgNuUw3L5Fx2_C_8pMLHSzXeRb6n7HdBpHDoVJZ1zCwcBt5UhgngVphmKriYwCQjXCYdqeFzqV0vy9VEp7D-EPKGRK8OEFjz-_6oezOUGJbr2LJWGEIpvjbKTs3EhLkYaZjx9nAHTKjYlu9SLeMUVJ_
\ No newline at end of file
+xLrjSzmsalxENy7IVaZYccnjaijLdMhlQcLPTbndoof9pjOcbL9193I3c0Hc0L2Eaij_lmB05m04IO3ao7Q2JzA0O7VpGQFHwCRBFnW7b5TbaQJfPocu9oXFzvJS5h1awNl4Tod0smBcQKR9UvRUGSYJD6Nl4Du32igqG1ZoXprC2UKxOZpA2i1O-bkIfcdwbD0SqbDI49h-vPjl_lFal_wcbL_lKFOEGyRIhvCq_LsASYhUXYn9Qped6KVtY3zSmWpj4SIS_PQaJrAKfySfmXnk8KsFqxzl8OOKS9Z_Jv9kAS3yfLdXykpvsUNP-UdPLNhqGZndHtz5UP8IleFq37F47qRUUDFMGh1-yKaAMidsX8xnEp4gjkzd50g1JtXwd79KGiP1kLVOYv_fhmJ3z3Z5vFtdNuZwrJxth_zxajZu2UOd-pqbzv1EWxZy-oe6wSXRRncaZuD5ciC2JjLEKaouU8V3NtYICWvunTBZ5EAg74Bpb1G5k2KKSFBdGdG0Z-UeW8-1mnqguCXRKemG77o_V-YxNn1GF4Umo2yXsnFCPe0WI7W38Igt7xWbeBY6vRu074eXBYWfzwFsp_t6OI1BQOr9tRL-_6aCnKl8I4MpHmMGrZn7tcx5EWn4YhXiYiRaA9Z4V_BdpGXKDyRhloKf1gN2NPOdajCT5OKrINzgV_-_BxySQwRXbfnkGKBH5c5GJBISqLPBBSSsWDUwRPmLXC6hYkqXpay95bsX7MpqPvSGC8ARWIU5E7y6vBWac2GfOlBBOF5mbYCEnQVS4b9085wv0mMJTIuv1tlG4X4AqOriikNQP-r0TmzejWFqyw-__VOBJMUQzmOwxPlRdw4Kw9HKT8xXQmbuDsRxHjTIalS8WLgu3pasjbBF6oJNWEbZ6pPR2UTR_JLW-GJjVn-vtY2WctzUY3ySN51oZtAO3um3KrGTWq7_bOcclYPxb7sOTkQ2zSZxIhGFqW7y1qiV8FgVldJO9BEu7dPI1-WR4KCmvFbgzZZRVk6f4lexI64tjWuEawpQ2w2kHIIa8INxMyjJwtOJG-byAnZUe9-WpHisTMUdh_xtj7tybi4yIK6U8uu81iKvuIASb2bT4DDyx7gXjPTks-Xl4YSiuNJh5e3njmh3fmz-q_sGm025MqJmQCEB2e_KYv-a8Xg5P4_inFawqJIyZllLVSpiVrOpjxqEg3ZNf5CEYgmIq-3yoeZtDDhIMGu10g6NfEZwUGuBT0_fmsl8ToJlYPPN573rOJdCK0cApx_PVFM5IrsxgN7Vto-5qiWgFpE2RuLwu6q5jZOMukK2Ra6cLCQKnQdp6bTSbRUGlhcx09IZ3Er5VBHha2JTWoqK2C5Q355YrBGbsLEofQ0KRUPwJ1lZst_mxmeoBfN0ihGYNcr0mwhR4Z6XErRZoGUSGnD93kWMyWko5mqk5Dp1Nhe8pWSSEFHFFBb7b5UWU5OhWqjBMtklnmk5AeLRUfs2jzKtmfIj6I2Zam4ftc5z8LqigIYJQ9HWpwx9o2pF8jZ8VmyAb3yVHUpxkifSgrTFiKsTVIBwmHvi0OTly6j8jz2OhdDQmGC5ipMGoxB2A9Cg_ufiVMUVcVNUwEJQhFeHpJTxedbyEMq6dD4Xiab67IeqB-hbW8FExeT_krz0Ph9JBGgT6A6d9-bU3FQCipVuxRuWm1c-Eco7EBk7c2kxU84Ami-1yXzE8UQGodRoK19eKHbfUfjX2oKsE8zGC4Lyb6oGATkvsxY5Udf2O1UfmRRnCrJwYAXvBM9mzlxmqxUNbsVdrpVNBr-VNLsVlBvmzJ_o7fk8kztf06WSRFiEtvvazeUrY7cRSq-J8qps5VeLcb_p3Zuuchy9IGWP1ZJ4pA86cp0HZW_DiMQM4p0wS0tu2o5PWayjorqW_EufoDwr-cMW7Pp5387gmsKg2Jvf9eINUbH7Fh5A5cSrG_EO8H0UCcxZ1aaVbqsmGaTihvZFqipcL2xJcRqKXLUeH0MWY3-yeAJQ-wKmm_NVOZFngbNxGA4u-4jML3dCNzwT4twIbmWx6ldt4R2cWReBJVJEyyBNnKKPz-y0Uudb318Cv5jx4ImBB3a8dku0tdhOWv3nPErizlonnuwNOVZ-oXqn9qK-yAGD2YaIGzwYV2334_reZ86xQ-3mxD7Dgvv3KxYffRde4AQNjPSYQfCbCv7WlT00L7NOKRrt9pxVI53i4ExOa7qJ36k2WRMkrwKotEV4jSlfDJ0S65rYC_J9Hirket1NjM4LElZFnUEJIE8uWPaQsFNHVEBbTbJlrNCrpNIJ728zqjP31BVPWcPAj0DiftVwMj9pCt2Y5xycERf3gpMYp3VL-O29bdP5vN4EQ3-rwUS_pTT-5RMw01HY7k9NXAmD_vXSvrgIxn7GyqyUyxt-0hBdb9HN9Q2SxI3arnnS503hSw3nzMEKknFlIzULIUJFPIBg5SBlrbtcxz-KsqwCuA4tB18fvRk4-m9aiAmJd8l3jKwYAmLl3ncMjUNsR7xhIwUqOGWOgihp5q4gfTv2UFjYTKsSxudJxXFrVcy2tKStVE0xt0pDKC6c9ouBFWECAr0yht2EyFOrmNpN8vdbo6MWQpLsFUfCwx19a5eAtV5H-Ung3pC42iJuMGbGCLN9IGXXcIMM_iMaH7fbQn5KdRMV4gbNpIcKdh0VyIl4ge9JaZlVTmmWcXwJQlz1WiDJSHiZKlC72W_Bi9ZRUZZcDK4cCGpJ0ytW_29P_kndZ9x2zxgeRWkK6MiFTu32_9z86YrpFzfBcIrw1ZZmpGIf_DBpyTJyoxPICEX6oYSzJZzx-Upi_FhbyvTdppuxFZwIqpfBwXlrpf4_Id9b0RuZNPBCc4LHc16Qdv9ABKwggx1i1qlGLspjOptYcFvB6XJIaoY7BmiWxx78dPzmCIdK5sgbn-dVwaXI1XaRhk3VI7CRYE-VZxQHXVwQ_Iw7TmWZFNubixSGWzi23gmukEFxAyWvmblZ7QiB2mtAgnanEQ4shm5xrxwu1xnJrprX_48r6S9RiNPB5xr5wZL01QJcAfPRaOBbxbLZsi1kBoERcoPlXnue70Cv3R7Nmpr9VoQtZYM8JyIoF16JM-pBKSYgWhSh4LcL2UGqWwaZXZd406VG1pPEqNOBQRlijgsbTrgBPjjv0OTRGXyqo6Siyv6H7ZrKwpziN2aOxgh2YGJ1Y1EAy5Rkhx-goX9G1Br7ScD2x1hAjOdxqL-Fj7UEr5pX7i7tBtCtWKwuIrcleTAyTHGq_iWmn6bGxRU-wI3FdNw0CKf8-OPk0KQ_sgk0lztyXxSx1akY5k9SgOq6FJB6k8SmuxJAnj-WqW8qwrMd7NI9qBEWoNpNeC-Hd75vkurtglwqfjI46rLftpjgLHR2QIelL4L3RSLxHzqWEJ9-zwHHYQs6MlF-KTW274cfpjSqeumL8_uOT9mmE3txpGYbKEhZCmZoLmZ3vV6pMe4zkBSHINtZwq9D_WunyTlBLLokok4ggEHx6D74HxJ5TuPCBSkFGuNzb2s77D8IOM3aL8t-Y04LEptkEqrpf7TNL3wzSLTqc_kwTgZ0z_XtUJabX-WOC-VrduechwA1QokTRIGVJUMkwiJbupabJQhKIsaRyJH5hL_-n9pXHzJDFvnPFAWzZ8PmKTiygjs8qv0u3N7CM5UqSlQC-V0sJaqlwi_nZaCjdjH_Oj4iLUyQbV23Z_HF7u9rewatAVzZ66wjDLRiE9OkF5Q_-UFuElhthEtgzB2Adavmo_XjcLvi45TjLR4PeLvzq3JP7st6P2_hj29IchgIYcT4Nx-f3WBc76IxqeGfC3MW6j3m4GCqhd-YXqC7ZNdmkfQkDna_lAqB2Jgi6Yv0NEI-pKzTqai-lea0ucQ4rgyNRIkKYqpkBFtLDporZJtlE86FxIY0yDLJbc1Bbp4FVESXbrvrdm9_uLSSxaEsyU3wdR6cgwjyMjjtUc3qj-uGxjgwC_6YtBRJgsCtmMCSXUUjK_hnyEhdzPob6XafzyAyE4S-xJZTnr_PEk-Knp_E2jSP6ne_bjrYrzPMKQFy0-mNwsFgjndCRnkNllbhpe6eQsmaNN1nI6WlEFJkCcqRzxp3KsTlJhj3bF7JsY3wr7s9ByJc0xcuTRhsEU5NOfBX9qVzRLxwxocpgEejigZiBR6ex2lmu8wzeIrwcYtfEhAkoIlDn3mEq9jtzFg5MBCXZuRq2r-46N6MaT41zaey_uWlRRbG-tSk86K4OrWiTTPjUiQfqz0yNIVq2tlxIEhrxO9XxoWkTJhhijN5yEvEsVl9y0TqRevIXiUbpB9_knxiARn3lpZEbhvp5JVTqxqjgcuBrmQ_Gsn-dk-iTG4iwwXQDqBjSRAZ-ymSJ_u50GtTMpPorzY7CcrZ-n3fw1795KHMcTZmyTdniVWDCd7MxZsW3mXlTG2sdhOJZvtdTwbEypsZr9Z436rQM7ToMxgFPIPxs9xBR9y_8wLzIlA7TZb-SyoNiz2vRR6YQ0GF_uY2_swW4Y0y9pL7Y_OjwKseOuhKDxNQNa8Ep7CGcnA_05w9IkXKIxUN8SxryRuY8kG8uu_XyWVV8vzzkIFMRS4rHza55iVRTQ-AmZcQsSv2z_tABpg0xvX3bb6qT_RgbJtBAoZTIksGKj-Dbo5R1rcFovYxsQjyO3tkCwptceZTQsrUwS-EYVSnmdFE3bxpMDlPPzLs7u_AgunGACzh1TbkLIidma9VMNui0SNNzZ6yjGzqOtjhJ4ZMkuGXk7qHXoS5zDkfUeXqZCzX4q_I4ZAcxePeStFgDDzces-J6Mot-NOVTYEgDibuZwCRvXvGPnqAV0ye0j8TDBUsUxi1WKcXGmwAmsFVs-RGmfNzcW-V-HCiwhtJR-uXfIY7Q_UHgcCnyKgpYVjG1uBUWwA2_So1pjf7O-LBVLKCsE4EkYtGjyD7T8UtvFVjuOzgFl21h1qdxw3Bb-EkgOHlyxM7k0RWCGt0ZfJmyi1tAksOlBA73fWmCNrxHRPg6hX-fCOTvoh0jaiQfyVfoUNboRz-_AaPLoHuDxBMaJWNG7p2ISLtXCe8ikFaWEOZNVSNEOD2EuBXkVDyA2E5hKaYyjK0tylpB13ZA6FD6l8RuLFSbEAEbXjIcp5XpyMnKJSbeEza1J56ofEFaxHi5z_So2PNGOKa_NS52iGVRW1Z5HLhIsvom7Qt1D2yIHU2Gd7J7d0A41Qqr18ifZrh4khwPyZikIneTBMvlsPfB7VfFJY8ANVLdOrwn0FbVZXvLiyAFK3fff24zc8IFvmyHmM7r2yBNvNOEIsMAx16Rx_uuUJLYNtfcKVKJBZpDreDMLOMDu3rYcuVUaKTa1D3ZgZxYsHJLKerP96-0gB3q7B2CNn_Gydx6tFcGOhSKLfFysuOsZfQufMvYAFEYz88qoXC0uj98bHzgAneJeIe3z7OFI4HBpTUZiX_XCeDLOHe529jXQ1rlrj4QmIjQDVziOh6AWmlEr8PmHe1A-8qYbFiUrumHNWiU4Qkbeu0sG1PDor4W1H0epT5Hv1S84M0UW35MOa3bcVFNdb0aWTyM6vzcBYm7U3yXeDjB_OiAaiGsqyR_6QrDUO6vL1kRUaldrJFdNauTS3wZJDPCu5FqAtSKQLSa4M0B83Y0EO0PUHAxGXG0r2CRayX5AUf8-2CNoKTSHh0AMBamiXzUNG4g05e_9RdY9EDqzWX1n4j6wqupdc05C5nW1D1iK4PlDytujFYCtmTS0J91L84o0LGHAM7ol1biH6X5g6eF8FGH5GiL0PldetmTF14Q0QXYR1Oi8HSx_gdGoRa6jS_7JoJTxj6d4oS1cubZJ0vON9SUHcH6a7AVJzZX6mQigXYatX5hV91oN7HGaHg2AbeguAmYaPg4gY8q212xllx8aecIaEN0qQSN3X5PV80AGvqWL35VZU5KmHe4A21UziYJ6wO8aGMH77_Ho144HuRdk5Rt290Ea0H60kOgJlO8ae6Id4Lw1H24HaR6VTTKX4S0Oxpxge8ZOHcyEAgY8-6JzGR2vTudEemCjjeQ5omn4c36nZ7_KmHcGQP28e5YdtZKqH5eZ9rXrI4oGtJn3LJ4JOtR14Y6wAH-weYGWgX_dLN4Hc1JFH-NAWuhhUAm3J03D-E5J6oC8hOEB0PlfChOjHW546nGEbV21Ln4RYuxuMAs2Xix9kk_jIu99-b9uoH34dRov8aaVFqzEYhe_JBBt_yosq1idURVG4Gtl0TojvXoDX_V_tJbr_AAjeMO25N-siZhprErEupNsBswIdfSReLHHlrftkI92DtFrdSn2LpCBVO_PQcH_WexHqedXwBwytCiaqA1fabZgjT0lXFtJcK7bkyHPMDeynyRNMfsb2njq3j6ydG5Gcp1R8lX-d9r5RO33setpoWR4sF29LCldT0kOPRSbgsnsXJDVES8PnwqP9ida1D7gqdXSwygRGAH-EQFnVRvjbgt4oBEVQXlTghoU3k8vkKTazNuwLiuDHtobghyTJwnHJZhkfgt8Txkiryg5-7y6uNsgN7ATl33842CTwMRhgxwxKoKuoaTtl5p95SlNjStm9hFzd8ykUKbBrt5adGEtHQU2zMsSRmBWI7PInx9pv9ZyagN9cfU-LWUboVccyzS3AthfS8QcR7_HtIQJFBDxqIPKuQbs55toEJ_qsw6k6SEnQTmYfMZMABNqdlYFm05Xut4eWvYAYrKN_6AoyInf7J4SP4BsMr5CuvIlZ0SoSdAlfmiunkp_lJ6LWkNYYd8SrNBDpQ7ifZ86VRjOByMK_jbXDtPZe5CrTvCw5q7AaTQAHT9vVNjjfwj74LicJeHEPr9tvxIRJHdcvkjmBt2NukJg7adH8ga9j7cvhbEepwIxLxxBQZrLUHGdUwXfbt3jSIdWqFPX9Fba_t6ygRb50HqmB5q3HVEdQ0CRJ5C7SG2XPco8lQz7ofURHxQg-8cqeVIjHkTIMisSehMYcdMPeFTy-SYa6bRVD12nZCHZ_al_qIffVEeYjj5w0Grx6eR8UAV1uQneIjTMVh_XLChRy0GunjekGDXXlrt_LxegZB8jcDtvEa5mVrPpfqJfhm8WSUzQEhg_0tZpSm-9kF1lZRfqG7pOjSxtT-jFMeSQYjujN5QEFPUjccLzJnNXo-ggFdZgXLCFtM0DK1UTN1Wgau8A7gP_v2KLO_crD4DkPZ4pcpglviZcJjsS3D95hzgXiedAAzaKFvM4RHKjgiepbVpWhdy0vXc7VgMQXaq0wrKHhuelSa2VU-1-CLctxOOYvzimqtUbCcUhRAdwnr2h2LvqBcqvf7E7sJzaRZmHBhSD0GUsozUPnoYhPRI67-z67J_KWyplcRDGpOvoiQ0q7lVAvD_s7xvkE8s9xIvnBKgjAuBtBGSfzYjqIWLfFlEe8EW6hTNf__Q9k5jzPYUTg390PqI3tSw70TG0edzockAwbegn5ihCSCASHAGenr4B-UiP8WGotXPAsVIRPA4smxWZvji9X_W3zTkhWcpXErrfvQTAGtWnEE5IKPjLHkMKOCrL9f9fl8biLyONL9N2AmZLEgPEGeiGKIE6s5-MYWlwufnG0EJhx6eKvhZLQZvhgjkAeKxi6oARLWjuxPMJs3X8rIlrJIV9dyPC07vDbbdyyjhaq8mggVTFdpy9ox7dx4TTcc_QyBGpf2v2l6sebTE1bmNoJFOAok9qUovleaWriN7vzUdpGIATSMVymITJeZlt4dvvVowqAlslVB-Al-IVevA4i3MgGbzk0CQyrbPhQlpgHJhQGNq5aK2seimKKl9gGiXrsUj36VDK9GcruiAj60df-MLW6zcb0_BytcSHb8KsYCd6cAURmQ1r-dhhpvMVJSQO2wK5yObTJdLCmsKafeGSEsGcm6RJCrsL7MZVeikXs-DS46t51Apc6eeKhwuqqu1-sO8etqlYkTGRFnHohnTvFkI0aLaQGRnzTQgqth_yTNQ2_dwrnUFCJSiPvukGG-Z9Vn6yPsknZDeIK49e_-ye4vl5nUOyUdxpMgVH7BzbHGeXOfMK-2QNSdixH4QLiKJAtTprltndgtXw2Og4CzPfiA9butBOLhUTkAIxEQfRtwIgtLUGwks7BAuAoTrEsttau1BlwPsXSGpDLGjPg5fclwTjv0bRDtIalyPD56gg7zF54ZnXhx2WJvPaLPMxwGlZtfq7cOaurbVEGwzehln9hY0Z3-Tf-BdNeYYqLh9oUKWSjvRLZqFTTGwdwX4GMNJq0abBzhAPNYrbzADOUslQ2desqy2QVjoXBNnFj9ub3gOws0Lq1zS3AkM0pCmqEvcAED8gqFDg7qkgDGn8fs2jNIVTQM1XEqnzPDDyBEekKmcXtPnHRJTG11o-7UUf0qkyuNRa-vhydqjqZCOj6-qeGUls_fDw66fcHY-om-Id_NU1BE7JoRCRBnn9Zv6GDxKFr9ExnJvhDTIuzeUNEpTtEmtgBzLTZeQ_OFnC8BjtLPcbuI2zdQjWov9GIj1THsk6G1OIC_l2hfWP3tFn7VaQfPZcsMzmfNbSS8jkfnKxGk8o-bTkxUcwiFcS_Mm02Tg0EMqdzPdVnWAcHTDaRWwdf7IRNTzNpCZVcuvw-PpztEaaFm0NOZDQTtGOuAuTgbEDTQ5MBsK2ywzAOnkXkEjNDisSklXZTCYPhEQRXx8zNS_iByzqZDPJ6mMMYf4J73sIkvALjz-y6y3cOliPvlREhuUd0Js-AeXksboEVImyCPyANmDq5CCxyvLr0j0zX6-_sxiOeZrX51wRfiLUA05CkyBAf7KET-coBI13ldKb7VbYJ_W3YloYBvVm00
\ No newline at end of file
diff --git a/docs/logical_data_model.puml b/docs/logical_data_model.puml
index d76624f3db..f418019de0 100644
--- a/docs/logical_data_model.puml
+++ b/docs/logical_data_model.puml
@@ -403,7 +403,7 @@ class GoalTemplates{
* updatedAt : timestamp with time zone
lastUsed : timestamp with time zone
source : varchar(255)
-!issue='column type does not match model: text != varchar(255)' standard : text
+ standard : text
}
class Goals{
diff --git a/src/models/goalTemplate.js b/src/models/goalTemplate.js
index e009940308..e1e1af4546 100644
--- a/src/models/goalTemplate.js
+++ b/src/models/goalTemplate.js
@@ -67,16 +67,16 @@ export default (sequelize, DataTypes) => {
allowNull: true,
type: DataTypes.STRING,
},
+ standard: {
+ allowNull: true,
+ type: DataTypes.TEXT,
+ },
isSourceEditable: {
type: DataTypes.VIRTUAL,
get() {
return this.source === null;
},
},
- standard: {
- allowNull: true,
- type: DataTypes.STRING,
- },
}, {
sequelize,
modelName: 'GoalTemplate',
From f528e95dd9b961e238e57a19d223cebfb3e841f6 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Thu, 21 Nov 2024 16:50:51 -0500
Subject: [PATCH 022/198] add function with test to get the grant citations
---
docs/logical_data_model.encoded | 2 +-
docs/logical_data_model.puml | 24 ++-
src/services/citations.js | 110 +++++++++-
src/services/citations.test.js | 355 ++++++++++++++++++++++++++++++++
4 files changed, 479 insertions(+), 12 deletions(-)
diff --git a/docs/logical_data_model.encoded b/docs/logical_data_model.encoded
index ee385ec3b6..234d1eafcc 100644
--- a/docs/logical_data_model.encoded
+++ b/docs/logical_data_model.encoded
@@ -1 +1 @@
-xLrjRzqsblwkNo5uFhGDRWVJThl06hEBSHmd3JPn3DlfO5eK1YtHzxAHo3iavTJjzhylIEg5f2Y9b7ITfEKd-_Bj73qy90_FuI7_mJYWl2gB94q_Iy4zGNcwf-IsW2LBtoE-Im7U5Z1FCqhUi_GEGfwaANk7y1vGKAO5mf0_x65AADyH8oih06FfRqcQfkbJGdD8Jqb1QFgNRxxvpvF_-fkMVhr3sJiC6Kk_JjBqTydAgQWDMPBMxPna7DqZ_d09CxH747FsMv8-IL6U7gS8SxY5D3rE_xw565B0OVu_IRgb0FENPe7Bi-TdbsVdfsTNwT4BaS-E_eho92Ly1-aPveW_ZDHuqvP2iAxu90KjPtj2n-YT60NR-Xc5Wk4JdXwd79MGSL3k5VRYP_ehmJ0zJZ5v__aNedorp_tB_vuaDdw2-Cb-JyazfDCWBl--Ya4wKkijW_Hni4XpMC2fqacA2HTrEFo59odZW5SiFKuXhieXD4z9KO1RGW4dVov41_1uZWhu63ZSeGAEr99iXE3W-_r3t_s2WE8vWaL-2TcUO3O31D64Dn2YSFk1MmAAQrZk2y2XPEA2ad8_El-vQnm6ifJMcDH3wyUVnb2yX8nKD7jK06bDSwHsozPX85ADowvefXm5a_XF_hmPWUh6gFvRac9GAjnbUY8ftL6HM3VftzhV_-_BByUwQRWdftkGKBH561GJFMVqrTAPOnj0Qzq-pWh2u5N5zX3dfmIBeT0kDdhU9GICu6RXII7Edm6vBWaYIOgO_39OF9pbY4FnARU45128AdUWOBBC9UTW3zf6X41qAvlikJQlNUZk0SrsW7x-zRVVVg79J5DlOD2-kVqPIePE4GSS4Hm-SjiclBkphT7rLKrzf8dN12DEACBRAmvm3cYkhISuHtZhLVcvAglD7lS8llUgUYcBVj0LkkikKj4vwzGgdILfZpHGC_TH03LFfra3fezDosavdBz8hm7B9xXSzv7hhTdwQiqByOU32qhkKGxpWMLxgbN7sjslCj7LrsIFiW_JproG7gclMFiW0y1Vj_41el_fIxKSCelgOVVWWBuH9X-3dwzcgqNj5vugfh-357QgqA1Mmwgz0EfQ6AwQfkHuQsbgmsv2yva_2uPtw2SuvmrFTMUdh_xtjCr-os2Uf23FaT3VG75EkB3-PKeMX5K6kogqywLxRlIV4YTcmkdUBG3ZRnM6Jn_yh3CXTXmAj-DwxQKiy4YZ-4agZ8T8ZiIEdQyJJS9hCsqznipnPtLpE-g0CajExg50aLN2Qf5V6UQvHbgwcKE0GEWbARfndiC2tKFwy1ho7SbxhEKLbJi8iTE26If1yVatcw--S8bhPydeRyzA8JIooC-Cu5iX7dXRWQsDXQXSm2t8bCYOl3XbNgEAg_8MqZTt5u1obA6zC6Tt7PAaUx250Y5Oio5IZ98s9tjAjXIKeizSsRIPVFiP_wuWur88h9bMk1SCQ5XrMo8cPCVgF9d1EJIHv81k8RyWUz7XGi0TvAMAuCpWm25_8kSUKLw1ubaj32ufRUTxtRmhH2byfjTRy9xr3QjoEmsGqSs0PE-mkf3EbXIKIJHAi6VNP6INPpNOy7yF2fG_7qNi-xh8t5cljeYOClj4Z8Czs08Et-3Na6wXOLtXj887yZre89TbXL4cHVuLsVhkFZEhtkZcsYpw4UqtUwnvV3bj5fpH9RBn7HxAaoBAvO23RjuF_tO_Wypae5faEZ4iXK_IlLZi6MTlyDjzGO3JsdZO0N5s3p5Ny-85AWW_1qf_E8MOGohxoKD8eDNAIDVR15ieiK5vf0MJnKTntfcydRidjr1DJp3iQcgmR_mcAdr5rBGMiJXxV_ZfsylBi_Fhc-kNhy-khazUNxZ67tcFJSJTwdG0D0woVOLlJpBxmrh4lATppob7JDbNw5TeViqx-E3e_iOa8NWOa13JwHqyXGdlIQXBFBEKWD65Qq1V277gV6g74Wg4tpq5sRjMlWoqWvtE8L1zECSomIVDXl1Ij-h872jbiHCwrEqS0k4XxJPkaFHrsGGhTCNsilaiTKHFkPwTpcQHU8LAL02A-CCBIggzdySvNFujCXDLhT87CiN1NxAYoc7-ykwSy9EyHDZHoR-FW3KDr5seNtwU5hukBcfo_mwmbrZk8C60UxndmRB0aelWwGxWhSTy4BAUh7tuOFxQOzlBC7n_vRQOao8Vo6cDGgg98MrHPaZpHDyQO-3kMZWyHtHJrNDf2fzgQIu6XAbbxIMBscR9T2Hudmi1LMrsLFNknESR3eeTmXqPic-COOqni9RrEbMQ-vouTZwz1eO3eqjiWZxUgDbz5AwgDgH25_-vU7mIJXmdC5CBkxwQJrHkL_LUELVNJ7V98TmZRJb4SEijQBP4Ey1s-g2lEJqt3ITwyckIepkqMYTAVrFj3vZewS6Tkv-wD5_ObP7g0b28UOWQ4N9o_sFIb6jElaT0TpTvRE_wn_kUKr9UbO1ojeEGNtDmKG2iRtd6J_THxlQpBptr9v4_btwdLmY_MpS8mxyhjnqRmIjjcE5HydS9zmN8O5dxCnTRPvsDhcIhFQGLrkRRi_kjBvsIXY5Wg4hFNmI9bAg5yFR5wf8uN_6ctIVguzu4kezk-3ftk0sP8OBDI5ncQmOOLw1uNicJjQy9vhjUarcEN1DqBSQkh6vg7PuIf9H2jpbKvbtjOOP0WHW_Am4gfWev6q9OEeMbEZ79aLxOElDKE-cdMEeLSpAb--T7r0XnYE2KvCxwZWL4asDIxJ-8BNZAJ6COPlv0rc1f1NFF7evjJD2K68RfWJPiVeOhV_8pLa-XUrrKjmNAZAE6Eq1X_aya9oqpMl2bT0uz0npuEdwKE-dvk2V-PLie67J3mvCUfv-zV7PsVlto-SkpPvyT7n_9NPp5zOsgS-IFaZn8WEz8vnCpDaqKPeJcfwHIPv7g8YpRmHBq1LlxM0-pfVy91eNq9ChXfm38UonSvITSx07rSrdfSVgtsd2KGSQ62_XtqjmEulldetraeR_ckClX7S98DrT9Pot4uBQ00mSCxlZ-2d8kmcxqEjOG5XAKLpDCS49iNWFshgsu1xmFnMk02aZDKoctmgNAtQV0j9NRFXOsr-tThYrGhFnods6VRtkI_ank7KSAduXZTiB6jbXPev1L1MzF5h8g4zAipOWwOfX33706VM3Z56staBR7QDjIHZILsMOxEW67Mq8VDCXd7DkHbXlTzEg_Bbmf6Ewgmeb46mCchpUugieIK0IzbsfZn-jQgxMT-T5VZxGwHrQCcgC1Eq2jQQMfTHIqm4WxmsbGwRU-xXtFG4l062MaV4Et0ADVxLEztsx-mrlVWYLH1skkLE10hnbZOKEOSLfbupMed06QywVJ6hX4w5dGRhqNhEYj5-g1aAEIKZVSY4fRxwWLMGYdMtcX2XfYA-eTTQjWI4ocSbGrhJNrZy8ErCYMQlzBiunODj-FGSO9WzFRsunG2gKyFeCWVuKmN6uWgmNOWtfhSjJZZXTAydU8YTzUkk1oKGuNGSNVmuWaFk9SV6Vmr9ByE4JOJzXoo24j4GQ6ZzGOZnXG4RbntqcQmrsxel3fdRkYNjoNjKDPjiU_oqifESJ0cBbn_rGaUHK9N5lex4doqKIklOBXqPGILbCCkY524zIwyYUc33-YRFxXfiM3peTn8bpKDuywRyHjIDo6E7QiAyvozexvy7ESsbxKd-DjXrgygV_4h5cgt3KguGTVwPy-1Cj6KswI_yUOkBNUM77WMBBmMEFcZwFhwD-pjghJmofgEi4zunS2lNX2NBLNHN11VRoWwR8_sex9FhTfMQGqSIKLpuX-G5FTmymvo7Qb2LEWQq1reA4Z1aXS_uOEXmmQ--1rPxhUPlpmSrs8qM5LSm7YmhbjVkgIKVBnRGJgwpTRfln6qRP2kV3apf9VFO2fxUnvrmDyRA7zuSUZBCQMdbCU-Cv3dgtgFWN-mgyut8U4vU2wpbZNrKK-hN2S7XZzK1p2NQ4u4nLb64wlZDq5Zt6rdRTCwRVpgv_NSuvgPAJSY-d5kV9fr-C-_j7LURSs_d6EiCu8KFnOeh-khT1F8_q3n7_rCVMBzlABqzAZk-PSSBf6OEY2Trj2Hy6XmyUqiEnvbw7EZZwxGvJnqtf2z2dx4b-8w09vUdMwz3dXLsAJuST7_MrUyk_9ioZgpR8exCsnAEpBy618tb0CdZQNT0DPb-ILTc8U5-BDkqhzGfpPa5Sz-falmYPnbf7H1VPAv7_4R-PSg02T2qWPmHWMIvLrkptZ5cbetfOJ-eMzG97w0BiWsBjQIzsskknqCRoxK_FzfFa3EgEVrCJZ4cOvlbqPUmhlq2-EkwBlNCdDy7J7FTLzuOhWEqHqytnbhJDWMLVFkX5ghuST3IHpvFmB0XeQF3RorDk7cjh60Wtfw9N75KHMceZe-EpuMFm6cRWxznxG1uItEW1RpxaJZztFTwbkvpsZrDZ430KjhBkvBTtNB9CzREzPjk-JaTm-9Vb3Uno_kRRBcUYCIwme6i73twBW7-vJ0eJdkEPeqTQIseIwTaXzKolj9UJ0lDUmIV4pu9MeX4wnT6iPufuUxoSVGgua_XWUS_2zmjcNEc9TZAsvoYwOSRnLzweedAkPhInqtw_qeWFeZZba6KLxvwXUMREyWbAjr2uTzOsi38aDP3qgO-fch_21vRXFi8urGNZR-PN-x9XuNmiyvoxWDQ-fdNttRFVngBpA28No_bMGxLQTvOHPw2jBZoMW-Dg-LhVMWMviROq9pdgFAWJtB-Bmf81-eQY7SCVut2Rn92qXO-gjXzOtfsxdRI9v6yzW6yVRVTYDADeuuptkPfXxuPnroUyze0X8TT2CNVTM0mATGeTS58VNlhVD9OGhwpKVtlCdIDJ1qM_k8QKeXskFaQeZCL6bYT8-r0MVzw2eODzpe9FsaLXPgI-A0I7ie0k1lXs-SZky9xzlBtDK-u4FDDjnyWwwT3dksahuTbzxAMi07jC0x4W9Bm_yHjAEoITuwC0K9hPlhyXMRGFNjzIuxLm5s7PEu_GuFRcyFFdNZpyrwue4xqQMEXBt0eXFEAduZb1fGNRS7D1yfEiz8gTHQ4SmF7TU06Mui3LEXByQu7jvOGLY76MiEKD-8tIAcrAykN93wcbCpEVYNgYR4j1tie1O8-LDnabQjeTlRcINAwD2alwx0WNY3pU0iOgAioKtEM2xMm9ethWBGQ6eQHcS0eH5M-e85jFo6eUw_XdIUouJ6bbjtgDPQijT-guDOafTTNkZ7l76k1zEdjMxGWzGkYqaeJsRn0ydpr718VNpmbSbTcwBvGciqTSiVZXvTM8_OkPXDHFg_8rMGgQLnGrWlQJRXrvI1-J4aYFA_YB9L9KI3Pcadu3eCBIKS0pVtr3o_iRSUP0YTwJMsxmRXhQEddXb7kAe_Q8qmZGAqy1XaWXL7ohBcXCXwaCqzkv7nCkDbyFo7w5oGnKX6eMeEo5O7tcEU7dYPRzl5z4QGIkkW_vOnSaz32_RiXl16W4huZIAKnnxNZ15U2nuHfxMZW3P05bFJqI0542ZBsL7a5mWHO1w0CLTYGEMPyzUUK2I1tnORdsOUB0UuFo6WstFLYyAIn3RJnlyPXLAT8DoA4E1-iTDLRxJRwbkdBznEhScy0bwbJoAb0ko2905K5p07A2CF2aU0Me0Yjw0AOJYDdK4dF6dj2DE0pX5Z5mOkH_FZW0r08s_SXrnid4QknGYecMZ5TSv3v0YE0vm8YWsA8CF7aTyMdo6tqUE8Bb0YW2v08gez50PVZqt8jGY58Ld49g8YeNAu5Ey4PuEdWZD83GH5WkMaETm_tJ8X3nx-wU3P_8jt8YJYTF0AwuH9YTCZYT28r8ZIBdViuoGJKCMHMmIhoXLFYWv7gQ8I0qX5UUg8ApY93K9L4He427tUuSH9HEbeSSK8qukdA8mUO0K5xf0gEAucy8f0ZG8qC0xTn4cDqmH8WkY6FyH2564nuPd-DMx290EK0J60cRgpRg8ae5IF0KwXL14naR6_VPM14U0utphAq8ZeHbyw2lYe-4JvSR29Lu_EupCDauqBbZYfi4DZECzguXCWqm4nGB5VY4heY9HcNfpAyBaXcdYMQu8cnks294DqSZxNX4XXL3_UR28Z40cQZykL1pFWKLW6c26NnEB65aOHMmSs0o_OHOnQZ2Ae5YWz1_GYhW8NDplQ4Li53Rsos1_QboIpz8JnaY6rBiaIPB8VNfwzDLH-kMNF_xbjW3Pkys-00Xl-0vbxp1axB-__kdBBsKHBGHOyDN-siXdeXCLVURRc7rxIdgSRaNIHltfSf9avAqiYvfub0_c5hdVqlG87wMz2yNJO_4Sd9csIS4WiSJn54u0_Qatcj9ZYxOeid6acTLMvwer5NiprDP8Xfv8cCrG_p6T7bTjaSrGHbltaFofQwBdSATg7f2tPiI6SYRh7P2lOKcRvkqnP4xjZ6SmfofQdCrZgF7pzRZGYJU5Lg5eFDle8jlahKRDdD77FhHNUZKUt6Daaz8kTpLQQewU-avPQZMQshYO8jmLNOtwy4YtYG-Dwn3QxK70z3XbUvWXK236srJHgVM8qT9CDfBUwnepDGrLzxX-rTVEsSZqvvIKlJgBEEXTYYsqAojjOsW_Wi8oahqRgQH6PoMkJDHziZ4zhc_rjpASnAtdBuAwsV6-Y5GgpVHDBqLD9utAiABkaSd_9bMDSCeTYKvW5Qb6iCMlf7U5Vm09ZnSJn1v9bDgeF-E55p7Zy3c9oKHFPJKNpZrK-C9p9pTI-dAphDsUzgSJi5IyK4f3agz8kAKzbCQ0Jxdh1VcptdekRzpfYHN4NEdDXjTnvNVW9EswMRsUgwsiZIkH9KCtHg-xCTtBHXlfwVQUAr2VeEUY5DgyaqC5SfErgKckGwX_bBn7RZjQV1KkTALhc7YdS2sXsT1WPkF9SdD4q_8c9LJOt40nP8QGYXrW36qnp1qD1JCpf4LjUhhKlBPxOgyQDuK-j56xrnMpPY-lE4ivfwNvSFUcKx6ej7q-E0C6az4V8MY_XEcfizgLSnUW4DUsico7YdmU6iQ6hLLdw_4Lt5h_W8XOsqJA6mmtwh_hzqIrBfFaC7vFafwTrf_eq3rkGeiSoEfRVrUXRrw_OD0timResxTO1-sBNFKxFThAQXHgAtbrDbfxkvwswILrj1URhshhliDg5Tn-Em7L3WCrm89RSK12rS_yXMfP_MnkETcOsIPoPb4zRmvdxTt5pIPQ_QexABn5Ez14wLX6qjAsMTVcV3uhdCCxX67UYMUXRFSxr5ffu8jUJXAkVG_QApPziyNSziq-tEXTcUZUDFzchLkABJySCdDh6k7jcsrlD1eiiIul3PcFhPh7AQTekuKKvayVDjsFnECUmbdBW7t_ex4GzEuFs_GpsvgFms9xYw2BKQj8exx8HSjzozuIXbf9lkiQT01Ko_Rwz4VxBBot5lFJNdGn84CgSwF3TG0kdDIdgAuaegr4iB4HCoGGAp4nDa3yHiPAZ0prn1ZfbTtYTQTFki5hwedx3hTLH6BRDUWZ4ulf1-ZtfOjguho1REsjJjVqPd03j28depEdcvUHYNCfDvDTPFDYLbYT4nSBREFaAXcvUcq11ywvoymQc3_Mv680Xn6wAqzRL3OwEMifsid0ZLnhBUMZQrbRS5Kkt9f4apee0rsrLNiZg__tE4nbtTUqkgiF6wb-Fh_vFFdnW0z8ipicS6vHRMn5LlcaE-WGVZX2jnFZEctJ_jVPYns-Z1MT8iHEdFICBE9dqAZQuoDPyo2PGQrpP4mV91jnQMYdT6QnEdrqOkC-HR6KNcSZrxvV6ihrJz8lr5bgUIHjOmzE--DSOcR_waGwqKfwbhT5Hhjc9hZe6U_Kp66d9wrnRWrRuQPdD4mqeEUV7r1gGDrG_m01cqTbm9-a4NBwa-RHQdO-UkjgcpEhFRU_faxKJx0bzOKdioSfnVJMiUIp6MsRFkIkPNrIdzLkXs_TO0DkQ4lEPQXfbVJNct0AsdrNMjFxBdL6cuq-Kec_ctH7IgWQIRfrVAr3_xJ-V_oe_tAwpdFWGyGT1x-NS-13U1czOMotYz4SEkjXqkYVyvX3owKrTdJwMgCAi5aMd4E9EarcvGIttLtnqpPAswJWrjh0y7eqrc53eiL4tGupUwfWehevKxZE8QvuXguvWwqVgLkTvk3HEYSFpj99tN_hXNHmySzK04SmLKFKQXAQhEbnuO4YPuVSI_naqaQAePsVg1LZZG0GqNnpzAzDNzY-lUdGQPYRZMLqPC11nNVYy7S167-xpqN8ft0tqbh92INWKbuR4dtDDRHrFzE8Wil7819AttNcJL7hdtcRqxPzygfhsyW9RzlbpgyRxtlpGriVwmMy0kg3ct55Oc0V7ah5N6rKq_fY4zfUTwUEC1nIg68iM9iQtA2zjcww5NPMMuv9isjsyOpfkicYfN1lPIJDzGKBuuZkwsRwiIopIBHL6jFZzstxZQcXIPc8myP7wUyQJSAvWsQpHZQQ63C_Oo2OsRzfXtRAVEBgT8kMDpKxx8RreYjM8QC7Lno8XDiPqRW2c7U0u2pYNfssBabJHMCX0P3Bkcr9WzOnUzjMiw1Xmsk_QjC3zlP_8huZLNDetqpl9QxAZs7frOssQ5r2NaBjZF6SAm-PJm1fW4pKrbAfFAtU_A0VPbqoXjNiUaT9jTtr5DoDcTrJ5iryjwRcO7o0t4Z9wLZPKIPSiQnpPwj2R5tgewdUROp6JB9sWoz7-TLmM9qewRG1kCFjveuNvXz9DvV4G4ZPqad4jAMlf2DjzI2LUGFCFeW_VxAfZVl1NMopepgsvv-VsnuUThylXR-ne9hErRcCQkx7rjNWFuoL4RCsHqhLPQky5gHOfcLHtNmxzzjK8usbTMuhwjkIv1yGvrLP9FyF
\ No newline at end of file
+xLrlSzscqd-lJw6wBtAREUdJfkqpKq_RkQvZfvcRE1xRwPtRJiU39SuvD1AS2yYkswVV_Hb0Vm0X2IGT9sdroZu8iQiV2ooxxF9FnW7bHPv5SVme1hS4fV6EafiCvYo-GtmR0xp9O1edbDonU0qXZ-AOlKLu1oZ8umnXoEztC2QKRudnACa0O_4VKHp7yVU4ff0UaupG-EClllZVewy_qygljYXvYo5ZyTCeZl-Kh1oBjo6BuhATeuIZMyJlBs22TWXYplw8uiSnIkF3664EDv36X_4t6uWX1HoctqNnXWBCNvWLBavEJovEpevFBkE3v-8vE_WKfL5C-1RI4ymHl-zvutLT2y7onSSng3dR4Tf7xyYeMB-VK2WOFkBnOSnH3Xa7-KvXBtx6lnCCuyCOaxj_V2Bg5xlKl_wVKMJZ9uWVxNQKt48w3EBqxoiOf3rllM6GFWmMSGepE5GxYZ7XuXqEV-DHp3ZW1OiFOuYBl0lDOv9bu8PGmCdV2z4EF4vH1Xy2XpNAOCzRaeqG77o_l-kxdnEGdIAOfHUGxGXc8q0GJNW38IhN7xWX8Bk2-Iu374ecpYd9Txtpd_kD_K0MqnAJScZz_4iVYXUGaO8c3meWBMdsl5spjH-8LEEoAwefHy4a_fC_htkWkZPU_oEACSWLxh8yafJML1IMJVgtzit_FNdIrqtTFJdSWuIY3S8W6-Wvew-cpXoR05hpzd6I4GOl2xQ3E3qcM0o5Uh7Hdrr0m0Yk5fuOulGPaEkI81B5YCalWzdzIOeu59zoGqW6WNXb3J8cwrnm3dQWRaG8HBEooPwhdzKBtIsWoHRGVppzwgjFD9cftkiOjAzlVeMIeMF40KS4Xu_SkWdlxgpVK7tr4MAiWDy-IaXMv3YcS0qfn0cCKmAPMDRZ7F1a6-SaXTbXVF2AOCG9HNXpoG5E0KrbE_57ttxhUlWIuRViuDC3Llv-aHuy4k9HVd0Frns49Z1yYd2AyCR9R_ciOdUjVfRSrhyycDM9JE3r0juYUETaLJsgEANoLybezSE3Jut0kz9u6l4d1HspxayMVl7XadPF0oLpxmLGutJ4EM1wtxJ6ciFnIiySi7o0wayRkPOEf6i_F1D_E3Ydv1QbCErGWuPQTUX2xLS6klgAx53oRlgP2z8zxolMtia0yDSLVO1eV_fKhP6CehfVcmQWhuK9pU3dyz5gPls5nuhf7o25ZMhPgIMoOYU0kXA6mAefETvgcjgm6z0any-2uHtw0SvvtLFToV7BVntNZzymc4OnI5Cab5EGdH4khAc9aWLXvSAk5-hvqZqt-a-90pFXSEzcWF4t2iFlx_siNI5sCGet_KhxXQpmGAFuSIoCiKYEn8QTfv4cuQNPknnZvlYpkhaUTK2PUGVtf21B2c45IYyDSsSbBStC8K0Wz89apJXFOOPk8Rr_1VcMf2tMqeBAxP7OkS84vI3xnnUhppznYMjZ6MflTtCXDBBAhupW6o4UU5E0ZOs5U3c36v0eaJ5vSSgyHXKN-GsahzVdW7AKe9rm3pGT4iKxi8428LYo8KgCcZOdEqes5EIYproFQJNvjX_yju2CIo6mPbhXhZ6WOSLDZfYG7Qlpv0FEeOaa1tG3-JdPYGuN0kuWBrK4vmCE7F8ddjmYoWkGFIk5mIMbzNolcyk5A8NFz7eBthE-OQMlfm7IkncGxErN5zBPae8oY4OETkoAzEpoh0Oh_cz1XjBJNiH-BAkCdVLA2vCfiu_5EDY13U3mDNu5wGOQrYfE2xWdNuu6SfuN62LIv5-IZU_kCx7UwzrScrNVe_rcYpMFBwVLWjExH5Q-s7rvfaIz573GpFlXt-vda1aSL9DC9uRbzN6y4uizu-mrVhFB271QR_Sx84vk8UPgNZn76U5dGEaFnn1pI6K_UGW9j5hfwNgRO0qbDYZtj62o-23EK9EtC_Va5ghf2OPTJNNsZTz4fQ-8cjOY5cVl3pz-St5nSdPrVVNYrSdbrT6hSzVu8tVOHDojJWT0uo3RLlXs9B4_h4RCUphtd7B4b7s9VeVcrxo5zuxkhqmIWeSE2J4F4GHFOS8R5Nh48B6KWDw5gqDV276IVMY7F3A4ttuFahUrV1be1plnGw1wTuvwWe-g3U65RjMHJwoIoKxzK3T-28JxjDkuGTBNPHEiq1NRo-OprR4_vCftEkz5uINAKGOeulVFAIbsVXxySFcto0oyQjNUaoaE_fPaHGhJ5-ipme_I76579__P0zOmKElGSmxZjF1vSL6J-m-0lS1o1mawZXkqB5WMM78HFDu2lF6O3qBMaxaFSunVUsnx6OPtbswBESdIZYUrKKWrqQKjoWVIFD6snXYuknOEpn5TlUgvD8LFjJGL0yBKidQIHMspP3eIlC_PWA8qEwhtrkBp3GV55k663DbLnh3A65ZAUbagpNrEN1kVNWN3dj4bDa4VRzJeFWfNLIjIeOl_M3m-22UE4vYXXLrVpGTUhbVrLZrNrmntoJxS8tKzH73jBQYsH5l0jlgWBpazCmqdUl4hag8rcgqJfR-fz0SCz7JdpjrFNHelxCf8TGuen3n4ZGY9X7YVDEMIanyHqDrD7jkxzd79KqhoonnGphOGyYiEpZE0zIrF_sb9ojr-zgcdA2NoPzBl98_1pHhKuj-LcuQDu9KspF0e-Rk0kmmaC2_zcPEjiyv6PxBLxjCAQ_FjsVtMbmv9GnSmL9MdpuD4Ihs5yERvx98uNl6cpIVguzu4kerk-3gtlmsP8OBLI9ncQmOO5o0xBUGntbS4ysshoIH5hWcw56FNesPgxPuIf9H2jpbKFhlSmGI1335-aWEK3LLoDeImT0fBTMAG8xsoTUQfTD8FiTIgvcHAzy_tg17Y4C4nIPtlEnOGJOv9iNidjk0XCurXcFaBMOEb5SmzUpYsCK5JOXYcUzgm-Hck_CcFM3s5xdLUt6GeCOuQRW65wR-HdBJCwCiNqZdq373Ww_XHxgJJVa_yYxfICEY6XoS-Z1-zU7PoTlNYzCN9iqU7XmTo5sTnVQtUEV17IHuam6lI-CICJHE52Q7fCIdKMQIo2aksC0TjmTO-riDiwN-2GI4zY7BuAG1oXuMh_8YBFQ1-BYi-Z3yh3KS9PAoxWdydZUqWlZljxICB_Jd7NGhN22FTN2MTjH23cmns731Oyzqbv5p4-EZlM4DOI51QZ3312R5v1TZR-iKTu6qeNW6KGMgUIhaHBbNkFWMchTpsiR2nRkzwRO5YvPVx3Fjys-FuLtBZE5BuM1op4pQsoSeQXQp8U7soa1M9a6PjHjGHmnnYW1Fe0nkdQBE1j3v6MzSoegdCDklH2JZS47glGJxYs8wqtEYcL_zruLB1i2uo9n9r39XC5t5Pv3cW27elrSQErxLMQplpWqyFj7SE8ZD6pT70O9XRq08ZxGp7GRNV-BftF0DN0MEKaFmrtG2CVhTFzNwr-j9Rtu8vKGThBbNWGAiPOs53c75Q9FrlK3a3LETFXpLmYT2peDrwApBIMo_Keovx9QKpThCMMGY7MtckAXfYAzuxQ5R1a01GvQZImb_b-mTX1sfaIwN_fLf6B9ll-o3ZXC7XxMr2A8LI7fz18EA2uNG3MId07jHhawCUnprHadz0JFpsqWMdYtAu22h-xdiayG7duhN1Kqto_H1XFs5B8OSqHnWOFb9ZF690HUJ6RIDf3dVjZCAdT-wCUd5RrlPbsUx_BY-bP1y3O-J6_KEHv5aaS6wXioV91nEvEh_4WoabJJQCWFGNZbGQ7vOo_82o-PUR5lSw7ouJuQI-UT1vOzc6BDiGnuvrnbdEtZ7lVuvJsuloSxpjK9CtzH-ODKlLkq8vVESh_VVtmR9HrHla_o4cRgqtPXmubYmyrBZvw-Ww-lVChUgqi5eqdU2UyGk1dZmXpjgh8hYWF5vGTTbVRSTadzkqB5AQk9A29qG_eAdoMdCEacrkmXJe6b0TQEY80HBNpj3Xa45Z7pplJDVQZEz_hWinEingBa1SEBVDJvqoIjw_ZK2PMxHcocyKRGgK2a_kAFdL0vZgpXwN7k1z2-qFFnvbCJRrd7B7T-xpR5NpAV0tVidXBYGi1vVlz7LTMUN32iTfWT4EnI7S5OepyA8CmsdLiRlW8LBRUSkKVjk1-darvgX6GlATc5wSAv-M7lVXZwwEozhurpd2EY90yNs9_hgoGZ-DydSG_zN6r2_QoozEIu_k6fFWT8x0q0dtu8IEWds7ZsbXsUEIeOwEFbiZolXnEI7wr7s1pqLqWJozEbpxZF1BiSdmuwFyDY_vz-HPLFLcMLJsvbXKzcMuCoIlAmPFcqkQWQnByamxCO-4UEDIgP-BCniorQEV-I9i4cS9wLrWByNpz_WEedCLe7CEAICOmsogicwtPsIHXcQsdD0lR0MAb0VO1S5MQYsLRNNQwMAuNglvVj3y0PtHJshYSGcp7D-k1piAr-WtHwxejQku6fxkkAUoBrmHVFk8RXwlh3K6B2lrh1kXEZriRo2Puyb_8A2XmMCzL-UzgwcjBz2GzgOyBY2ooaH4nwF3G_4R929txYsWzmXlT00sddCdxilFTw6oyvvGQcnYke8MvjtSvkuqosG7wppBTZuPoUtt9BslZ-DNQRkSXSwun8AY7ZpyBGh-uJuvG7YCQuvczLjIcr1fXzGtrTAUGmxCl8PLYPy1BqSvT8gbMyiGUNgyhtm8IKdyFbpcu9l2sPSwOasChRdAJfXnl5Nt8ikSgvcjB3JVh_IYFUYEAMG9HJldg9vPixo4KgtKaexwHbQ6n0Qo5fK-TRDLUSzot2POnfeWl6dyIdztJBpJ0e_91JndcjhHkMURZqVvPK8ebFbNGRATTPOJPg7FBJsMWEofdLQtr86kRMsD2SvQboe4zo_Yy2Y3Vg6e-t37-6IJU1pda35rrqFjcrFPUQj8tiepMCVnDXzsBQfMpdYFt0nZ4yvPINvl0KL0geDcxBgt6X3e5DhdejYzzhsfaX3EhDUyUy-V8546Hx--XPIY5CuzHeg6nC0hLPJslX9yteAiW6tFWKxQHsBbIsrH30XX1qq8z4rtoktmdlonJkQezWFlQBRZv2rqwNBSjPNmxRuwAMi07iC0R4aEB_PyHzAMoIjuQCSK9hPlByb6RGFdjzIuxLmPs3HEu_WmFhguEFhFpxzKwue4xrhMEX1v0SGd71LyIwYq83io1rGVwRgFo6aKsX8Cpvm9e0mNjiQ9y5S3V2iJLY36KSAgC-JNGgUupiMTAzUadCR4V2xoGbtdWBwL5SGQAJCPHt5U7xwvbbskZ0fH_Dy2P8ZVNmF62YhEbbodW6rl2A1fvIu4cQAcFU0K82pQKaUmcFQiSgZhdo6zvxAWajNgFfgfjjofzzeWfjHLjpVg0JF6_NJqhDmLUe3IR249RTaaVZewHHc7rCyBN-JOkIsM9x1wRxFuwUZbaNs9cOVLJAZprreDMPPdru2rYcxkMqKDa0DBZgZxcoHJ54eqP95-0gB1q5B2CRoxfkJksjnva2nrb9RRl6k6DewUU6aUuYZzedI1gHUcZiCa2rHjg2nhpW9H6wA-UpyMlDnuEIp_2rHkg1Q8UY5gJWYMHvuNl3pnik-tMz3gGMjP1_snMYRtCBnio-u2LWzO2qwzE2rZpmkc1HuBdfwhjHU0R82igoSMW1H0lJTbBo2vG5e0QW6qx4eSi3nxzj83aZJWmrBhmyI5zG2drnniMhPwKLc2M7zQvpUhKA9hb48P2jG-RggUE_sMAkFyjTEvDO1FqAtaYqYv85i0iW1Q5Z07A3rNYYy0LG0jte0XX9PjQWyu_LTfBpXruBHWvCB8VNds0b05KFyjrmjEDavzII4Mj6oqbdNE0wG570Su2oWsA9qN7Y_urFXqtu-yWEG2QG781T22IWkLxjjp5uHgX1RXwOHcGSL2fVCgtWMU1fu5cave5bWiM4Akm_tR8HDoxUtV3fx1kt0Nd4oSEcvnNM1omkIu8dH1gWSfztsM2sJ3a2qYDeZN8bK-A3akfbYGwa9gcQk2i5ecgK6W5eGw4NBVTxYWPABKTPdYWfCBfqMOF41Q5hf0g9PnDuJJ0b0dG1ttt2qmkM5Q8B8WMk8_0Y5QuB7XwRxrRW581sW1nW9cwCww1IK3fUKKw1122p8sCkwxjnPm13ZEkxuMQ2GqdHTzBVXe-2noO-6of9_jHcQpJ3GkM9PDWniOntrM2p8LCWie5YZs3LaBAXALeJkt5fZSC0rSoxN0vePjWSY6gET-hmKXXL3xUh056O7CelsuK75S1xO0Km7JVPlOWf65qOAD0vlkEymMZ2gC5b0iKDeNQ2su25pSzsWjs2Xix9l1_jQuH8_Y8yP8WjIz954KoNrwVF3vGVpaoSyVtsGWURlQPG3X5T-Yv2s6Z7txz9SdJsGLBGHOy5N_MibheYCLVURRc9qz9JwCDw9efVuqJwAevwuiYvgub0-c5ha_ulW07yIjXEBneRWEpYnRnQ2JMEIu2YU0_j0TJV6XHLjKMJdApDTcvwes5NipLByHTLsHC9gXzcKwBQxgAgkk1_Mx--HFfOaUmvrwSu-yLICsb3FvtXxvwfQqOTuUGkRKpdW4S-f5YxDn35L-lXONEl6gqYmKZMiRNMIhPxDHCsVqlWzjrPwD1_SOMSMqUJeRBJL73tsdh3KRJMrS315kgiv6lNkasyIxndK8xVeWO7mOinrC4CWGutMfwDHon6Xb9XjHwtQDaPg6UlUmlDNNVhAIvQUKvArt9d7GIeejj2khR6LeFuB2D9AjZ5HIexCAPoRgFjaO7vPP_Qq7JkBMzHT1tUouNqGgbMRvgbMYffDAPPXHzqZaVuEgHZZbXaGZi8fK9TZYLzHAuHz0mkF61B574hLMwaz_vKdCE7nEOZBHIrd9XNCF5RxmhEL6IbzkfhNRqlvq0fQA5qgfI_8L2RUqXzBey2atlI1_bblFwOqxFR62cAkysl0wKdu7JjAkpEhbN6qzMhaAMTILCNhUZUbEDbf9JxFtd87w03qNfT3cdHmgaAj7gvBaF8JQHybRv3QzrMVXGbUwkeat2jSKcXKDPZOUJJT7L2slOaA5pGraNMO8gTW1nj06mzJHK30pNLhKggLBZMkx9VQgSLFeGnkrUrqnQ_RoZBEKSreQ3tThEXMBIjtddJDWC1dz4eBwIvXUEgfTENS12d5fAqksA4l7eM5is6frjXnVmQsr3uY8jKsKlCPmfVwxkYEgTLaaX_4TYLfsMd-YNNdSX1RPyQXj_Ps5lfb-mQ1lFkgWRsUn3jeNkTvlz6ZhKQLGMyMpir7Rs_MoLQkeeJtPV5LQznrMg-3swmceT1Ye0HRSYWCIgdwcprNBwcULvMHbFZQHCwlgpyPmLkSkhZRHwbVPGU4jse4cIiSoYgQspPesn_DPu1Ik4OPf8vwPijrlKMsbWSzRJw9mxRlJNh5gcot6jc_suKhloaZtflatQsKfjlnmo7nQqWXltsnxfT5WaPMyD6G-jjuyZ3b5cmsaCFt-VjNyY3po48vQ1kpyZyP2qCeVjce_hDzy61RhB88kHgqgbVeY5ortbhrvlN1hrVAWrwtaO3fF7UbE6rOwEgqSR_FJxvMZS0cQp4-TVAD3hjN3bGxN7hXmKX-YEjEmgIM2zOtYBe6OYOc-GUitMcTYfAiddAJrF6wpTC6cQT2d3oRJ3z3nSnVLo9c5RDuvayg61wTCHNyoGrxxhwRfLUUkR4PEUgTEDoyzC-TKRWPbnkR5V65sJbmii9MJgwWd7zm43foJb_ceOmAPauiE79nqZSnSymxNfFpVusJHT7vwb_FUzQokE2iNRcsYAhN4mChTDBlR3nkbVlw_yJpvyO3tIBCvCd1cKQtiHbRzg3dq47vGGhSPux9iLV_Ns8uTVeqP7IJ4BXpqaapYfzF0sk8ZclDGcK6jSMeC7ohhSMze9tP6iJfyZ6BbFb4nbLvZBzU-NnhDzK_Ia-jCjKAIkh5xforoB36pVtKYZMX5BIlS90DUDHESjmnvQkQmowFQK3U7tN3LCY166j3Zpn8eDRbig7sOW8sZqk5RqWWvVKlZQ5Kx8BrrjTqPrPxRTrCMwgVPeZBvoUX9IZ6zSMWyR4JRfZ-vQLcVr4Urs-1RDbY16zgISncgMYNjzIQSWfRxIcjwrvIEQBFIHmhnTnLlIGvL8r6pZiyL043B__kF_K-dwpmJtaNSSGXyEWS-XAVHwyPsCndDhdCV9etscOynj5nQOqEdhJMgO9jfWJgKg9DbrfFWjfjqFtMZj2uARgqE4Yz7Onb295gCLAQOqLC54w-RCuNhJEACv-cgyzZwyRhPQNOUitFouAmTbVqtNtW75_yC1NC4KpL4hIIXoPeU8nueSdRulSHF5gsa6DhF8xLYp14C8fglZW7MvHFRjpSTEWrpip6DZWm-AFXM70JF0EDlTga8_nIkHregEQ44NCfhHQAjkOQsxcSQKN3v64H2wRlElobgxVDlOzgsJs4rRHjFmjjskVRBnjkU_T3M-te1pu1wvsPSaUZOXuTICLSRrRI-s8Isr-JN4NPJ2bMy1QjBWnkqrzPLiyAEQiinQJRDJfunNLajQWgtreEeIcKmCQRejcoc7TSoYaJzIcLw_B5klp8rL2K3CTdu8FvJeqauTz2iriXA4sFcPtG4DlitxT1EcK-CdMxHygPcX-2mLgJ5B6JqG0iNYOyGOmNyzX7GelnZ364QSnBVmBWwN7L3-3PE4HNl4SmUUV2MVlNb5vzztSTSb_FXi3k-y3tZL_VvpD9VNa8uhuYOAYSB2UwS9ePJ6hH226pRquI6ZTC8ipRTuJ_uD-9RKbG12MPvgt1MVWpBjMw4HlCIyX9gP_pgM7b8V89Z0sIYjFvAPN3svZlzIlEICQo0LrjAYissfR2lpCSjvTTyxjOy7OntaEkfEHm7tCk4Aqvtx6oLYQrOUUpMkutUU51M6opaAB_i3XaKDPqs3NSeuTZnmloZ1CUIw0YNcpg9cBw1cNI-ZHw0Csw0sK46uoURqbaHtCKsEwfZM-SDnhQ7kuyJJ8M_iWYRpkAnZ6hkrDfruZ-CbL6sDAzArMMZl30acwPbKTqSFVRRL2ELvNXiA-hR8fTo7SlzFTx4QqhoE2SfpDYZqKguuy0f4GpVmomZTwiRajdy4fzJLzya92FqyEEdNtxz-UdfToNBJmJBJoJBt_oFhF5TD0FdrNcp3uxnPi_oiAn7_uGuBV8i-dy0
\ No newline at end of file
diff --git a/docs/logical_data_model.puml b/docs/logical_data_model.puml
index 850cd3df81..a67de22812 100644
--- a/docs/logical_data_model.puml
+++ b/docs/logical_data_model.puml
@@ -89,9 +89,9 @@ class ActivityReportGoals{
class ActivityReportObjectiveCitations{
* id : integer :
* activityReportObjectiveId : integer : REFERENCES "ActivityReportObjectives".id
- * citationId : integer : REFERENCES "MonitoringStandards".id
- * findingId : integer : REFERENCES "MonitoringFindings".id
- * reviewId : integer : REFERENCES "MonitoringReviews".id
+!issue='column reference does not match model: "MonitoringStandards".id !== "MonitoringStandardLinks"."standardId"' * citationId : integer : REFERENCES "MonitoringStandards".id
+!issue='column reference does not match model: "MonitoringFindings".id !== "MonitoringFindingLinks"."findingId"' * findingId : integer : REFERENCES "MonitoringFindings".id
+!issue='column reference does not match model: "MonitoringReviews".id !== "MonitoringReviewLinks"."reviewId"' * reviewId : integer : REFERENCES "MonitoringReviews".id
* createdAt : timestamp with time zone : now()
* updatedAt : timestamp with time zone : now()
}
@@ -2569,6 +2569,7 @@ Imports "1" --[#black,dashed,thickness=2]--{ "n" ImportFiles : import, importFi
MaintenanceLogs "1" --[#black,dashed,thickness=2]--{ "n" MaintenanceLogs : triggeredBy, triggered
MonitoringFindingHistoryStatusLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringFindingHistories : monitoringFindingStatusLink, monitoringFindingHistories
MonitoringFindingHistoryStatusLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringFindingHistoryStatuses : monitoringFindingHistoryStatuses, statusLink
+MonitoringFindingLinks "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportObjectiveCitations : finding, activityReportObjectiveCitationFinding
MonitoringFindingLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringFindingGrants : findingLink, monitoringFindingGrants
MonitoringFindingLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringFindingHistories : monitoringFindingLink, monitoringFindingHistories
MonitoringFindingLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringFindingStandards : monitoringFindingStandards, findingLink
@@ -2576,19 +2577,18 @@ MonitoringFindingLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringFindi
MonitoringFindingStatusLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringFindingGrants : statusLink, monitoringFindingGrants
MonitoringFindingStatusLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringFindingStatuses : monitoringFindingStatuses, statusLink
MonitoringFindingStatusLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringFindings : monitoringFindings, statusLink
-MonitoringFindings "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportObjectiveCitations : finding, activityReportObjectiveCitationFinding
MonitoringGranteeLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringFindingGrants : granteeLink, monitoringFindingGrants
MonitoringGranteeLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringReviewGrantees : monitoringReviewGrantees, monitoringGranteeLink
+MonitoringReviewLinks "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportObjectiveCitations : review, activityReportObjectiveCitationReview
MonitoringReviewLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringClassSummaries : monitoringReviewLink, monitoringClassSummaries
MonitoringReviewLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringFindingHistories : monitoringReviewLink, monitoringFindingHistories
MonitoringReviewLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringReviewGrantees : monitoringReviewLink, monitoringReviewGrantees
MonitoringReviewLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringReviews : monitoringReviews, monitoringReviewLink
MonitoringReviewStatusLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringReviewStatuses : monitoringReviewStatuses, statusLink
MonitoringReviewStatusLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringReviews : monitoringReviews, statusLink
-MonitoringReviews "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportObjectiveCitations : review, activityReportObjectiveCitationReview
+MonitoringStandardLinks "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportObjectiveCitations : citation, activityReportObjectiveCitations
MonitoringStandardLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringFindingStandards : statusLink, monitoringFindingStandards
MonitoringStandardLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringStandards : monitoringStandardes, statusLink
-MonitoringStandards "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportObjectiveCitations : citation, activityReportObjectiveCitations
NationalCenters "1" --[#black,dashed,thickness=2]--{ "n" EventReportPilotNationalCenterUsers : nationalCenter, eventReportPilotNationalCenterUsers
NationalCenters "1" --[#black,dashed,thickness=2]--{ "n" NationalCenterUsers : nationalCenter, nationalCenterUsers
NationalCenters "1" --[#black,dashed,thickness=2]--{ "n" NationalCenters : mapsToNationalCenter, mapsFromNationalCenters
@@ -2649,7 +2649,10 @@ ActivityReportCollaborators "n" }--[#black,dotted,thickness=2]--{ "n" Roles : ro
ActivityReportGoals "n" }--[#black,dotted,thickness=2]--{ "n" Resources : resources, activityReportGoals
ActivityReportObjectives "n" }--[#black,dotted,thickness=2]--{ "n" Courses : courses, reportObjectives
ActivityReportObjectives "n" }--[#black,dotted,thickness=2]--{ "n" Files : files, reportObjectives
-ActivityReportObjectives "n" }--[#black,dotted,thickness=2]--{ "n" MonitoringStandards : citations, activityReportObjectives
+!issue='associations need to be defined both directions'
+ActivityReportObjectives "n" }--[#d54309,dotted,thickness=2]--{ "n" MonitoringStandardLinks : activityReportObjectives
+!issue='associations need to be defined both directions'
+ActivityReportObjectives "n" }--[#d54309,dotted,thickness=2]--{ "n" MonitoringStandards : citations
ActivityReportObjectives "n" }--[#black,dotted,thickness=2]--{ "n" Resources : resources, activityReportObjectives
ActivityReportObjectives "n" }--[#black,dotted,thickness=2]--{ "n" Topics : topics, activityReportObjectives
ActivityReports "n" }--[#black,dotted,thickness=2]--{ "n" Files : files, reports
@@ -2679,4 +2682,11 @@ Roles "n" }--[#black,dotted,thickness=2]--{ "n" Topics : topics, roles
Roles "n" }--[#black,dotted,thickness=2]--{ "n" Users : users, roles
Scopes "n" }--[#black,dotted,thickness=2]--{ "n" Users : users, scopes
+!issue='association missing from models'!issue='associations need to be defined both directions'
+MonitoringFindings o--[#yellow,bold,thickness=2]--o ActivityReportObjectiveCitations : missing-from-model
+!issue='associations need to be defined both directions'
+MonitoringReviews o--[#yellow,bold,thickness=2]--o ActivityReportObjectiveCitations : missing-from-model
+!issue='associations need to be defined both directions'
+MonitoringStandards o--[#yellow,bold,thickness=2]--o ActivityReportObjectiveCitations : missing-from-model
+
@enduml
diff --git a/src/services/citations.js b/src/services/citations.js
index 2d7875fcdb..73d2c5192b 100644
--- a/src/services/citations.js
+++ b/src/services/citations.js
@@ -1,15 +1,117 @@
+/* eslint-disable no-plusplus */
/* eslint-disable import/prefer-default-export */
import { sequelize } from '../models';
/*
The purpose of this function is to get citations by grant id.
- We then need to format the response for how it needs to be displayed on the FE.
+ We then need to format the response for how it needs to be
+ displayed on the FE for selection on objectives.
*/
export async function getCitationsByGrantIds(grantIds) {
- /* Utilize Garrett's suggestion here for getting citations for all grants in the grantIds array */
- const citationsFromDB = await sequelize.query(
+ /*
+ Questions:
+ - Do we need to take into account the grant replacements table? (what if a grant was replaced?)
+ - Is it enough to join on grant number? Or do we need to use links table?
+ */
+
+ const cutOffStartDate = '2024-10-01'; // TODO: Set this before we deploy to prod.
+
+ // Query to get the citations by grant id.
+ const grantsByCitations = await sequelize.query(
+ /* sql */
`
+ SELECT
+ ms."standardId",
+ ms.citation,
+ JSONB_AGG( DISTINCT
+ JSONB_BUILD_OBJECT(
+ 'findingId', mf."findingId",
+ 'grantId', gr.id,
+ 'grantNumber', gr.number,
+ 'reviewName', mfh."name",
+ 'reportDeliveryDate', mfh."reportDeliveryDate",
+ 'findingType', mf."findingType",
+ 'findingSource', mf."source",
+ 'monitoringFindingStatusName', mfs."name"
+ )
+ ) grants
+ FROM "Grants" gr
+ JOIN "MonitoringReviewGrantees" mrg
+ ON gr.number = mrg."grantNumber"
+ JOIN (
+ -- The below 'DISTINCT ON' determines the single record to return values from by the 'ORDER BY' clause.
+ SELECT DISTINCT ON (mfh."findingId", gr.id)
+ mfh."findingId",
+ gr.id AS "grantId",
+ mr."reviewId",
+ mr."name",
+ mr."reportDeliveryDate"
+ FROM "MonitoringFindingHistories" mfh
+ JOIN "MonitoringReviews" mr
+ ON mfh."reviewId" = mr."reviewId"
+ JOIN "MonitoringReviewGrantees" mrg
+ ON mrg."reviewId" = mr."reviewId"
+ JOIN "Grants" gr
+ ON gr.number = mrg."grantNumber"
+ ORDER BY mfh."findingId", gr.id, mr."reportDeliveryDate" DESC
+ ) mfh -- Subquery ensures only the most recent history for each finding-grant combination
+ ON mfh."grantId" = gr.id
+ JOIN "MonitoringFindings" mf
+ ON mfh."findingId" = mf."findingId"
+ JOIN "MonitoringFindingStatuses" mfs
+ ON mf."statusId" = mfs."statusId"
+ JOIN "MonitoringFindingStandards" mfs2
+ ON mf."findingId" = mfs2."findingId"
+ JOIN "MonitoringStandards" ms
+ ON mfs2."standardId" = ms."standardId"
+ JOIN "MonitoringFindingGrants" mfg
+ ON mf."findingId" = mfg."findingId"
+ AND mrg."granteeId" = mfg."granteeId"
+ WHERE 1 = 1
+ AND gr.id IN (${grantIds.join(',')}) -- :grantIds
+ AND mfh."reportDeliveryDate" BETWEEN '${cutOffStartDate}' AND NOW() -- Between is inclusive.
+ AND gr.status = 'Active'
+ AND mfs.name = 'Active'
+ GROUP BY 1,2
+ ORDER BY 2,1;
`,
);
- return ['bad'];
+
+ // From the response we need to get a list of citations for each grant.
+ const citationsByGrant = Object.values(grantsByCitations[0].reduce(
+ (acc, citation) => {
+ const { grants } = citation;
+ // Create a for loop to iterate over every object in the grants array.
+ for (let i = 0; i < grants.length; i++) {
+ const grant = grants[i];
+ // Check if the grant number is already in the accumulator.
+ if (!acc[grant.grantId]) {
+ acc[grant.grantId] = {
+ grantId: grant.grantId,
+ citations: [],
+ };
+ }
+
+ // Build a citation object to push into the array.
+ const citationObject = {
+ findingType: grant.findingType,
+ findingSource: grant.findingSource,
+ grantId: grant.grantId,
+ standardId: citation.standardId,
+ citation: citation.citation,
+ findingId: grant.findingId,
+ reviewName: grant.reviewName,
+ grantNumber: grant.grantNumber,
+ reportDeliveryDate: grant.reportDeliveryDate,
+ monitoringFindingStatusName: grant.monitoringFindingStatusName,
+ };
+ // Push the citation into the array for the grant number.
+ acc[grant.grantId].citations.push(citationObject);
+ }
+ return acc;
+ },
+ {},
+ ));
+
+ return citationsByGrant;
}
diff --git a/src/services/citations.test.js b/src/services/citations.test.js
index e69de29bb2..a3807cb57d 100644
--- a/src/services/citations.test.js
+++ b/src/services/citations.test.js
@@ -0,0 +1,355 @@
+/* eslint-disable max-len */
+/* eslint-disable prefer-destructuring */
+import { v4 as uuidv4 } from 'uuid';
+import faker from '@faker-js/faker';
+import { getCitationsByGrantIds } from './citations';
+import db, {
+ Recipient,
+ Grant,
+ MonitoringReviewGrantee,
+ MonitoringReview,
+ MonitoringFindingHistory,
+ MonitoringFinding,
+ MonitoringFindingStatus,
+ MonitoringReviewStatus,
+ MonitoringFindingStandard,
+ MonitoringStandard,
+ MonitoringFindingGrant,
+} from '../models';
+import { captureSnapshot, rollbackToSnapshot } from '../lib/programmaticTransaction';
+
+// create a function to create a citation for a grant.
+const createMonitoringData = async (
+ grantNUmber, // Grant Number.
+ reviewStatusId, // Status ID.
+ reportDeliveryDate, // Report Delivery Date must be between start cutoff and todays date.
+ reviewType, // Review Type must be in ('AIAN-DEF', 'RAN', 'Follow-up', 'FA-1', 'FA1-FR', 'FA-2', 'FA2-CR', 'Special')
+ monitoringReviewStatusName, // Monitoring Review Status Name must be 'Complete'.
+ citationsArray, // Array of citations to create.
+) => {
+ const reviewId = uuidv4();
+ const granteeId = uuidv4();
+
+ // MonitoringReviewGrantee.
+ await MonitoringReviewGrantee.create({
+ id: faker.datatype.number({ min: 9999 }),
+ grantNumber: grantNUmber,
+ reviewId,
+ granteeId,
+ createTime: new Date(),
+ updateTime: new Date(),
+ updateBy: 'Support Team',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ }, { individualHooks: true });
+
+ // MonitoringReview.
+ await MonitoringReview.create({
+ reviewId,
+ contentId: faker.datatype.uuid(),
+ statusId: reviewStatusId,
+ name: faker.random.words(3),
+ startDate: new Date(),
+ endDate: new Date(),
+ reviewType,
+ reportDeliveryDate,
+ reportAttachmentId: faker.datatype.uuid(),
+ outcome: faker.random.words(5),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ }, { individualHooks: true });
+
+ // MonitoringReviewStatus.
+ await MonitoringReviewStatus.create({
+ statusId: reviewStatusId,
+ name: monitoringReviewStatusName,
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ }, { individualHooks: true });
+
+ // MonitoringFindingHistory (this is the primary finding table and the relationship to citation is 1<>1).
+ // If we wanted one grant to have multiple citations, we would need to create multiple findings here and below.
+ await Promise.all(citationsArray.map(async (citation) => {
+ const findingId = uuidv4();
+ const findingStatusId = faker.datatype.number({ min: 9999 });
+ await MonitoringFindingHistory.create({
+ reviewId,
+ findingHistoryId: uuidv4(),
+ findingId,
+ statusId: findingStatusId,
+ narrative: faker.random.words(10),
+ ordinal: faker.datatype.number({ min: 1, max: 10 }),
+ determination: faker.random.words(5),
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ sourceDeletedAt: null,
+ }, { individualHooks: true });
+
+ // MonitoringFinding.
+ await MonitoringFinding.create({
+ findingId,
+ statusId: findingStatusId,
+ findingType: citation.monitoringFindingType,
+ hash: faker.datatype.uuid(),
+ source: 'Internal Controls',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ }, { individualHooks: true });
+
+ // MonitoringFindingStatus.
+ await MonitoringFindingStatus.create({
+ statusId: findingStatusId,
+ name: citation.monitoringFindingStatusName,
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ }, { individualHooks: true });
+
+ // MonitoringFindingGrant.
+ await MonitoringFindingGrant.create({
+ findingId,
+ granteeId,
+ statusId: findingStatusId,
+ findingType: citation.monitoringFindingGrantFindingType,
+ source: 'Discipline',
+ correctionDeadLine: new Date(),
+ reportedDate: new Date(),
+ closedDate: null,
+ hash: faker.datatype.uuid(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ sourceDeletedAt: null,
+ }, { individualHooks: true });
+
+ // MonitoringFindingStandard (this table joins a finding to a standard (citation)).
+ const standardId = faker.datatype.number({ min: 9999 });
+ const citable = faker.datatype.number({ min: 1, max: 10 });
+ await MonitoringFindingStandard.create({
+ findingId,
+ standardId, // Integer
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ }, { individualHooks: true });
+
+ // MonitoringStandard.
+ await MonitoringStandard.create({
+ standardId,
+ citation: citation.citationText,
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ contentId: uuidv4(),
+ hash: uuidv4(),
+ citable,
+ }, { individualHooks: true });
+ }));
+};
+
+describe('citations service', () => {
+ afterAll(async () => {
+ await db.sequelize.close();
+ });
+ describe('getCitationsByGrantIds()', () => {
+ let snapShot;
+
+ let recipient1;
+ let recipient2;
+
+ let grant1; // Recipient 1
+ let grant1a; // Recipient 1
+ let grant2; // Recipient 2
+ let grant3; // Recipient 2 (Inactive)
+
+ beforeAll(async () => {
+ // Capture a snapshot of the database before running the test.
+ snapShot = await captureSnapshot();
+
+ // Grant Numbers.
+ const grantNumber1 = faker.datatype.string(8);
+ const grantNumber1a = faker.datatype.string(8);
+ const grantNumber2 = faker.datatype.string(8);
+ const grantNumber3 = faker.datatype.string(8);
+
+ // Recipients 1.
+ recipient1 = await Recipient.create({
+ id: faker.datatype.number({ min: 64000 }),
+ name: faker.random.alphaNumeric(6),
+ });
+
+ // Recipients 2.
+ recipient2 = await Recipient.create({
+ id: faker.datatype.number({ min: 64000 }),
+ name: faker.random.alphaNumeric(6),
+ });
+
+ // Grants.
+ const grants = await Grant.bulkCreate([
+ {
+ // Grant 1 for Recipient 1.
+ id: faker.datatype.number({ min: 9999 }),
+ number: grantNumber1,
+ recipientId: recipient1.id,
+ regionId: 1,
+ startDate: new Date(),
+ endDate: new Date(),
+ status: 'Active',
+ },
+ {
+ // Grant 1a for Recipient 1.
+ id: faker.datatype.number({ min: 9999 }),
+ number: grantNumber1a,
+ recipientId: recipient1.id,
+ regionId: 1,
+ startDate: new Date(),
+ endDate: new Date(),
+ status: 'Active',
+ },
+ {
+ // Grant 2 for Recipient 2.
+ id: faker.datatype.number({ min: 9999 }),
+ number: grantNumber2,
+ recipientId: recipient2.id,
+ regionId: 1,
+ startDate: new Date(),
+ endDate: new Date(),
+ status: 'Active',
+ },
+ {
+ // Grant 3 for Recipient 2 (Inactive).
+ id: faker.datatype.number({ min: 9999 }),
+ number: grantNumber3,
+ recipientId: recipient2.id,
+ regionId: 1,
+ startDate: new Date(),
+ endDate: new Date(),
+ status: 'Inactive',
+ },
+ ]);
+
+ // set the grants.
+ grant1 = grants[0];
+ grant1a = grants[1];
+ grant2 = grants[2];
+ grant3 = grants[3];
+
+ /*
+ Citation Object Properties:
+ citationText, // Citation Text
+ monitoringFindingType, // Monitoring Finding ('Deficiency', 'Significant Deficiency', 'Material Weakness', 'No Finding').
+ monitoringFindingStatusName, // Monitoring Finding Status name must be 'Active'.
+ monitoringFindingGrantFindingType, // Monitoring Finding Grant Finding Type must be in ('Corrective Action', 'Management Decision', 'No Finding').
+ */
+
+ // Create Monitoring Review Citations.
+ const grant1Citations1 = [
+ {
+ citationText: 'Grant 1 - Citation 1 - Good',
+ monitoringFindingType: 'Citation 1 Monitoring Finding Type',
+ monitoringFindingStatusName: 'Active',
+ monitoringFindingGrantFindingType: 'Corrective Action',
+ },
+ {
+ citationText: 'Grant 1 - Citation 2 - Bad MFS name',
+ monitoringFindingType: 'Citation 2 Monitoring Finding Type',
+ monitoringFindingStatusName: 'Abandoned',
+ monitoringFindingGrantFindingType: 'Corrective Action',
+ },
+ {
+ citationText: 'Grant 1 - Citation 3 - Good 2',
+ monitoringFindingType: 'Citation 3 Monitoring Finding Type',
+ monitoringFindingStatusName: 'Active',
+ monitoringFindingGrantFindingType: 'Corrective Action',
+ },
+ ];
+
+ // Grant 1.
+ await createMonitoringData(grant1.number, 1, new Date(), 'AIAN-DEF', 'Complete', grant1Citations1);
+
+ // Grant 1a (make sure other grant citations comeback).
+ const grant1Citations1a = [
+ {
+ citationText: 'Grant 1a - Citation 1 - Good',
+ monitoringFindingType: 'Citation 4 Monitoring Finding Type',
+ monitoringFindingStatusName: 'Active',
+ monitoringFindingGrantFindingType: 'Grant 1a Corrective Action',
+ },
+ ];
+ await createMonitoringData(grant1a.number, 2, new Date(), 'AIAN-DEF', 'Complete', grant1Citations1a);
+
+ // Grant 2.
+ const grant1Citations2 = [
+ {
+ citationText: 'Grant 2 - Citation 1 - Good',
+ monitoringFindingType: 'citation 5 Monitoring Finding Type',
+ monitoringFindingStatusName: 'Active',
+ monitoringFindingGrantFindingType: 'Corrective Action',
+ },
+ ];
+ // Before delivery date.
+ await createMonitoringData(grant2.number, 3, new Date('2024-09-01'), 'AIAN-DEF', 'Complete', grant1Citations2);
+ // After delivery date (tomorrow).
+ await createMonitoringData(grant2.number, 4, new Date(new Date().setDate(new Date().getDate() + 1)), 'AIAN-DEF', 'Complete', grant1Citations2);
+
+ // Grant 3 (inactive).
+ const grant1Citations3 = [
+ {
+ citationText: 'Grant 3 - Citation 1 - Good',
+ monitoringFindingType: 'Material Weakness',
+ monitoringFindingStatusName: 'Active',
+ monitoringFindingGrantFindingType: 'Corrective Action',
+ },
+ ];
+ await createMonitoringData(grant3.number, 5, new Date(), 'AIAN-DEF', 'Complete', grant1Citations3);
+ });
+
+ afterAll(async () => {
+ // Rollback any changes made to the database during the test.
+ await rollbackToSnapshot(snapShot);
+ });
+
+ it('correctly retrieves citations per grant', async () => {
+ // Call the service to get the citations by grant ids.
+ const citationsToAssert = await getCitationsByGrantIds([grant1.id, grant1a.id, grant2.id, grant3.id]);
+ // Get the first citation object for grant 1.
+ const grant1Citations = citationsToAssert.find((g) => g.grantId === grant1.id);
+ expect(grant1Citations).toBeDefined();
+ expect(grant1Citations.citations.length).toBe(2);
+
+ // Get the first citation ''Grant 1 - Citation 1 - Good'.
+ const citation1 = grant1Citations.citations.find((c) => c.citation === 'Grant 1 - Citation 1 - Good');
+ expect(citation1).toBeDefined();
+ expect(citation1.findingType).toBe('Citation 1 Monitoring Finding Type');
+ expect(citation1.findingSource).toBe('Internal Controls');
+ expect(citation1.grantNumber).toBe(grant1.number);
+
+ // Get the second citation ''Grant 1 - Citation 3 - Good 2'.
+ const citation2 = grant1Citations.citations.find((c) => c.citation === 'Grant 1 - Citation 3 - Good 2');
+ expect(citation2).toBeDefined();
+ expect(citation2.findingType).toBe('Citation 3 Monitoring Finding Type');
+ expect(citation2.findingSource).toBe('Internal Controls');
+ expect(citation2.grantNumber).toBe(grant1.number);
+
+ // Look for Grant1a.
+ const grant1aCitations = citationsToAssert.find((g) => g.grantId === grant1a.id);
+ expect(grant1aCitations).toBeDefined();
+ expect(grant1aCitations.citations.length).toBe(1);
+
+ // Get the first citation ''Grant 1a - Citation 1 - Good'.
+ const citation1a = grant1aCitations.citations.find((c) => c.citation === 'Grant 1a - Citation 1 - Good');
+ expect(citation1a).toBeDefined();
+ expect(citation1a.findingType).toBe('Citation 4 Monitoring Finding Type');
+ expect(citation1a.findingSource).toBe('Internal Controls');
+ expect(citation1a.grantNumber).toBe(grant1a.number);
+
+ // Looks for Grant2.
+ const grant2Citations = citationsToAssert.find((g) => g.grantId === grant2.id);
+ // Assert we don\'t have any citations for grant 2.
+ expect(grant2Citations).toBeUndefined();
+
+ // Looks for Grant3.
+ const grant3Citations = citationsToAssert.find((g) => g.grantId === grant3.id);
+ // Assert we don\'t have any citations for grant 3.
+ expect(grant3Citations).toBeUndefined();
+ });
+ });
+});
From e3f0f6c02acd363a43b887cbcc983bcd8b954d7b Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Fri, 22 Nov 2024 09:32:02 -0500
Subject: [PATCH 023/198] see if we need these for now
---
docs/logical_data_model.encoded | 2 +-
docs/logical_data_model.puml | 13 +++++++------
src/models/monitoringFindingLink.js | 3 ++-
src/models/monitoringReviewLinks.js | 3 ++-
src/models/monitoringStandardLink.js | 5 ++++-
5 files changed, 16 insertions(+), 10 deletions(-)
diff --git a/docs/logical_data_model.encoded b/docs/logical_data_model.encoded
index 234d1eafcc..5f6fd30d87 100644
--- a/docs/logical_data_model.encoded
+++ b/docs/logical_data_model.encoded
@@ -1 +1 @@
-xLrlSzscqd-lJw6wBtAREUdJfkqpKq_RkQvZfvcRE1xRwPtRJiU39SuvD1AS2yYkswVV_Hb0Vm0X2IGT9sdroZu8iQiV2ooxxF9FnW7bHPv5SVme1hS4fV6EafiCvYo-GtmR0xp9O1edbDonU0qXZ-AOlKLu1oZ8umnXoEztC2QKRudnACa0O_4VKHp7yVU4ff0UaupG-EClllZVewy_qygljYXvYo5ZyTCeZl-Kh1oBjo6BuhATeuIZMyJlBs22TWXYplw8uiSnIkF3664EDv36X_4t6uWX1HoctqNnXWBCNvWLBavEJovEpevFBkE3v-8vE_WKfL5C-1RI4ymHl-zvutLT2y7onSSng3dR4Tf7xyYeMB-VK2WOFkBnOSnH3Xa7-KvXBtx6lnCCuyCOaxj_V2Bg5xlKl_wVKMJZ9uWVxNQKt48w3EBqxoiOf3rllM6GFWmMSGepE5GxYZ7XuXqEV-DHp3ZW1OiFOuYBl0lDOv9bu8PGmCdV2z4EF4vH1Xy2XpNAOCzRaeqG77o_l-kxdnEGdIAOfHUGxGXc8q0GJNW38IhN7xWX8Bk2-Iu374ecpYd9Txtpd_kD_K0MqnAJScZz_4iVYXUGaO8c3meWBMdsl5spjH-8LEEoAwefHy4a_fC_htkWkZPU_oEACSWLxh8yafJML1IMJVgtzit_FNdIrqtTFJdSWuIY3S8W6-Wvew-cpXoR05hpzd6I4GOl2xQ3E3qcM0o5Uh7Hdrr0m0Yk5fuOulGPaEkI81B5YCalWzdzIOeu59zoGqW6WNXb3J8cwrnm3dQWRaG8HBEooPwhdzKBtIsWoHRGVppzwgjFD9cftkiOjAzlVeMIeMF40KS4Xu_SkWdlxgpVK7tr4MAiWDy-IaXMv3YcS0qfn0cCKmAPMDRZ7F1a6-SaXTbXVF2AOCG9HNXpoG5E0KrbE_57ttxhUlWIuRViuDC3Llv-aHuy4k9HVd0Frns49Z1yYd2AyCR9R_ciOdUjVfRSrhyycDM9JE3r0juYUETaLJsgEANoLybezSE3Jut0kz9u6l4d1HspxayMVl7XadPF0oLpxmLGutJ4EM1wtxJ6ciFnIiySi7o0wayRkPOEf6i_F1D_E3Ydv1QbCErGWuPQTUX2xLS6klgAx53oRlgP2z8zxolMtia0yDSLVO1eV_fKhP6CehfVcmQWhuK9pU3dyz5gPls5nuhf7o25ZMhPgIMoOYU0kXA6mAefETvgcjgm6z0any-2uHtw0SvvtLFToV7BVntNZzymc4OnI5Cab5EGdH4khAc9aWLXvSAk5-hvqZqt-a-90pFXSEzcWF4t2iFlx_siNI5sCGet_KhxXQpmGAFuSIoCiKYEn8QTfv4cuQNPknnZvlYpkhaUTK2PUGVtf21B2c45IYyDSsSbBStC8K0Wz89apJXFOOPk8Rr_1VcMf2tMqeBAxP7OkS84vI3xnnUhppznYMjZ6MflTtCXDBBAhupW6o4UU5E0ZOs5U3c36v0eaJ5vSSgyHXKN-GsahzVdW7AKe9rm3pGT4iKxi8428LYo8KgCcZOdEqes5EIYproFQJNvjX_yju2CIo6mPbhXhZ6WOSLDZfYG7Qlpv0FEeOaa1tG3-JdPYGuN0kuWBrK4vmCE7F8ddjmYoWkGFIk5mIMbzNolcyk5A8NFz7eBthE-OQMlfm7IkncGxErN5zBPae8oY4OETkoAzEpoh0Oh_cz1XjBJNiH-BAkCdVLA2vCfiu_5EDY13U3mDNu5wGOQrYfE2xWdNuu6SfuN62LIv5-IZU_kCx7UwzrScrNVe_rcYpMFBwVLWjExH5Q-s7rvfaIz573GpFlXt-vda1aSL9DC9uRbzN6y4uizu-mrVhFB271QR_Sx84vk8UPgNZn76U5dGEaFnn1pI6K_UGW9j5hfwNgRO0qbDYZtj62o-23EK9EtC_Va5ghf2OPTJNNsZTz4fQ-8cjOY5cVl3pz-St5nSdPrVVNYrSdbrT6hSzVu8tVOHDojJWT0uo3RLlXs9B4_h4RCUphtd7B4b7s9VeVcrxo5zuxkhqmIWeSE2J4F4GHFOS8R5Nh48B6KWDw5gqDV276IVMY7F3A4ttuFahUrV1be1plnGw1wTuvwWe-g3U65RjMHJwoIoKxzK3T-28JxjDkuGTBNPHEiq1NRo-OprR4_vCftEkz5uINAKGOeulVFAIbsVXxySFcto0oyQjNUaoaE_fPaHGhJ5-ipme_I76579__P0zOmKElGSmxZjF1vSL6J-m-0lS1o1mawZXkqB5WMM78HFDu2lF6O3qBMaxaFSunVUsnx6OPtbswBESdIZYUrKKWrqQKjoWVIFD6snXYuknOEpn5TlUgvD8LFjJGL0yBKidQIHMspP3eIlC_PWA8qEwhtrkBp3GV55k663DbLnh3A65ZAUbagpNrEN1kVNWN3dj4bDa4VRzJeFWfNLIjIeOl_M3m-22UE4vYXXLrVpGTUhbVrLZrNrmntoJxS8tKzH73jBQYsH5l0jlgWBpazCmqdUl4hag8rcgqJfR-fz0SCz7JdpjrFNHelxCf8TGuen3n4ZGY9X7YVDEMIanyHqDrD7jkxzd79KqhoonnGphOGyYiEpZE0zIrF_sb9ojr-zgcdA2NoPzBl98_1pHhKuj-LcuQDu9KspF0e-Rk0kmmaC2_zcPEjiyv6PxBLxjCAQ_FjsVtMbmv9GnSmL9MdpuD4Ihs5yERvx98uNl6cpIVguzu4kerk-3gtlmsP8OBLI9ncQmOO5o0xBUGntbS4ysshoIH5hWcw56FNesPgxPuIf9H2jpbKFhlSmGI1335-aWEK3LLoDeImT0fBTMAG8xsoTUQfTD8FiTIgvcHAzy_tg17Y4C4nIPtlEnOGJOv9iNidjk0XCurXcFaBMOEb5SmzUpYsCK5JOXYcUzgm-Hck_CcFM3s5xdLUt6GeCOuQRW65wR-HdBJCwCiNqZdq373Ww_XHxgJJVa_yYxfICEY6XoS-Z1-zU7PoTlNYzCN9iqU7XmTo5sTnVQtUEV17IHuam6lI-CICJHE52Q7fCIdKMQIo2aksC0TjmTO-riDiwN-2GI4zY7BuAG1oXuMh_8YBFQ1-BYi-Z3yh3KS9PAoxWdydZUqWlZljxICB_Jd7NGhN22FTN2MTjH23cmns731Oyzqbv5p4-EZlM4DOI51QZ3312R5v1TZR-iKTu6qeNW6KGMgUIhaHBbNkFWMchTpsiR2nRkzwRO5YvPVx3Fjys-FuLtBZE5BuM1op4pQsoSeQXQp8U7soa1M9a6PjHjGHmnnYW1Fe0nkdQBE1j3v6MzSoegdCDklH2JZS47glGJxYs8wqtEYcL_zruLB1i2uo9n9r39XC5t5Pv3cW27elrSQErxLMQplpWqyFj7SE8ZD6pT70O9XRq08ZxGp7GRNV-BftF0DN0MEKaFmrtG2CVhTFzNwr-j9Rtu8vKGThBbNWGAiPOs53c75Q9FrlK3a3LETFXpLmYT2peDrwApBIMo_Keovx9QKpThCMMGY7MtckAXfYAzuxQ5R1a01GvQZImb_b-mTX1sfaIwN_fLf6B9ll-o3ZXC7XxMr2A8LI7fz18EA2uNG3MId07jHhawCUnprHadz0JFpsqWMdYtAu22h-xdiayG7duhN1Kqto_H1XFs5B8OSqHnWOFb9ZF690HUJ6RIDf3dVjZCAdT-wCUd5RrlPbsUx_BY-bP1y3O-J6_KEHv5aaS6wXioV91nEvEh_4WoabJJQCWFGNZbGQ7vOo_82o-PUR5lSw7ouJuQI-UT1vOzc6BDiGnuvrnbdEtZ7lVuvJsuloSxpjK9CtzH-ODKlLkq8vVESh_VVtmR9HrHla_o4cRgqtPXmubYmyrBZvw-Ww-lVChUgqi5eqdU2UyGk1dZmXpjgh8hYWF5vGTTbVRSTadzkqB5AQk9A29qG_eAdoMdCEacrkmXJe6b0TQEY80HBNpj3Xa45Z7pplJDVQZEz_hWinEingBa1SEBVDJvqoIjw_ZK2PMxHcocyKRGgK2a_kAFdL0vZgpXwN7k1z2-qFFnvbCJRrd7B7T-xpR5NpAV0tVidXBYGi1vVlz7LTMUN32iTfWT4EnI7S5OepyA8CmsdLiRlW8LBRUSkKVjk1-darvgX6GlATc5wSAv-M7lVXZwwEozhurpd2EY90yNs9_hgoGZ-DydSG_zN6r2_QoozEIu_k6fFWT8x0q0dtu8IEWds7ZsbXsUEIeOwEFbiZolXnEI7wr7s1pqLqWJozEbpxZF1BiSdmuwFyDY_vz-HPLFLcMLJsvbXKzcMuCoIlAmPFcqkQWQnByamxCO-4UEDIgP-BCniorQEV-I9i4cS9wLrWByNpz_WEedCLe7CEAICOmsogicwtPsIHXcQsdD0lR0MAb0VO1S5MQYsLRNNQwMAuNglvVj3y0PtHJshYSGcp7D-k1piAr-WtHwxejQku6fxkkAUoBrmHVFk8RXwlh3K6B2lrh1kXEZriRo2Puyb_8A2XmMCzL-UzgwcjBz2GzgOyBY2ooaH4nwF3G_4R929txYsWzmXlT00sddCdxilFTw6oyvvGQcnYke8MvjtSvkuqosG7wppBTZuPoUtt9BslZ-DNQRkSXSwun8AY7ZpyBGh-uJuvG7YCQuvczLjIcr1fXzGtrTAUGmxCl8PLYPy1BqSvT8gbMyiGUNgyhtm8IKdyFbpcu9l2sPSwOasChRdAJfXnl5Nt8ikSgvcjB3JVh_IYFUYEAMG9HJldg9vPixo4KgtKaexwHbQ6n0Qo5fK-TRDLUSzot2POnfeWl6dyIdztJBpJ0e_91JndcjhHkMURZqVvPK8ebFbNGRATTPOJPg7FBJsMWEofdLQtr86kRMsD2SvQboe4zo_Yy2Y3Vg6e-t37-6IJU1pda35rrqFjcrFPUQj8tiepMCVnDXzsBQfMpdYFt0nZ4yvPINvl0KL0geDcxBgt6X3e5DhdejYzzhsfaX3EhDUyUy-V8546Hx--XPIY5CuzHeg6nC0hLPJslX9yteAiW6tFWKxQHsBbIsrH30XX1qq8z4rtoktmdlonJkQezWFlQBRZv2rqwNBSjPNmxRuwAMi07iC0R4aEB_PyHzAMoIjuQCSK9hPlByb6RGFdjzIuxLmPs3HEu_WmFhguEFhFpxzKwue4xrhMEX1v0SGd71LyIwYq83io1rGVwRgFo6aKsX8Cpvm9e0mNjiQ9y5S3V2iJLY36KSAgC-JNGgUupiMTAzUadCR4V2xoGbtdWBwL5SGQAJCPHt5U7xwvbbskZ0fH_Dy2P8ZVNmF62YhEbbodW6rl2A1fvIu4cQAcFU0K82pQKaUmcFQiSgZhdo6zvxAWajNgFfgfjjofzzeWfjHLjpVg0JF6_NJqhDmLUe3IR249RTaaVZewHHc7rCyBN-JOkIsM9x1wRxFuwUZbaNs9cOVLJAZprreDMPPdru2rYcxkMqKDa0DBZgZxcoHJ54eqP95-0gB1q5B2CRoxfkJksjnva2nrb9RRl6k6DewUU6aUuYZzedI1gHUcZiCa2rHjg2nhpW9H6wA-UpyMlDnuEIp_2rHkg1Q8UY5gJWYMHvuNl3pnik-tMz3gGMjP1_snMYRtCBnio-u2LWzO2qwzE2rZpmkc1HuBdfwhjHU0R82igoSMW1H0lJTbBo2vG5e0QW6qx4eSi3nxzj83aZJWmrBhmyI5zG2drnniMhPwKLc2M7zQvpUhKA9hb48P2jG-RggUE_sMAkFyjTEvDO1FqAtaYqYv85i0iW1Q5Z07A3rNYYy0LG0jte0XX9PjQWyu_LTfBpXruBHWvCB8VNds0b05KFyjrmjEDavzII4Mj6oqbdNE0wG570Su2oWsA9qN7Y_urFXqtu-yWEG2QG781T22IWkLxjjp5uHgX1RXwOHcGSL2fVCgtWMU1fu5cave5bWiM4Akm_tR8HDoxUtV3fx1kt0Nd4oSEcvnNM1omkIu8dH1gWSfztsM2sJ3a2qYDeZN8bK-A3akfbYGwa9gcQk2i5ecgK6W5eGw4NBVTxYWPABKTPdYWfCBfqMOF41Q5hf0g9PnDuJJ0b0dG1ttt2qmkM5Q8B8WMk8_0Y5QuB7XwRxrRW581sW1nW9cwCww1IK3fUKKw1122p8sCkwxjnPm13ZEkxuMQ2GqdHTzBVXe-2noO-6of9_jHcQpJ3GkM9PDWniOntrM2p8LCWie5YZs3LaBAXALeJkt5fZSC0rSoxN0vePjWSY6gET-hmKXXL3xUh056O7CelsuK75S1xO0Km7JVPlOWf65qOAD0vlkEymMZ2gC5b0iKDeNQ2su25pSzsWjs2Xix9l1_jQuH8_Y8yP8WjIz954KoNrwVF3vGVpaoSyVtsGWURlQPG3X5T-Yv2s6Z7txz9SdJsGLBGHOy5N_MibheYCLVURRc9qz9JwCDw9efVuqJwAevwuiYvgub0-c5ha_ulW07yIjXEBneRWEpYnRnQ2JMEIu2YU0_j0TJV6XHLjKMJdApDTcvwes5NipLByHTLsHC9gXzcKwBQxgAgkk1_Mx--HFfOaUmvrwSu-yLICsb3FvtXxvwfQqOTuUGkRKpdW4S-f5YxDn35L-lXONEl6gqYmKZMiRNMIhPxDHCsVqlWzjrPwD1_SOMSMqUJeRBJL73tsdh3KRJMrS315kgiv6lNkasyIxndK8xVeWO7mOinrC4CWGutMfwDHon6Xb9XjHwtQDaPg6UlUmlDNNVhAIvQUKvArt9d7GIeejj2khR6LeFuB2D9AjZ5HIexCAPoRgFjaO7vPP_Qq7JkBMzHT1tUouNqGgbMRvgbMYffDAPPXHzqZaVuEgHZZbXaGZi8fK9TZYLzHAuHz0mkF61B574hLMwaz_vKdCE7nEOZBHIrd9XNCF5RxmhEL6IbzkfhNRqlvq0fQA5qgfI_8L2RUqXzBey2atlI1_bblFwOqxFR62cAkysl0wKdu7JjAkpEhbN6qzMhaAMTILCNhUZUbEDbf9JxFtd87w03qNfT3cdHmgaAj7gvBaF8JQHybRv3QzrMVXGbUwkeat2jSKcXKDPZOUJJT7L2slOaA5pGraNMO8gTW1nj06mzJHK30pNLhKggLBZMkx9VQgSLFeGnkrUrqnQ_RoZBEKSreQ3tThEXMBIjtddJDWC1dz4eBwIvXUEgfTENS12d5fAqksA4l7eM5is6frjXnVmQsr3uY8jKsKlCPmfVwxkYEgTLaaX_4TYLfsMd-YNNdSX1RPyQXj_Ps5lfb-mQ1lFkgWRsUn3jeNkTvlz6ZhKQLGMyMpir7Rs_MoLQkeeJtPV5LQznrMg-3swmceT1Ye0HRSYWCIgdwcprNBwcULvMHbFZQHCwlgpyPmLkSkhZRHwbVPGU4jse4cIiSoYgQspPesn_DPu1Ik4OPf8vwPijrlKMsbWSzRJw9mxRlJNh5gcot6jc_suKhloaZtflatQsKfjlnmo7nQqWXltsnxfT5WaPMyD6G-jjuyZ3b5cmsaCFt-VjNyY3po48vQ1kpyZyP2qCeVjce_hDzy61RhB88kHgqgbVeY5ortbhrvlN1hrVAWrwtaO3fF7UbE6rOwEgqSR_FJxvMZS0cQp4-TVAD3hjN3bGxN7hXmKX-YEjEmgIM2zOtYBe6OYOc-GUitMcTYfAiddAJrF6wpTC6cQT2d3oRJ3z3nSnVLo9c5RDuvayg61wTCHNyoGrxxhwRfLUUkR4PEUgTEDoyzC-TKRWPbnkR5V65sJbmii9MJgwWd7zm43foJb_ceOmAPauiE79nqZSnSymxNfFpVusJHT7vwb_FUzQokE2iNRcsYAhN4mChTDBlR3nkbVlw_yJpvyO3tIBCvCd1cKQtiHbRzg3dq47vGGhSPux9iLV_Ns8uTVeqP7IJ4BXpqaapYfzF0sk8ZclDGcK6jSMeC7ohhSMze9tP6iJfyZ6BbFb4nbLvZBzU-NnhDzK_Ia-jCjKAIkh5xforoB36pVtKYZMX5BIlS90DUDHESjmnvQkQmowFQK3U7tN3LCY166j3Zpn8eDRbig7sOW8sZqk5RqWWvVKlZQ5Kx8BrrjTqPrPxRTrCMwgVPeZBvoUX9IZ6zSMWyR4JRfZ-vQLcVr4Urs-1RDbY16zgISncgMYNjzIQSWfRxIcjwrvIEQBFIHmhnTnLlIGvL8r6pZiyL043B__kF_K-dwpmJtaNSSGXyEWS-XAVHwyPsCndDhdCV9etscOynj5nQOqEdhJMgO9jfWJgKg9DbrfFWjfjqFtMZj2uARgqE4Yz7Onb295gCLAQOqLC54w-RCuNhJEACv-cgyzZwyRhPQNOUitFouAmTbVqtNtW75_yC1NC4KpL4hIIXoPeU8nueSdRulSHF5gsa6DhF8xLYp14C8fglZW7MvHFRjpSTEWrpip6DZWm-AFXM70JF0EDlTga8_nIkHregEQ44NCfhHQAjkOQsxcSQKN3v64H2wRlElobgxVDlOzgsJs4rRHjFmjjskVRBnjkU_T3M-te1pu1wvsPSaUZOXuTICLSRrRI-s8Isr-JN4NPJ2bMy1QjBWnkqrzPLiyAEQiinQJRDJfunNLajQWgtreEeIcKmCQRejcoc7TSoYaJzIcLw_B5klp8rL2K3CTdu8FvJeqauTz2iriXA4sFcPtG4DlitxT1EcK-CdMxHygPcX-2mLgJ5B6JqG0iNYOyGOmNyzX7GelnZ364QSnBVmBWwN7L3-3PE4HNl4SmUUV2MVlNb5vzztSTSb_FXi3k-y3tZL_VvpD9VNa8uhuYOAYSB2UwS9ePJ6hH226pRquI6ZTC8ipRTuJ_uD-9RKbG12MPvgt1MVWpBjMw4HlCIyX9gP_pgM7b8V89Z0sIYjFvAPN3svZlzIlEICQo0LrjAYissfR2lpCSjvTTyxjOy7OntaEkfEHm7tCk4Aqvtx6oLYQrOUUpMkutUU51M6opaAB_i3XaKDPqs3NSeuTZnmloZ1CUIw0YNcpg9cBw1cNI-ZHw0Csw0sK46uoURqbaHtCKsEwfZM-SDnhQ7kuyJJ8M_iWYRpkAnZ6hkrDfruZ-CbL6sDAzArMMZl30acwPbKTqSFVRRL2ELvNXiA-hR8fTo7SlzFTx4QqhoE2SfpDYZqKguuy0f4GpVmomZTwiRajdy4fzJLzya92FqyEEdNtxz-UdfToNBJmJBJoJBt_oFhF5TD0FdrNcp3uxnPi_oiAn7_uGuBV8i-dy0
\ No newline at end of file
+xLrlSzscqd-lJw6wBtAREUdJfkqpKq_RkQvZfvcRE1xRwPtRJiU39SuvD1AS2yYkswVV_Hb0Vm0X2IGT9sdroZu8xQv-BB3iil1FnW7bHPv5SVme1hS4fV6EafiCvYo-GtmR0xp9O1edbDonU0qXZ-AOlKLu1oZ8umnXoEztC2QKRudnACa0O_4VKHp7yVU4ff0UaupG-EClllZVewy_qygljYXvYo5ZyTCeZl-KeXoBjo6BufBEKSBHBUBt5p11EmGnPtz4yUCOfV5XZ3276qZZm_YR3SIG0WxJxw9uGm7cBymA5oUd9nSdPySdb_71S_6S7NoAqYWcV0lf2UQ8t_UyyRgkXM3vukCOr9Bj2EtZTsHKhDzFA1G27_7uCEOeXuo3VAUm5p_Ztmc6yM6CoTq_FX7rYrtgN__FABBn4yoFTZjARY4T1d7wznKCqfwttZ187mQBE8KP7AeTnPXmyGw7l_4ePXpmWiM7CSH5teNcCSaoS4CeuEJlXMW77gSeWm-1mnfbiESjAKO83hxVt_NTpmd8JX7CqWl8TWIp4I084jv0YChr1su8oAvWlii0XrA8SqhoNU_uvtvZFv35jCIax5e__zA7uWLafA39Wm98IvuzRzTYxGSYHLmsHJNaA9Z4V_BdTGzKryRhVqGn1hd2NPP7ajEQYiAIf3-rR_xloPE-PkfkoU4M91Hj40RPGCiPRJLrvDW4y9gtZPECC7XPi1t4wJ71eYlKaut-x082Hd0rySGGfy-0d9S4aoX6J7uPpEvFKiIX-9GRGZ88n2jha35Pvu9pi0Cr4Q4GpSmcMwv-Lazqju0cMq3_yVIhhpxHTAPwhwFJlhxv5Kg1ZX47714StdDh4jxDMR_Hj5N9lOV4Au8H9nJXpIK7E0KqLMz_X1WhuBUF4f8LEOufN4CASGBZb40cZ88u1ppPnZb9ONQeqpb5E4Lu8yd47ttxhUFPIuJVieDF3zRgvulqu96SlyfV7iBgJ2R8UW5l4RnpYgeULP9I-QiKjFhX8QV6u5sP7uqkaeAEcOHdqKJ9kuT0ATnx0TGuWsqEcDut4wwMSBm5CWSi7w0jqaQkPP3efWZFn1yEpYcvHIbCEsoI8QeEmwJzoa1JNv4TIjvDEtF1kkTz9TexwG3-XfiF4ErFdsh2aL5Sz-jW0FIry6SPydm-skQhl_1O2Vq3fB2nypfDDbRi151NmYjNaPBzhUQfTRi1isxyA1ZUe1_WcDSzr9qSl_pNSVtu3ODf58CqHS8u1TaPuSArcIXT42udx7gX7Y_TOwF_a3Wm4WwtRWOOVwCm_FxUp_KKugIYSDDlj5_82W_KYn_7mZEAP4zin7aQQHfU-g3BFcRsFwkPwru7bCzrS848igGGBfdqql0zLjhIZ1m2148lIDRqynHcw1RI-rUGRqdQ4Yqjg5mUYMKaJ50EidzyiVhy4vUwDcwQzjep5qeZg_pA2BuHwe4t1TZOM8YNCxW1YL8PKnQdpcjSSP7VGFfwVGueHmdQ2Lzzqo1Hl0CRA122fHYYnAXfIxAZP4D1BjhCzPXMWs_xmNyBoBXK0cka5R4J0nfMtEI82Tsn6ay-u1oQI790Dv2VavreSA7W3lRIH70-uC2XV-B9BQAy0DcpKXbSMbgFzxhlMgXI-4YlK_0UzGsNUJqCaDx5W1HllxQGBfPKb44qSh1ZLwHdbMSHhEI_1HbAJtiH-x6iCdRKAzqzfiu-5lrW1pQ0mzVu5QGRQBYEE2tWdW8h6iXvNc2KI9L_IJQ-gy_4UgztSMrMVO_McotHFBwSLXDExnHP1KRt5IOIzLB0GJFkXt-xdqDci5DDSfqORj37yKvCzeopr_ZDBYF0wKhTRuSukOMOgt7n76M4dm5bFnn3p26LxUIX9D2YqjFqDiCQIcnmxg5WYlWWaKbJjZFtu1PgwGc1NQGwsyPleD9dHCrg4PEpzuSVlxcuE3cxkhvwyUhaykherRch_v4xR2BkDgS3e76mRHjyEvBO7rOZvZsSUmuvOiY-HBz3yslUmdj7yrSk2I53XuQO4VtW8Ko4XaBZ_Obb1FHEN1tsGaZ4waFRSP4X_FPxaBojzSj0EpYY7WBLkukQ4tnKBGWlTA-EV6ALJ8wmXyRKGo3Sfz_63P8-BvjWXSxONZwVgL7seRYwCv8k2YzHZZ904Rz_JacnymkdX-k_nMRWLQlsdbnnyBSaArAOlbYV4NwKvWWx6ldF7h26WLg7RZeOPuNFYuiQt7y0x0MMAqZG4VjlUR0iiEGaUBm5UECn7eJC9tFlh1W_zzZ-CWplBjqUSvAb7Kzgfv0YqMKjoWVIFT7snXYukoOEpnvTlUevDOKFjJGL7SAqidPIH6snP0OIl3UzWA9qEwhtrkBpZG355k663TbLph3A6LZgUbagtNrEN9kONWN3dj4bDaEVQDJeDWetLIjMePF_M3m-Y22E4vYXYLsVpGTUhcVrLZrNqmmtoJx28tKzH70TBQYMH5l0jdgdBoKzCnqdUl4hag8rcgqHPRofZ0SCj7JdojtFN1glxCf9TGuen3p4Z0X9ItYVBELIaXyHqCDD7ikxzjx5KqhoonnGphOGyYiEpZE0zILF_zP5ojrnzgbR6YNoPpBk9BV1pPfXuj-KcqQCu9aspF4evRk0kmmaC2_ZcPEziqwYC_bgz-c5jUNsR7xhIuUqeGiOgihJvq4gfTv2UFDyTaqSRtZJvXFrVcy2tKOtV9xRtuBCKC6g9qxpDGEC2v2T5d8Rxok2-RP5CacHQu0krTZrg1bQkuS4gQNGxGwLpovtC44WGuoVv01bGvMSX12MGM5fJWnAf5UszZoLGVeXaLhLd4BfyVaUCq9i8ZWcgVDznmIYwJ4fTk_45ppafZ4SCtyXRx3qWhb73ySo9cX83CDqJmisFyULF_iHiwUmlSwBcmmbnbR3BQ0m_JUIWvQP2lb21eUU0Gvy0pygd7JQ7ub_KLSA1js8yCI7yQCNpqxEhbwSlZXvzkZmy42kuYofhzKx1_ueoOq4U4sQc9YnQA8m8JGz9eNQYrHMOSaMvg2jsDh7UhWj_NSmAAG7KGxV1K3ki0YL7t6n1jJtPSM7yMUbuoY1Z7LNy6-aiGt4TwUzRMIXFsU_I-4QOQIxgsIm5c9may6E3GPhlhk4d8iS7_qTgmiB3KgB4IuueJQl07jRlxW3l0t5Au0oICrTAJSYfChTky2qcTlU5ZOsx5tFBL3C_7ATOUzlUnp_IcuSMmeVYsrsmiSssLYZa1KPhtSMiY8HsgoDY3fYc4CCS09zOECKRJQGrjjewrf6DfL9i-wTWCCDeVSQv2EsxKZFZUvwzPyNBXKCwo9p8b7dWCcp6owBF0SKGU-fsbZnkkPgRML-yEc1ziv1PEQeEGWCfha5JJ3Ipt0SLFkDRtZ7CuiKC4O9oL_J3S3eTtjNxrUhBxtjBPWJjR7cKYKEjCWO4nl2ZDCYwN-3oX6WsdywR85BX9u5hTPRcP7UTg6UTTchAPsvcxNAGZXCvRcgQAYjUE-WcGL34a1LeAgYLhr_4BQXHb8guo_LD6BvVDy77ISC3Yzl4q8fbFhu38Bq9S9mkeEiL60FxhNBKK-gdgZ9Fo0cVdlPWjDLELm5bNptFPhuW6Fnsf2fPlb-gB2VigMGGveZ38mOgT4VCQ0YS-EsqJH7k7P6vTCRTqQxkArh-_9izt-Nbr6oZuunSk7-eMZoB1eujzBPW-I3gLnzliI36IMiv1Xq_8McgENB7vWn_82w-PSR5lSw7Yu3uQIsUT1nOrc6BCiGnufrnb7ENZ7lVunJouloSxpzK9CNzH-ODqlLkq8vVESp_VVtmRfHrHkK_o46RgqrPkmubYuyLB_vwzWw-lVCRUgqi5eidU2MyKk1dZmWpjgg8hcWt5vGDTbVRSLaxzkqF5AQkfA29qJVeAaEcUOS9DlSX2amDA0QqB0H0pIkVKA7GmUDTV2y8rrhCRx_iIx4wJ2hkm1n4TmsFtLABtd_5WBbsQArK7wzQ5EWL4HoH-cldi1KD_Quum7UjzJ-yErHbc1JzobtVES-zrOrdm9_uvSSxaBISd3vJb_JbLK-h7IS5XZjL1p2NQau0o_AD9nM7Ri97k9eEs-TqgyUK3-lncpLI4Y-4yEBS-RJZiTz-Ashuqvj_EjiOHsHeFY-P7zTMQcVHlaxoFzgO-gNxUMNfwLdTuqvu7IEnD09bzA4ze9zfuzfQTdZ3aCTdNssHfJnut90zAdx0f-Bx0AvUtMuzXdXbsAHuIT7-MrU-kyfiwZgBRAex2sngEmhS6TANbSEdZQMD1rPr-IPLc8U1-ADEqhzNEpPa4Sz-halmWPnbf3U2VPIF7-FBmGwLmawvv0oWZ7CfXhhDZtZ56bedfOJ-eMjH95o0xjWs3fQIvssEcrtCLolrNW_wFw0pkmdDV2ub5dsRzU37OLhz9lZhEYrgxZgdkwyfx8jN1by-yXk7c-iDGuiw-XPDqBryR2zYMIE9_-1W8OcZ_NSO_UkXhQ-H4FQU-Hn0P5L9Z8w7XqUYjyW4MVdtW9wZ_161s3ZENRYllFvBjJfl2UK6ekOhgI5UHUtkVkOPRA3zJdBzdaoKjhlINfVRyUlZkro5Zhp4WkAUl3mzoZuf_la0E4neJcQrMz9Qq8TEwI-gTRs779WdcjObVWvy7AKGwTQkgqCSKwFzvKF8KSIlm-NEVWUuUpB7R4cnzQSPIUCEDwcUv5bpjdCDXQQxrTsqHvinnIoXAATirVFR5bUGcbMQeUE-cPMXkG6iXQNFdEpLNdFUjncMEQQ8Bnfuaf_TqoyZmKUSnRmdCVKZZvxjlbub5ycX49v_Yf8jhEESi8jzBbbnnAGFRMFrIrje6lRMcD2ijQb2i7jYpWyog1VAke-mZ4-fqayZdF8cBhheOxDgTDvgqpUAZ9Ov_4s7tOjgbR1U8-p6SOUE6STydiBA0BI73HprxtDWC2Wq3udnUwvxqrr2E7CTisxZ_m95cLozEjRI2bAuTnzf6XO3BnIPMdlwu3FMv0Li1qvq5dxQAnygQsA0Idie4c1lka-SZky9N-i3tDKwu5tDDfnyXQwR3dkMahuDbzzA6i07iC0R4aEB_PyHzAMoIjuQCSG9fPlByb6BGFdzzIu_LmPs3H1u_WmFhguEFhFpxzKyue4xrhMEX3d0eXFE2hubb3fGNQS751zfEi-8gTJQ4imF7CU06M4i3L9HBmQu5jvO0LY72MisaD-BMoAwrpSkN9Doc5CZEVYNAZh7D2tieAO8-L9nb6SrvjlhcMJAwT25F-t01dYzzU0iOAAlINNAM3R6m9eMeKBGQQuQGyu1GWBMceB5jEULG5r_JDaz5mMLBfQtKSprPSxzLuRn9Ivwev6FU29SJyTFIjNXHwWD4j8Gbi-YP-EJb768VMBmXSvTYwBvGwitjiiVZfwUMHVOkRXDHFc_BLMGvRbsJLWhQAR-rPH0-J0b2Fg_YRP58LI3Lcaxu1eCBICS0plhYcvkpPN7YHB7QNbCk-QeOsZ9LwQcpZAk2XTOEg5gMCpoG9L6ohBcxD0r8QexzwF1IytNWxF_mAL6we5ebw8cXs2vPRd1IzFlEpxxHQqUj1Qva5_h9R1FGmlsxExWDK3re9JBqvBt_E2QO77WgVdQkq581iWw-g91I05KEzDsGj8Bf0MW5e0BQiI1opFdhtqW2GD-B3K-Z1nOJt0wJK7cvQlNbIM8DQVrldDAfNe6gLGfWArjvkgvywufJfn_jePt1h09-XMyaMbN90j05a0BHEO0vIUAyKNW2g0bcZ044BBChK7dFwhj1USEd1QC79XvBuyUm6e0gZ_bki59nkdlaCX5hHij9PvpWEa1Hm7E0ieDYYT5nul-DJuTD-Fl83a0ca1o0NGWaeBbUxRSnU4QeGMvUc4Pa75GgNpAju5dWQU1PfEQ1POB5X2hiFzso4JSktjtmwUmRjm5vnCd3fkSLrWSiBak29qGQe7AVTzbWjamv0jAZQ8rw9LFYWvBgPOaEf2QXchWh1Q1gb1e1Q4EX5ottUue6IYr7MPueAJ2wT5cJn0MYQwGAYMVZU4qm9G9q0TzzmjCBbXMY2o85hoFm8XMk2nuUc-zMu1I0Te0SO2PkZEkWKb0wLb5-WGGWioDZBkkxSMS0Guphk-5cWaD9qNVItuQFWiUcFXiXm_sutCPXbeNB0iYmOtC8xxh1PaAcGMK2nGx1ko5bGaAaDtRYqmkM4Qk9PhWSqDsmAH3L7F_LuBGWgXzlLW2p82cSNwSQ7Yk0vi0QO2flisiGKZ2wC56mSstNUOBHXL62sWMA2qBz1QS16ukE_HMh1HsDWtW_sjSOaUnKUCaGIfUqcYA9BhzFZWyuFuoPEVFxx9GF9sjSi0mYk-HSbR31dxzkalJvx8AbeAi92h_hMIrqH7AllCbvXzUafy66z4qKhzQPv47o762lZj8w297cC8qvYJ-0QAopOHXcbyG_WsJX4LCKg2sQF8dMjI2vX-zUKNdttTBU7GjIxYtZn5TmuHjPslSqf89Tav13HsmCqfObB-7qKzbw9PQYLkNuIfnV2FE3x01t7hv7Z1movRnQ2JMUIuyoY0_z1bNF6XnLjKMJdApDTcbwgE2NWBLByHTTs5CLeWfsO0TvLLruFwtNtAfyu37oETUdEFibL3SwXayhqzojMjQSEoFOJEAHV82E7KYvPaumIg0hejANJcLQLPE1htSxhOLizcusQEw7qVsgezcmpkFhAAQVDqDbvgZ1xwJbbhDfvQlnXYt5ITZNjtJxUPT_Nh4DxrZi_uC6Oxc22G8SRhKj6ePO_HoamnejOrAYCj3VNkONbzi_ra9SjFASbRxqoIe7NwkJ2ZhRALOVq83jDOjeuUawQxLJWpK_SMrlYmlBx1Uk1aRjrp4jJCZejaf55fbgzQ9MeugKo66MsFHFutQ6g4SsvOD0ghM5MABNv5hRiQ0bXuN8qX5iEYrahzwQyyIHX7S54C9defAwLmBfR39vwrN37hqyqqkYthJsTWcNYXdBeqNx5mQtieZuxD0qqho9-bZs4wSMV8NJ1pbLUU3SVPxe63jEi8_NAijXwjN8KiwZeQVDw5wKuIhRIdMNlEm7s0dujJA7cdX0hawj6gP3aFetREG8lTfhVwB5BeHbStyyRWciApui5Cuj5qTqH5UHKJAfoh8EjoGqZ33Z26DXYc3aB1mdHPKQkMBJMETalj5N5JwKE9sgskX3LRUSPHoZaiZOUxDPnAPshLvfqZO30R_RQ9-akONZgAhXox08Muz9KbMvIbuz0m5corELlshs1gxPr0OcuJ9QynNAd_hkw8eYwBP3U-8r7B8U_FjEl0Fc55TZngk_bduM-Uis7mDnzryBUFAmzQ5xdUR-newr5bK5l5iujH1rRqibMhgE4zsNrLXd4SNgjW-lK0L4Q6L0SBfkA0XAgVwND5MVsiZhDPcOzDvCog-hCnajM7L5UBQFMhRA1mYdP6aRAnZAAfj5b3SxoS1Qv2JhLwPiftlqMrbmO-Rps9mgtlJd_5gkss64lVRCELtfIH7ZRr9siTHsq5N97vjEGHJj-alT9uiCWwfJPaFBRUF8mvHPiDf33z_dxL_8Wy3fmSDGtO1tqQ4q7heBXL-s7zviE8M1yNwHBKgbAPBtBI6WXnZn2erkHrGz01KAdRy_FlRRFmrfeiJtsYIOSEgOSh7QuzIE7aFiHr9XMjDOBrzPWkXPX9YBuEujSpPXAagoMShFN3yMPfWar3qASD9lCFiATp5zU13IupBIqQqglE1SUCalIEL6vPUWQggT8CDf4jY_d2x9AuH60hfrHHpvbs4oHmZXNceux-EgSK7JWu-RhtLasShNg7wxhYh5Au9ykgr8BTEkzfzWWIDahzGadZPVAJ0U-HPnDzFhEuLYu8gtxKnC_3O-ww-n6NPQlwlyODwN2Hp-Xh9tRXeCDzaZoTlhgM7D6S-vCCRer_V7X_Ke-dNDlyCKhKuOZyoXsSdyglSRrgto_zh_edwGVoJ7LeaPhOlTEjDPQPsRuwaqQrefP3Pv8lg1e9pbi-8JLosELHxVEPmmMrQlcY8WteyM5fvT0h2RNZqcmSbuumXSR86QOOnwioy7NgofeVHSyT1YWBzOrHKNbWokX1IWajQDYZ41j1smnDDfJrGNqKtGRVac05RYWbvpHKKINj-IQSWtPUbaPwrvIEe5dP8uNukugt98UAIDJCutFj9QVb__qxoAVpTMulxcBks4yyNG8VnjFuTMExcOncrnO2aqTx-y0ONYwhiU7JjXfLtiQqd0zAr2ObwqamxUQ8e9eehOk2gTM9vMLlBBDH1CL8dIupUgXWudLpf71jRvjnQrQrjLVVgJKxpyuTMquPdBMZiltconJWuf-Xl81WheQgbImqNTFpoWDLpaxPb_Z9efLKG_jvf6QCEV8A1FbgHrbTFf6-lUZGUPYBZMbyv3xsWc_5dk8CCFvcbugKKH75egMH4yh0PRnMBDgMQoXrFzU8Wil78HBAtxMLId5hRwKQGzlUqDFGDfw4qtP5YKlYxI1nAFM-Li0peDwv65SiEjRXeLpCsKQHrYURK7hDcJnYHpk5Qki-gqk32TfpwwgPOKVHSWoQ3Td98wRh98oQmwtr8AgEFvYOdd9RaccdI8nYKRzIkHv_hEbNheQgP22z5n_4Vsey2USEdjKSMLbYpFoCZXnPVz8EwvJvf6lf7YrlRdOs1sjjsf-5ZRvdtyLYT6hui6nbSdYFSExGGUAHnpjleyuBkMd2iP2yksdDEeMWjRlRzkLC86m4hsSbtU6F_pVYMr9KUyNDybRXhF8Odc3TgOlcPUIbrCxKmh7waFZaBGReHEbDbSRLxEGxupJpiZ6yQbTRIehDjcK0hyp7BMFIVEwsv1o2Tx3hgJcSejahX2k5SknebOcbJRaZqfj6RweUgnq0ifTVTcTpYPfE7u4xosjiVs7-2LnZMNHaCaqTH4nV-qmsr4RFZHZNmUpzt-C7czBv7zp4jZYgOLln-x7qS7TuVYhybOVL3BwE5LGBJzQX2Fzkh9oSfRjIcYuR5TKWsJGhY-hktR7VfeXKdjYnLDItHIxZEfRhPRo9rvJaSKvIcB57yvrnnM8Y1Bw7MKRkLZSai-KbFYVlV9EGZD33Zvz--VNdfwVVbI8_4I8_aI9_yp-ondVp7Ntr0GhUh5aULlJedn2dHPv5_my0
\ No newline at end of file
diff --git a/docs/logical_data_model.puml b/docs/logical_data_model.puml
index a67de22812..e9ae59b9f5 100644
--- a/docs/logical_data_model.puml
+++ b/docs/logical_data_model.puml
@@ -89,7 +89,7 @@ class ActivityReportGoals{
class ActivityReportObjectiveCitations{
* id : integer :
* activityReportObjectiveId : integer : REFERENCES "ActivityReportObjectives".id
-!issue='column reference does not match model: "MonitoringStandards".id !== "MonitoringStandardLinks"."standardId"' * citationId : integer : REFERENCES "MonitoringStandards".id
+ * citationId : integer : REFERENCES "MonitoringStandards".id
!issue='column reference does not match model: "MonitoringFindings".id !== "MonitoringFindingLinks"."findingId"' * findingId : integer : REFERENCES "MonitoringFindings".id
!issue='column reference does not match model: "MonitoringReviews".id !== "MonitoringReviewLinks"."reviewId"' * reviewId : integer : REFERENCES "MonitoringReviews".id
* createdAt : timestamp with time zone : now()
@@ -2503,6 +2503,12 @@ class ZALZAFilter{
Files "1" --[#black,plain,thickness=2]-- "1" ImportFiles : importFile, file
Grants "1" --[#black,plain,thickness=2]-- "1" GrantNumberLinks : grant, grantNumberLink
+!issue='associations need to be defined both directions'
+MonitoringFindingLinks "1" --[#d54309,plain,thickness=2]-- "1" ActivityReportObjectiveCitations : finding
+!issue='associations need to be defined both directions'
+MonitoringReviewLinks "1" --[#d54309,plain,thickness=2]-- "1" ActivityReportObjectiveCitations : review
+!issue='associations need to be defined both directions'
+MonitoringStandardLinks "1" --[#d54309,plain,thickness=2]-- "1" ActivityReportObjectiveCitations : citation
ActivityReportCollaborators "1" --[#black,dashed,thickness=2]--{ "n" CollaboratorRoles : collaboratorRoles, activityReportCollaborator
ActivityReportGoals "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportGoalFieldResponses : activityReportGoal, activityReportGoalFieldResponses
@@ -2569,7 +2575,6 @@ Imports "1" --[#black,dashed,thickness=2]--{ "n" ImportFiles : import, importFi
MaintenanceLogs "1" --[#black,dashed,thickness=2]--{ "n" MaintenanceLogs : triggeredBy, triggered
MonitoringFindingHistoryStatusLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringFindingHistories : monitoringFindingStatusLink, monitoringFindingHistories
MonitoringFindingHistoryStatusLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringFindingHistoryStatuses : monitoringFindingHistoryStatuses, statusLink
-MonitoringFindingLinks "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportObjectiveCitations : finding, activityReportObjectiveCitationFinding
MonitoringFindingLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringFindingGrants : findingLink, monitoringFindingGrants
MonitoringFindingLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringFindingHistories : monitoringFindingLink, monitoringFindingHistories
MonitoringFindingLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringFindingStandards : monitoringFindingStandards, findingLink
@@ -2579,14 +2584,12 @@ MonitoringFindingStatusLinks "1" --[#black,dashed,thickness=2]--{ "n" Monitorin
MonitoringFindingStatusLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringFindings : monitoringFindings, statusLink
MonitoringGranteeLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringFindingGrants : granteeLink, monitoringFindingGrants
MonitoringGranteeLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringReviewGrantees : monitoringReviewGrantees, monitoringGranteeLink
-MonitoringReviewLinks "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportObjectiveCitations : review, activityReportObjectiveCitationReview
MonitoringReviewLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringClassSummaries : monitoringReviewLink, monitoringClassSummaries
MonitoringReviewLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringFindingHistories : monitoringReviewLink, monitoringFindingHistories
MonitoringReviewLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringReviewGrantees : monitoringReviewLink, monitoringReviewGrantees
MonitoringReviewLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringReviews : monitoringReviews, monitoringReviewLink
MonitoringReviewStatusLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringReviewStatuses : monitoringReviewStatuses, statusLink
MonitoringReviewStatusLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringReviews : monitoringReviews, statusLink
-MonitoringStandardLinks "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportObjectiveCitations : citation, activityReportObjectiveCitations
MonitoringStandardLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringFindingStandards : statusLink, monitoringFindingStandards
MonitoringStandardLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringStandards : monitoringStandardes, statusLink
NationalCenters "1" --[#black,dashed,thickness=2]--{ "n" EventReportPilotNationalCenterUsers : nationalCenter, eventReportPilotNationalCenterUsers
@@ -2650,8 +2653,6 @@ ActivityReportGoals "n" }--[#black,dotted,thickness=2]--{ "n" Resources : resour
ActivityReportObjectives "n" }--[#black,dotted,thickness=2]--{ "n" Courses : courses, reportObjectives
ActivityReportObjectives "n" }--[#black,dotted,thickness=2]--{ "n" Files : files, reportObjectives
!issue='associations need to be defined both directions'
-ActivityReportObjectives "n" }--[#d54309,dotted,thickness=2]--{ "n" MonitoringStandardLinks : activityReportObjectives
-!issue='associations need to be defined both directions'
ActivityReportObjectives "n" }--[#d54309,dotted,thickness=2]--{ "n" MonitoringStandards : citations
ActivityReportObjectives "n" }--[#black,dotted,thickness=2]--{ "n" Resources : resources, activityReportObjectives
ActivityReportObjectives "n" }--[#black,dotted,thickness=2]--{ "n" Topics : topics, activityReportObjectives
diff --git a/src/models/monitoringFindingLink.js b/src/models/monitoringFindingLink.js
index 23185008a7..4297669378 100644
--- a/src/models/monitoringFindingLink.js
+++ b/src/models/monitoringFindingLink.js
@@ -24,7 +24,8 @@ export default (sequelize, DataTypes) => {
* monitoringClassSummaries: MonitoringClassSummary.findingId >- findingId
* monitoringFindingLink: findingId -< MonitoringClassSummary.findingId
*/
- MonitoringFindingLink.hasMany(models.ActivityReportObjectiveCitation, { foreignKey: 'findingId', as: 'activityReportObjectiveCitationFinding' });
+ // eslint-disable-next-line max-len
+ // MonitoringFindingLink.hasMany(models.ActivityReportObjectiveCitation, { foreignKey: 'findingId', as: 'activityReportObjectiveCitationFinding' });
}
}
MonitoringFindingLink.init({
diff --git a/src/models/monitoringReviewLinks.js b/src/models/monitoringReviewLinks.js
index 83648c87c8..38bd6cdefd 100644
--- a/src/models/monitoringReviewLinks.js
+++ b/src/models/monitoringReviewLinks.js
@@ -24,7 +24,8 @@ export default (sequelize, DataTypes) => {
* monitoringClassSummaries: MonitoringClassSummary.reviewId >- reviewId
* monitoringReviewLink: reviewId -< MonitoringClassSummary.reviewId
*/
- MonitoringReviewLink.hasMany(models.ActivityReportObjectiveCitation, { foreignKey: 'reviewId', as: 'activityReportObjectiveCitationReview' });
+ // eslint-disable-next-line max-len
+ // MonitoringReviewLink.hasMany(models.ActivityReportObjectiveCitation, { foreignKey: 'reviewId', as: 'activityReportObjectiveCitationReview' });
}
}
MonitoringReviewLink.init({
diff --git a/src/models/monitoringStandardLink.js b/src/models/monitoringStandardLink.js
index 39bd6b10c3..23ce44e150 100644
--- a/src/models/monitoringStandardLink.js
+++ b/src/models/monitoringStandardLink.js
@@ -10,13 +10,16 @@ import { Model } from 'sequelize';
export default (sequelize, DataTypes) => {
class MonitoringStandardLink extends Model {
static associate(models) {
- MonitoringStandardLink.hasMany(models.ActivityReportObjectiveCitation, { foreignKey: 'citationId', as: 'activityReportObjectiveCitations' });
+ // eslint-disable-next-line max-len
+ // MonitoringStandardLink.hasMany(models.ActivityReportObjectiveCitation, { foreignKey: 'citationId', as: 'activityReportObjectiveCitations' });
+ /*
MonitoringStandardLink.belongsToMany(models.ActivityReportObjective, {
through: models.ActivityReportObjectiveCitation,
foreignKey: 'citationId',
otherKey: 'activityReportObjectiveId',
as: 'activityReportObjectives',
});
+ */
}
}
MonitoringStandardLink.init({
From 32db7070c9a4768bca81ae4767567b0e38aa7c41 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Fri, 22 Nov 2024 09:44:14 -0500
Subject: [PATCH 024/198] overwrite from main
---
docker-compose.yml | 20 +++++++++++++++++++-
1 file changed, 19 insertions(+), 1 deletion(-)
diff --git a/docker-compose.yml b/docker-compose.yml
index f383a2094d..d411d83ac3 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,6 +1,8 @@
services:
api-docs:
image: redocly/redoc
+ profiles:
+ - full_stack
ports:
- "5003:80"
volumes:
@@ -20,6 +22,8 @@ services:
shm_size: 1g
minio:
image: minio/minio:RELEASE.2024-01-01T16-36-33Z
+ profiles:
+ - full_stack
env_file: .env
ports:
- "9000:9000"
@@ -29,12 +33,16 @@ services:
command: server /data --console-address ":9001"
aws-cli:
image: amazon/aws-cli
+ profiles:
+ - full_stack
env_file: .env
command: ["--endpoint-url", "http://minio:9000", "s3api", "create-bucket", "--bucket", "$S3_BUCKET"]
depends_on:
- minio
clamav-rest:
image: ajilaag/clamav-rest
+ profiles:
+ - full_stack
ports:
- "9443:9443"
environment:
@@ -61,12 +69,16 @@ services:
- "6379:6379"
mailcatcher:
image: schickling/mailcatcher
+ profiles:
+ - full_stack
ports:
- "1025:1025"
- "1080:1080"
testingonly:
build:
context: .
+ profiles:
+ - full_stack
ports:
- "9999:9999"
depends_on:
@@ -79,6 +91,8 @@ services:
- NODE_ENV=development
sftp:
image: jmcombs/sftp:alpine
+ profiles:
+ - full_stack
volumes:
- ./test-sftp:/home/tta_ro/ProdTTAHome
- ./test-sftp/sshd_config:/etc/ssh/sshd_config
@@ -86,4 +100,8 @@ services:
- "${SFTP_EXPOSED_PORT:-22}:22"
command: ${ITAMS_MD_USERNAME:-tta_ro}:${ITAMS_MD_PASSWORD:-password}:1001
volumes:
- dbdata: {}
\ No newline at end of file
+ dbdata: {}
+ minio-data: {}
+ yarn-cache: {}
+ node_modules-backend: {}
+ node_modules-frontend: {}
\ No newline at end of file
From e8a4918fcb86e61b9e733b05737fb4882d6c1581 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Fri, 22 Nov 2024 10:57:09 -0500
Subject: [PATCH 025/198] per Jon switch json column to array of objects to
match what we expect for map
---
src/migrations/20241115203616-add-post-processing-to-import.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/migrations/20241115203616-add-post-processing-to-import.js b/src/migrations/20241115203616-add-post-processing-to-import.js
index ec84c54796..14a65e9190 100644
--- a/src/migrations/20241115203616-add-post-processing-to-import.js
+++ b/src/migrations/20241115203616-add-post-processing-to-import.js
@@ -26,7 +26,7 @@ module.exports = {
// Update Imports set the postProcessingActions column to the object.
await queryInterface.sequelize.query(/* sql */`
UPDATE "Imports"
- SET "postProcessingActions" = '{"name": "Monitoring Goal CRON job", "function": "createMonitoringGoals"}'
+ SET "postProcessingActions" = '[{"name": "Monitoring Goal CRON job", "function": "createMonitoringGoals"}]'
WHERE "name" = 'ITAMS Monitoring Data';
`, { transaction });
});
From d5187ceb856ac1b3a58b222c707714d2285144bc Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Fri, 22 Nov 2024 11:57:17 -0500
Subject: [PATCH 026/198] fix process exit when running from cmd
---
src/tools/createMonitoringGoalsCLI.js | 12 ++++++++----
1 file changed, 8 insertions(+), 4 deletions(-)
diff --git a/src/tools/createMonitoringGoalsCLI.js b/src/tools/createMonitoringGoalsCLI.js
index 94228c6255..63590e8011 100644
--- a/src/tools/createMonitoringGoalsCLI.js
+++ b/src/tools/createMonitoringGoalsCLI.js
@@ -1,7 +1,11 @@
import createMonitoringGoals from './createMonitoringGoals';
import { auditLogger } from '../logger';
-createMonitoringGoals().catch((e) => {
- auditLogger.error(e);
- process.exit(1);
-});
+createMonitoringGoals()
+ .catch((e) => {
+ auditLogger.error(e);
+ process.exit(1);
+ })
+ .finally(() => {
+ process.exit(0);
+ });
From 2a0b7a2505c7602d4c34bd1b329bcb9afd4c4782 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Fri, 22 Nov 2024 17:21:16 -0500
Subject: [PATCH 027/198] apply Garretts idea for a approach that doesnt
require joins to monitoring
---
docs/logical_data_model.encoded | 2 +-
docs/logical_data_model.puml | 23 +-----
...add-activity-report-objective-citations.js | 38 ++-------
src/models/activityReportObjectiveCitation.js | 54 +++----------
src/models/monitoringFinding.js | 1 +
src/models/monitoringFindingLink.js | 2 -
src/models/monitoringReviewLinks.js | 2 -
src/models/monitoringStandard.js | 3 +-
src/routes/citations/handlers.js | 8 +-
src/routes/citations/handlers.test.js | 7 +-
src/services/citations.js | 43 +----------
src/services/citations.test.js | 77 +++++++++----------
12 files changed, 74 insertions(+), 186 deletions(-)
diff --git a/docs/logical_data_model.encoded b/docs/logical_data_model.encoded
index 5f6fd30d87..b46490ff53 100644
--- a/docs/logical_data_model.encoded
+++ b/docs/logical_data_model.encoded
@@ -1 +1 @@
-xLrlSzscqd-lJw6wBtAREUdJfkqpKq_RkQvZfvcRE1xRwPtRJiU39SuvD1AS2yYkswVV_Hb0Vm0X2IGT9sdroZu8xQv-BB3iil1FnW7bHPv5SVme1hS4fV6EafiCvYo-GtmR0xp9O1edbDonU0qXZ-AOlKLu1oZ8umnXoEztC2QKRudnACa0O_4VKHp7yVU4ff0UaupG-EClllZVewy_qygljYXvYo5ZyTCeZl-KeXoBjo6BufBEKSBHBUBt5p11EmGnPtz4yUCOfV5XZ3276qZZm_YR3SIG0WxJxw9uGm7cBymA5oUd9nSdPySdb_71S_6S7NoAqYWcV0lf2UQ8t_UyyRgkXM3vukCOr9Bj2EtZTsHKhDzFA1G27_7uCEOeXuo3VAUm5p_Ztmc6yM6CoTq_FX7rYrtgN__FABBn4yoFTZjARY4T1d7wznKCqfwttZ187mQBE8KP7AeTnPXmyGw7l_4ePXpmWiM7CSH5teNcCSaoS4CeuEJlXMW77gSeWm-1mnfbiESjAKO83hxVt_NTpmd8JX7CqWl8TWIp4I084jv0YChr1su8oAvWlii0XrA8SqhoNU_uvtvZFv35jCIax5e__zA7uWLafA39Wm98IvuzRzTYxGSYHLmsHJNaA9Z4V_BdTGzKryRhVqGn1hd2NPP7ajEQYiAIf3-rR_xloPE-PkfkoU4M91Hj40RPGCiPRJLrvDW4y9gtZPECC7XPi1t4wJ71eYlKaut-x082Hd0rySGGfy-0d9S4aoX6J7uPpEvFKiIX-9GRGZ88n2jha35Pvu9pi0Cr4Q4GpSmcMwv-Lazqju0cMq3_yVIhhpxHTAPwhwFJlhxv5Kg1ZX47714StdDh4jxDMR_Hj5N9lOV4Au8H9nJXpIK7E0KqLMz_X1WhuBUF4f8LEOufN4CASGBZb40cZ88u1ppPnZb9ONQeqpb5E4Lu8yd47ttxhUFPIuJVieDF3zRgvulqu96SlyfV7iBgJ2R8UW5l4RnpYgeULP9I-QiKjFhX8QV6u5sP7uqkaeAEcOHdqKJ9kuT0ATnx0TGuWsqEcDut4wwMSBm5CWSi7w0jqaQkPP3efWZFn1yEpYcvHIbCEsoI8QeEmwJzoa1JNv4TIjvDEtF1kkTz9TexwG3-XfiF4ErFdsh2aL5Sz-jW0FIry6SPydm-skQhl_1O2Vq3fB2nypfDDbRi151NmYjNaPBzhUQfTRi1isxyA1ZUe1_WcDSzr9qSl_pNSVtu3ODf58CqHS8u1TaPuSArcIXT42udx7gX7Y_TOwF_a3Wm4WwtRWOOVwCm_FxUp_KKugIYSDDlj5_82W_KYn_7mZEAP4zin7aQQHfU-g3BFcRsFwkPwru7bCzrS848igGGBfdqql0zLjhIZ1m2148lIDRqynHcw1RI-rUGRqdQ4Yqjg5mUYMKaJ50EidzyiVhy4vUwDcwQzjep5qeZg_pA2BuHwe4t1TZOM8YNCxW1YL8PKnQdpcjSSP7VGFfwVGueHmdQ2Lzzqo1Hl0CRA122fHYYnAXfIxAZP4D1BjhCzPXMWs_xmNyBoBXK0cka5R4J0nfMtEI82Tsn6ay-u1oQI790Dv2VavreSA7W3lRIH70-uC2XV-B9BQAy0DcpKXbSMbgFzxhlMgXI-4YlK_0UzGsNUJqCaDx5W1HllxQGBfPKb44qSh1ZLwHdbMSHhEI_1HbAJtiH-x6iCdRKAzqzfiu-5lrW1pQ0mzVu5QGRQBYEE2tWdW8h6iXvNc2KI9L_IJQ-gy_4UgztSMrMVO_McotHFBwSLXDExnHP1KRt5IOIzLB0GJFkXt-xdqDci5DDSfqORj37yKvCzeopr_ZDBYF0wKhTRuSukOMOgt7n76M4dm5bFnn3p26LxUIX9D2YqjFqDiCQIcnmxg5WYlWWaKbJjZFtu1PgwGc1NQGwsyPleD9dHCrg4PEpzuSVlxcuE3cxkhvwyUhaykherRch_v4xR2BkDgS3e76mRHjyEvBO7rOZvZsSUmuvOiY-HBz3yslUmdj7yrSk2I53XuQO4VtW8Ko4XaBZ_Obb1FHEN1tsGaZ4waFRSP4X_FPxaBojzSj0EpYY7WBLkukQ4tnKBGWlTA-EV6ALJ8wmXyRKGo3Sfz_63P8-BvjWXSxONZwVgL7seRYwCv8k2YzHZZ904Rz_JacnymkdX-k_nMRWLQlsdbnnyBSaArAOlbYV4NwKvWWx6ldF7h26WLg7RZeOPuNFYuiQt7y0x0MMAqZG4VjlUR0iiEGaUBm5UECn7eJC9tFlh1W_zzZ-CWplBjqUSvAb7Kzgfv0YqMKjoWVIFT7snXYukoOEpnvTlUevDOKFjJGL7SAqidPIH6snP0OIl3UzWA9qEwhtrkBpZG355k663TbLph3A6LZgUbagtNrEN9kONWN3dj4bDaEVQDJeDWetLIjMePF_M3m-Y22E4vYXYLsVpGTUhcVrLZrNqmmtoJx28tKzH70TBQYMH5l0jdgdBoKzCnqdUl4hag8rcgqHPRofZ0SCj7JdojtFN1glxCf9TGuen3p4Z0X9ItYVBELIaXyHqCDD7ikxzjx5KqhoonnGphOGyYiEpZE0zILF_zP5ojrnzgbR6YNoPpBk9BV1pPfXuj-KcqQCu9aspF4evRk0kmmaC2_ZcPEziqwYC_bgz-c5jUNsR7xhIuUqeGiOgihJvq4gfTv2UFDyTaqSRtZJvXFrVcy2tKOtV9xRtuBCKC6g9qxpDGEC2v2T5d8Rxok2-RP5CacHQu0krTZrg1bQkuS4gQNGxGwLpovtC44WGuoVv01bGvMSX12MGM5fJWnAf5UszZoLGVeXaLhLd4BfyVaUCq9i8ZWcgVDznmIYwJ4fTk_45ppafZ4SCtyXRx3qWhb73ySo9cX83CDqJmisFyULF_iHiwUmlSwBcmmbnbR3BQ0m_JUIWvQP2lb21eUU0Gvy0pygd7JQ7ub_KLSA1js8yCI7yQCNpqxEhbwSlZXvzkZmy42kuYofhzKx1_ueoOq4U4sQc9YnQA8m8JGz9eNQYrHMOSaMvg2jsDh7UhWj_NSmAAG7KGxV1K3ki0YL7t6n1jJtPSM7yMUbuoY1Z7LNy6-aiGt4TwUzRMIXFsU_I-4QOQIxgsIm5c9may6E3GPhlhk4d8iS7_qTgmiB3KgB4IuueJQl07jRlxW3l0t5Au0oICrTAJSYfChTky2qcTlU5ZOsx5tFBL3C_7ATOUzlUnp_IcuSMmeVYsrsmiSssLYZa1KPhtSMiY8HsgoDY3fYc4CCS09zOECKRJQGrjjewrf6DfL9i-wTWCCDeVSQv2EsxKZFZUvwzPyNBXKCwo9p8b7dWCcp6owBF0SKGU-fsbZnkkPgRML-yEc1ziv1PEQeEGWCfha5JJ3Ipt0SLFkDRtZ7CuiKC4O9oL_J3S3eTtjNxrUhBxtjBPWJjR7cKYKEjCWO4nl2ZDCYwN-3oX6WsdywR85BX9u5hTPRcP7UTg6UTTchAPsvcxNAGZXCvRcgQAYjUE-WcGL34a1LeAgYLhr_4BQXHb8guo_LD6BvVDy77ISC3Yzl4q8fbFhu38Bq9S9mkeEiL60FxhNBKK-gdgZ9Fo0cVdlPWjDLELm5bNptFPhuW6Fnsf2fPlb-gB2VigMGGveZ38mOgT4VCQ0YS-EsqJH7k7P6vTCRTqQxkArh-_9izt-Nbr6oZuunSk7-eMZoB1eujzBPW-I3gLnzliI36IMiv1Xq_8McgENB7vWn_82w-PSR5lSw7Yu3uQIsUT1nOrc6BCiGnufrnb7ENZ7lVunJouloSxpzK9CNzH-ODqlLkq8vVESp_VVtmRfHrHkK_o46RgqrPkmubYuyLB_vwzWw-lVCRUgqi5eidU2MyKk1dZmWpjgg8hcWt5vGDTbVRSLaxzkqF5AQkfA29qJVeAaEcUOS9DlSX2amDA0QqB0H0pIkVKA7GmUDTV2y8rrhCRx_iIx4wJ2hkm1n4TmsFtLABtd_5WBbsQArK7wzQ5EWL4HoH-cldi1KD_Quum7UjzJ-yErHbc1JzobtVES-zrOrdm9_uvSSxaBISd3vJb_JbLK-h7IS5XZjL1p2NQau0o_AD9nM7Ri97k9eEs-TqgyUK3-lncpLI4Y-4yEBS-RJZiTz-Ashuqvj_EjiOHsHeFY-P7zTMQcVHlaxoFzgO-gNxUMNfwLdTuqvu7IEnD09bzA4ze9zfuzfQTdZ3aCTdNssHfJnut90zAdx0f-Bx0AvUtMuzXdXbsAHuIT7-MrU-kyfiwZgBRAex2sngEmhS6TANbSEdZQMD1rPr-IPLc8U1-ADEqhzNEpPa4Sz-halmWPnbf3U2VPIF7-FBmGwLmawvv0oWZ7CfXhhDZtZ56bedfOJ-eMjH95o0xjWs3fQIvssEcrtCLolrNW_wFw0pkmdDV2ub5dsRzU37OLhz9lZhEYrgxZgdkwyfx8jN1by-yXk7c-iDGuiw-XPDqBryR2zYMIE9_-1W8OcZ_NSO_UkXhQ-H4FQU-Hn0P5L9Z8w7XqUYjyW4MVdtW9wZ_161s3ZENRYllFvBjJfl2UK6ekOhgI5UHUtkVkOPRA3zJdBzdaoKjhlINfVRyUlZkro5Zhp4WkAUl3mzoZuf_la0E4neJcQrMz9Qq8TEwI-gTRs779WdcjObVWvy7AKGwTQkgqCSKwFzvKF8KSIlm-NEVWUuUpB7R4cnzQSPIUCEDwcUv5bpjdCDXQQxrTsqHvinnIoXAATirVFR5bUGcbMQeUE-cPMXkG6iXQNFdEpLNdFUjncMEQQ8Bnfuaf_TqoyZmKUSnRmdCVKZZvxjlbub5ycX49v_Yf8jhEESi8jzBbbnnAGFRMFrIrje6lRMcD2ijQb2i7jYpWyog1VAke-mZ4-fqayZdF8cBhheOxDgTDvgqpUAZ9Ov_4s7tOjgbR1U8-p6SOUE6STydiBA0BI73HprxtDWC2Wq3udnUwvxqrr2E7CTisxZ_m95cLozEjRI2bAuTnzf6XO3BnIPMdlwu3FMv0Li1qvq5dxQAnygQsA0Idie4c1lka-SZky9N-i3tDKwu5tDDfnyXQwR3dkMahuDbzzA6i07iC0R4aEB_PyHzAMoIjuQCSG9fPlByb6BGFdzzIu_LmPs3H1u_WmFhguEFhFpxzKyue4xrhMEX3d0eXFE2hubb3fGNQS751zfEi-8gTJQ4imF7CU06M4i3L9HBmQu5jvO0LY72MisaD-BMoAwrpSkN9Doc5CZEVYNAZh7D2tieAO8-L9nb6SrvjlhcMJAwT25F-t01dYzzU0iOAAlINNAM3R6m9eMeKBGQQuQGyu1GWBMceB5jEULG5r_JDaz5mMLBfQtKSprPSxzLuRn9Ivwev6FU29SJyTFIjNXHwWD4j8Gbi-YP-EJb768VMBmXSvTYwBvGwitjiiVZfwUMHVOkRXDHFc_BLMGvRbsJLWhQAR-rPH0-J0b2Fg_YRP58LI3Lcaxu1eCBICS0plhYcvkpPN7YHB7QNbCk-QeOsZ9LwQcpZAk2XTOEg5gMCpoG9L6ohBcxD0r8QexzwF1IytNWxF_mAL6we5ebw8cXs2vPRd1IzFlEpxxHQqUj1Qva5_h9R1FGmlsxExWDK3re9JBqvBt_E2QO77WgVdQkq581iWw-g91I05KEzDsGj8Bf0MW5e0BQiI1opFdhtqW2GD-B3K-Z1nOJt0wJK7cvQlNbIM8DQVrldDAfNe6gLGfWArjvkgvywufJfn_jePt1h09-XMyaMbN90j05a0BHEO0vIUAyKNW2g0bcZ044BBChK7dFwhj1USEd1QC79XvBuyUm6e0gZ_bki59nkdlaCX5hHij9PvpWEa1Hm7E0ieDYYT5nul-DJuTD-Fl83a0ca1o0NGWaeBbUxRSnU4QeGMvUc4Pa75GgNpAju5dWQU1PfEQ1POB5X2hiFzso4JSktjtmwUmRjm5vnCd3fkSLrWSiBak29qGQe7AVTzbWjamv0jAZQ8rw9LFYWvBgPOaEf2QXchWh1Q1gb1e1Q4EX5ottUue6IYr7MPueAJ2wT5cJn0MYQwGAYMVZU4qm9G9q0TzzmjCBbXMY2o85hoFm8XMk2nuUc-zMu1I0Te0SO2PkZEkWKb0wLb5-WGGWioDZBkkxSMS0Guphk-5cWaD9qNVItuQFWiUcFXiXm_sutCPXbeNB0iYmOtC8xxh1PaAcGMK2nGx1ko5bGaAaDtRYqmkM4Qk9PhWSqDsmAH3L7F_LuBGWgXzlLW2p82cSNwSQ7Yk0vi0QO2flisiGKZ2wC56mSstNUOBHXL62sWMA2qBz1QS16ukE_HMh1HsDWtW_sjSOaUnKUCaGIfUqcYA9BhzFZWyuFuoPEVFxx9GF9sjSi0mYk-HSbR31dxzkalJvx8AbeAi92h_hMIrqH7AllCbvXzUafy66z4qKhzQPv47o762lZj8w297cC8qvYJ-0QAopOHXcbyG_WsJX4LCKg2sQF8dMjI2vX-zUKNdttTBU7GjIxYtZn5TmuHjPslSqf89Tav13HsmCqfObB-7qKzbw9PQYLkNuIfnV2FE3x01t7hv7Z1movRnQ2JMUIuyoY0_z1bNF6XnLjKMJdApDTcbwgE2NWBLByHTTs5CLeWfsO0TvLLruFwtNtAfyu37oETUdEFibL3SwXayhqzojMjQSEoFOJEAHV82E7KYvPaumIg0hejANJcLQLPE1htSxhOLizcusQEw7qVsgezcmpkFhAAQVDqDbvgZ1xwJbbhDfvQlnXYt5ITZNjtJxUPT_Nh4DxrZi_uC6Oxc22G8SRhKj6ePO_HoamnejOrAYCj3VNkONbzi_ra9SjFASbRxqoIe7NwkJ2ZhRALOVq83jDOjeuUawQxLJWpK_SMrlYmlBx1Uk1aRjrp4jJCZejaf55fbgzQ9MeugKo66MsFHFutQ6g4SsvOD0ghM5MABNv5hRiQ0bXuN8qX5iEYrahzwQyyIHX7S54C9defAwLmBfR39vwrN37hqyqqkYthJsTWcNYXdBeqNx5mQtieZuxD0qqho9-bZs4wSMV8NJ1pbLUU3SVPxe63jEi8_NAijXwjN8KiwZeQVDw5wKuIhRIdMNlEm7s0dujJA7cdX0hawj6gP3aFetREG8lTfhVwB5BeHbStyyRWciApui5Cuj5qTqH5UHKJAfoh8EjoGqZ33Z26DXYc3aB1mdHPKQkMBJMETalj5N5JwKE9sgskX3LRUSPHoZaiZOUxDPnAPshLvfqZO30R_RQ9-akONZgAhXox08Muz9KbMvIbuz0m5corELlshs1gxPr0OcuJ9QynNAd_hkw8eYwBP3U-8r7B8U_FjEl0Fc55TZngk_bduM-Uis7mDnzryBUFAmzQ5xdUR-newr5bK5l5iujH1rRqibMhgE4zsNrLXd4SNgjW-lK0L4Q6L0SBfkA0XAgVwND5MVsiZhDPcOzDvCog-hCnajM7L5UBQFMhRA1mYdP6aRAnZAAfj5b3SxoS1Qv2JhLwPiftlqMrbmO-Rps9mgtlJd_5gkss64lVRCELtfIH7ZRr9siTHsq5N97vjEGHJj-alT9uiCWwfJPaFBRUF8mvHPiDf33z_dxL_8Wy3fmSDGtO1tqQ4q7heBXL-s7zviE8M1yNwHBKgbAPBtBI6WXnZn2erkHrGz01KAdRy_FlRRFmrfeiJtsYIOSEgOSh7QuzIE7aFiHr9XMjDOBrzPWkXPX9YBuEujSpPXAagoMShFN3yMPfWar3qASD9lCFiATp5zU13IupBIqQqglE1SUCalIEL6vPUWQggT8CDf4jY_d2x9AuH60hfrHHpvbs4oHmZXNceux-EgSK7JWu-RhtLasShNg7wxhYh5Au9ykgr8BTEkzfzWWIDahzGadZPVAJ0U-HPnDzFhEuLYu8gtxKnC_3O-ww-n6NPQlwlyODwN2Hp-Xh9tRXeCDzaZoTlhgM7D6S-vCCRer_V7X_Ke-dNDlyCKhKuOZyoXsSdyglSRrgto_zh_edwGVoJ7LeaPhOlTEjDPQPsRuwaqQrefP3Pv8lg1e9pbi-8JLosELHxVEPmmMrQlcY8WteyM5fvT0h2RNZqcmSbuumXSR86QOOnwioy7NgofeVHSyT1YWBzOrHKNbWokX1IWajQDYZ41j1smnDDfJrGNqKtGRVac05RYWbvpHKKINj-IQSWtPUbaPwrvIEe5dP8uNukugt98UAIDJCutFj9QVb__qxoAVpTMulxcBks4yyNG8VnjFuTMExcOncrnO2aqTx-y0ONYwhiU7JjXfLtiQqd0zAr2ObwqamxUQ8e9eehOk2gTM9vMLlBBDH1CL8dIupUgXWudLpf71jRvjnQrQrjLVVgJKxpyuTMquPdBMZiltconJWuf-Xl81WheQgbImqNTFpoWDLpaxPb_Z9efLKG_jvf6QCEV8A1FbgHrbTFf6-lUZGUPYBZMbyv3xsWc_5dk8CCFvcbugKKH75egMH4yh0PRnMBDgMQoXrFzU8Wil78HBAtxMLId5hRwKQGzlUqDFGDfw4qtP5YKlYxI1nAFM-Li0peDwv65SiEjRXeLpCsKQHrYURK7hDcJnYHpk5Qki-gqk32TfpwwgPOKVHSWoQ3Td98wRh98oQmwtr8AgEFvYOdd9RaccdI8nYKRzIkHv_hEbNheQgP22z5n_4Vsey2USEdjKSMLbYpFoCZXnPVz8EwvJvf6lf7YrlRdOs1sjjsf-5ZRvdtyLYT6hui6nbSdYFSExGGUAHnpjleyuBkMd2iP2yksdDEeMWjRlRzkLC86m4hsSbtU6F_pVYMr9KUyNDybRXhF8Odc3TgOlcPUIbrCxKmh7waFZaBGReHEbDbSRLxEGxupJpiZ6yQbTRIehDjcK0hyp7BMFIVEwsv1o2Tx3hgJcSejahX2k5SknebOcbJRaZqfj6RweUgnq0ifTVTcTpYPfE7u4xosjiVs7-2LnZMNHaCaqTH4nV-qmsr4RFZHZNmUpzt-C7czBv7zp4jZYgOLln-x7qS7TuVYhybOVL3BwE5LGBJzQX2Fzkh9oSfRjIcYuR5TKWsJGhY-hktR7VfeXKdjYnLDItHIxZEfRhPRo9rvJaSKvIcB57yvrnnM8Y1Bw7MKRkLZSai-KbFYVlV9EGZD33Zvz--VNdfwVVbI8_4I8_aI9_yp-ondVp7Ntr0GhUh5aULlJedn2dHPv5_my0
\ No newline at end of file
+xLrjRzqsilwkNw7h_P1jSBBeo_O2Q-nUhElOQN0Jnx2TBhP5OT0IZmuRYJnBKaxTtlxt2v9w8IbA8YKT9sdroJuKEJDyE2I7Ctpv9-E0yhB8epX-4eDxWj9uJybT3WiMlqTy5mESvJ2D4qhUitWB8O_YcBr3U0yeAE8SOSWVzZ0cbE-8yId90MFnhr4Sn_4tXAQGdfASqFXFNttrtoT_-rnBlzcXv1s6ZCLVHd7ycnJbL9I6B8ehEYS9H_U8Frp11EqHn9pz6iLFOvJ6np7276QGniVntpE88GKSflyJnHa5cByqCroTdPzTdLsSdbt7Ho_4TtRq5vH6CU4xICymH_nXeCIR9XU2LS6dCMWasnCwnE_4oDYKJoWK0f_m-3Zcg82CWsAli1S_ury8Xl5nZCdxFtycyfVxr3__Rr5auoUOd-ptbDn3EWxY_FgL3DA1KcyPf8-3HPp278xbZc9Ck2Z3uSyyYXa7l6JnSGnnMVIXUKho7DmH2ZZvOo7QWySvok5ZuB15EHme9SK8mS6t_dtrVa50VevWdbv1jYUOJG11aF06GcHkAfWHaD_0Ofy33gKGbvGK-y7nprtYC935jCAawbe__3Y4uXLafAJ9em98Avu3fHkn3WEH8YwReXvo54pYF_dpTW2gMwFuhr6CGQ5mb-cHvBIb8h4YwK_jx__txDbGC_Mtv7WB4eeoX44-qd96MyqTER80NiksSP8J1gzBjeSuFILOT8Lcij6_NK702AuddXZYz3cGyv9W4iM8oPh1_E4iHHoAgjmHaaCWYcn1pcIUAyx17ZH4X4AqCvjikUPRCz7T0vhi0Ftpbrz__PccCtMvdaxxvkudA0KwHHnmHB1Fpqsgl8rCuTDLGJ3YX2ASNS4jf10dK5JY9qRmtLBJMLitKj8fWtf3m5jHcnAj5ahU0mGguJxalDz8FSq0M0AcZyqaQ2ISFgCj0SidEBTraUjCSFhKndFnXuDBIkvH2jEUEMwCgcEYwnOPQUeRiaV9XsbdBbWFb5UYVP0Eu6y8-03HhVfSau1CudhO1IkWRuH_n93dYyasH5F3KoNqjv32rcxeDG5OkHT0tGWlJqrAzhU6fzRjHio4wQDWU8D-WcDU_r1tTlhgVq-RpsyPJACOf2aIvXF8BmWNhh94wO9mOM1NWMQyT8-D_aFYo4WuthevOFojml2R1pzNHeZJWy8C4JpOy2B38_NYfx7mj2axjnNdoqZJyCgdMVKni_zPpTpqEg1yUACB3f0dfLZYfzU6BwlMbdRa420GUaNojf-dC4Vta3wyXdn7qeveQKbh0pw52If0_kUlDbzyvbBNTjdVNSiKGcbabifkpm7EIf3PM8Z2ESn0ebJ6bCMfynfNN1PtaBxPNWBAKOBsmlVRDaWKxq461KAm4aEa63bjIVQKP1GKeirSdsaz-7O__AU4Z4ibi4PQuOite67bNO6Oq1shyUG7pg6990SqW_oIx4M32mLti9ScWlC31mxz4r_k4UKbo9zNok3IqcQyrtsXGfN2HtgTWlUeRxXWcs6032u6f7Y7jOLqiQIYZA9HW3sx8GEhF8jWBVyzo55wFeZOTrMMgQDUvJumTVQfw0Txa04Et-3Na6RGS4LmMi83EUera8kYn2YHAl-APBwhpyGgDxYuDhD-HElDZcWUDQVrX3CuHTQ1jaE57KI-570Gwg_Hw-xda1djLBDS9uPRxMcy5vCzu-mDVhlF2N2wvWxR8Sxk8UPgNBn4EU4NGDaFfn1pI6KxUIW9jAWCDBtDi84I6vm7A9WYFagaovJjmZsuXLfwGc0NgTwsyHkez1bHirh4vEnTyTEtLrTd5pUtDozVdrtVdBo-TFK_yXwRYBlDwG1e76ox1j-U97R56iIyXtFF8KSCsTVe5sZMvXryS3H_xP8GClHeY1aX3Xx2HAWXD7wSM4R0uCJLOo-4H40-jGX-Zl2xRq3ohj4l0-tW26q8L1yk2almIRqWlDAzElABbJ8xOXiQems1y91-6pT8-hniWXKwOtlvVEP6RSVYjCx8h4YyHWNA0KNyuGKbvTulF1kk_nQRaQ9MxWEvuk3FILwcC7svlI3y92qGTZJoRmDWZGEr3GrYJxF2byL53UxV0lOIonKaw8abTpnO5bXo4ZpT0PnvZ0SXyydIaVhfSu_jByFm_RNROqxojETBunHI9FgiHVb1kYUwtXW3TzV4uTcZwwAUKrFuK4jJqI5CAsilHTGMIsOWmNiJ1IYDVGDRKNqZGB53k6L3zhKph7O6LhfUfKftdnFNDgPNmt1WT8dDq2SQDRcDmatLcbNev3yLZqyYYEC4PcnYrsVpaSBDhDx9vsgQyIOvH7YaoOS8Rg85zP9e1zXEx_ILf1UcuqHFVarIj4MBDQACDrNnW96Mxbsv-vbBqvLTEQak0OMOXvWH8TlSFoPBkPBaEmJqS9F7ygwx5-wSakAw09Ip7OJyXeFB761zoJEy5OvoTvnzxXOvYVo5Z3kzGlZTiYSM_9ioQyK8dcd378zIlYkupq42YogUETixwoIwaA_sa5vOMsxFvhTgEgODVS1GbPw-258fLGhXxCNYcZZTyAPj5VL-RmBTHJTydZlSWinGmRedplCr0unBa9-NE4KuUqqmttL89Ybo3T2L6hi-jKDh_v0WjHIwtQ7os_KU9WZaYF6p0g1yB9EJ4C8oA2pDYL69zCpMdgSgI3ycKYURKoWzFZzWHeXD1ASaTPPtJ22Q7fDo_q5ymMDk6iCnyplo1ailc7lqS2nZWeR4C4mFj63pSrpvivymUmhRwxAyor5YR3NS0GhJVo4vQ9b7wrxAuD1pm85lu4U5ayw78_4lcqp1e1iHdlWuVlBo-Td5pSlpbsVFdnmV7ybLd2NrhIfpv8-It4Y0jwIDYHcB9WeJGjDJKggzI5KMbknW0RgAhLNMmstfluD18JsASlWk0F8UYr3v2HThG7rVLdmSVruvZXB8M7y6_qQQsa1yVpxOHXRwI_Qx56uHHhgxIWPh8GPtEUpPOB3b-sl8ESAPKSRgmaA3qdA22qwepMl0tZK5zu1teNWDK0wfkIlbJeHKkNiNc3Ppszl2feQzcxa5YfbVhZFi_MrFuv_8dMDBuLEnpKxus6ORQnIoCkVDpa9M9aARjH5HHmnpY05Ee0zidA8iWxJPHjlaqdkbHRFfdO33ZD073V89stQavqPtlVhF2zSAXcsPEv5eZdR9km2kow80547lgTfOyRhcQcrbVlIN8wtCaLeCt2Fu_cNg9l1QEtScyjLKwb97GHECz3ES1kL-wnlUyJoL21YZX1HlQ0Om-iNUrVlrvg_UzXOiYDfOSwqE3dJ861CRmepJCXakGUK8KEy_7RV09KBFWrRhROEU8xcayhOBjoh_kAPKX6jLQSeTDIeBuJYL5ogZeRQYt54x2mvCq1rfLA2gfbQNFmhRK2CfLV6NUXgnVBuVWuuJ1iUNjmcX54fzV0v1-WfXEDz1LYcm1_V6vQWVqfsfoTyY9dvxsOBpLJbS1PLy3mEQ-8XZyTQGgM7vSQYmdxAba4DQ86oUhJRw8mPKdDouRpND4QvTKFdqnbtGBkxgsg4ysyFryZ9A3j4n9YxSFnLDNaG3brOwUv1yD9MxvW2dWAQFPvImYgENqjRYQOfQ5Vx4ZE57rCtVtR1uqF5m7GXdjSsZZXlhCcHTXZXMh0kESlQA-V1Zd5fULDNnzaFDNjH_OjuibUy65V23p_JVF0JhHfrlLFx76BYrrbYmurYwy5ZzvlTZw-ZrMThKwM2rMZh1BUDt17hsWBXeguh31TNsWf2xy3wbfhNtRLeUAKrTISaJec_GL9SMSmwINM7251WQK0res8W16jUzeSEXWyOwUDcHhhCOF_nO5-9qMDHTWBW8RfjLdTDBFhw5W7txfLyloagKYaZk9FtL3p0rZJtjUe2FxMX_-FOeIx2v-vHxwdd8lLLjLU3F_9h3VSYn75oL6MgwAikdTHod0qRtA0xXhgCu8mNbCPnE7hi97kDgEs-Tqg-oLpzlfcpLI4Yn5yEBS-RJhiTz_Ashqsvj_FriOPrmGF5ZEVrrOntzDD8_mFclBgP-fxtypcdffxkcN72wnC7G6O-SXFQ2GnyVquvbJxiCTVBfiosYZ9zM0w9FtfVuKfmkaBjNntDV47uj5Xb-SbHryz9_Jvb5Lc-HHMPlYKLcNu5xZ-JrdS7JnfAs8-kQlD1ApED2l6bNgL-AFHlogaS_zqNO8CuICZZ1Nell7_43CvVgWCubf8nW3FEvnhfDpxX56fgtvOJMi7DGfDh0BXXshjPIjwtEsrqCjrirte-wFw3pz9CQU5pABFktwyEUmYtwsN5NzDQLN7TFzxcdgYrS9_WESRfvkh7MEh2igsVT23NNmmuU97Dy-JS4DFJmsC2xQ1zgQ1iwQ4W7hpmk8RBIn8cE9yV7uZT8nDsvzu0-89pf06IUOoSUkf_lIjtEUqQfiOZfExImxEAsS5-pJFQmkMVPlaz6IliDIH-QxVZrjLtEHEUvn9AY0JpyFGh-ntqAWF2Kq1pDcrBIcb3N3gawDTfUG0xCUmth53y7Ne4Aw5HBjvKXxkdnVl8Xv4hYZ-4n1zyhd7sv8zQUiJLdsHaZZhVfNkQvSpQpZOMs_iekEe3jc4AMKBHtzkg5BSjhACrAxT9HtvEL8MS3MOVBaBdPoNnWFUuohCwQ89orSQK_EYRUrm9FkGjuoMjg9zyzsrOU5TS9eL3UlmgItQAZd70BVM7PiGJaNzhNwbOsqArjhJ4XMUTHXU3sHHoUvD3lAEeXmZ4-jqayZGl8c7hheOdDgTjvwvEyTMImESVRLknMPAs3yHvtCqozSCwwvFSUK0IaEMYUNVSs0mA3GeUS5FldlhVr9OGBspKVFl8dM5I7ez_SGqfH2fVk8sNBOgH8VKhxK1TyteEyXDt8WSxQHsDbeQug1XnX1-qHwBl-AxV3M_AxvZ97jHxuGACT9--WovRZhgM4R_Crbx86u34Em8uKyEh0RoJjc7o2X_OCOMBvzefaQXculAV6xUVAGTO6ZUFZ-EJgwkJVF_nOpoiIl5kPwqZS2o2gS5Bo7Q7IW-ouEQ2kfEiz8gTJQ4Sml71U06M4i3L9HBmQu7jvOGLY72MitaD-AsoAsqAykN9Bgc5CZEVYNgZR0j1tCeEO8-LDnf7sMEgjR7BbL8ZY_vGWH_pX5Z1MKh6tv3O5R7T704thS2785JVj0wS0OK5BRS5YshSweAvNGqRFPKAjg_LxD7DzkNFVjK5CcQlkQDGZtXZt_SdpQbNe0KgxX2IquvFu_kGSvHnIloXyMM3dibZkm1fypUBxarSdzYDc7bupOSxVQZbaMf7V0jP9kdle9BL0Zamv8k_FaarPAjMGIVeEY1P3omZ7yFqj9UzlpPK7oLD7QhNCksKeSxJ4AtCJHnKNfIjCWp3rR2PPeUf2PNbpLeYw40sz-x7YvSRBuVbVeN93jK8q256xGx3QyhpYvOcN_RxTYjO0Mkiyw8_LEhY7uMLxTbUm1i1QSHh4QUrxNZ2jy5Zm33ojlG9a0sGzFR42f00QV2bx1Sa5qWfG1w1rLUA0vVdpLnwGf04LIwsAYGVh0KvlESWwldgHMOBOLAstpkeeqIrAeJmArDrkghuxuvRgnlbRPt1h19-WLybNbN91jW9a0RHEO0vG1fuKNm6g0LgZ0MC8hSlK0N26dz9USUh1QSN8XP7xyUmLe1gWuLUkLvnid8OkXLZHij5QvpaFa5Pm7E2ieDYWz3vulEBJuDF_Fl8AaWka5I0NG2ig7LJwNyzT4MeGMfMd5veL5GkLtgTuLtXQU5PeUg5POR5W2NaEzm-7JCYpjtyuU4RVmbrnCd7gUSLrXSa9a-E9q1Mf0QJSxrcka8r3jghOA5whLlcWv7YOOaMg3wbrh0h2QXsaHe5Q4Uf5oFtUuOgIYLBVOuehJYwSLcNo06cTw0AXMllT64uhGBq0zRnpjyBaXcc5o89eFVyH2D4AZmrF_wjtAq0wG2im5Z1ZRzQjA1aehRj0nn1QaR6HSR-zjO8Zm76-zRL28w7fVUXhnKV3PzKR2vVr-jb-PB9re771iYuQtC0uthDQaQcHMK6nGB5Vo5fHaQYClRcrmkI6QUHPhXKsDsmhH3P40-zxhGWfXBhFWwt826HMwyU5YkCvi1MO6fZ-ryHMZ2oCLcmSsFI_ORPYL6EqWcA1qdr0QyL5uE9-Hsl5Hc5ZlmpsXyObUXAVC4OIfDubYQ98hjFZeo-EucVFVlZJNGwITvjz3X3Uy1rAtc78s3--_F7PCvb5Em8iv6f-RUGpqP4wVUPBpBuz9JwECv6qKN_QBr4qy5QMnKqyoWTJYzslSNo4Z-BEXUBniNWEpapRnA2NMUHu2YU0_zWRJV6nnLjasJZIp6ABIzMuYhq5Qaf4VKyaJ1Qe-vXEHwecowRlGrrsDCD1Aw3kaPuYlYfvGDdgBXqgcIntGBdwLN5SjiTGIhLqdY2SAcX9DasWEX3OaK9dNcTQXAFnhgEBRVrjCMxca3zKqKxknMHmzv5DJDklY_5IW_XOFMMknNXg_MACSPjrCUxzFTrctjUlGtWtEmNZuvZjOO90ZXXlKaMZLZr6IpF5Y3hFRqmqHLJPu-hPN_lBHgQVKr9qoioIe7DhjZ2_hBIDOHu93lDOTgwiaYQVbRWmK_VMn_YuklRV-k1aRhsF4TJCZWT8fDLffcywAMeugKs61MsFHFutQcg4SsvPj0ghMDMABNv5dJl-0rXuFAaW5b4YrqRz6SuyInX7K4eC9deggwHmBhB29vwjN6dfqyqykXthJsTWcNYXdFeqNx5mQtieZvwWSziAycUrDfrFt1cg5SnSwVMAs74dUE0WxRfwlHghhKyRLoAB-gwKNdRbkfT4Q-rf5wSBS1_Xv-8KIjufeGAv_hIfM2x3gBlNlCCk6yo-Y1IwqTNFl3Muvl2iUJ2JU3JVNr5JtiI42gSSoFly5DAm0uoX3CFKGIXOc3uhQjEnfQRHxgYz8cugVIXHkzMLi6OhBpXAEOUbwN1tXbCngJHjtZi21hFHNobeQiBqr4bqvTW5ACILCotPeJoTXeQnO6lCsQf_1LFhxY0HSvj8UOFXHlrtyvuewZ99TkFt9EeuSlsPzbtq9gp8dazTz_0Y_Ctx6Gp-vgSU_lOrrq7h8bVvrewsFjK6jPMyk1YDGrvFstIIMjwptLUL-z_XjGgctmn0LKH1TR1WAWv8gFgPlb2KrS_w6fID-Tb4piogVviYMNEvSRs8Lh-g1WgdQ9z4KJQMaJINDcjfJfUpn2dS8WpJLZrpvRlVeSPB0rzqlaJXrdUllsBJTLjCfM-suKPloaWFj_cJjQunjW9roFnQyGYdRzBUQ3nOPDrk6Z8UMoMFOqvHba4AqsyUDksFoEEYmadJW7r1f34HT2uOstG_DYKVHyJcca6Ne5QMsdoHavRx6huv0ZMQl6q9EW2gT7jv_eDj5jvQi-LfRiBeqI7rSQF3TG0fdDp7k2waeer6iA4UqwSG2manr47yUiP2WlIrX9ErVgdQ8asGjGHzsc0s_m1xkd1nZPnNWYszj6X9RteN70jACZXIkMKP6AZcIZFQHBOiPe759N69m3PEgQA1kz1cIE0SAynFFVrrIIbwS73oDMwbMcgs6XokcuoBIk6UBBLPKgkzTKtGuW4aRAJwHPAco-Gd0puWppRuV65nhJqGLlwe3P-7XznrxoEkqZRDVwqNqk4XNz3LJkp4KN_xB7bwNNKTEQ8bjoSPt5ht-FZsf057kRRtOvAeum7vZJisFvTVwtdLwY_ThtgLz7jSvlgpI4FimUdLcihCR5-TIQjQKSUVCyiLL8y4bnsNa1evxFIeSVaiuO7QT5nHaGRqzAYqykTLXBfXQJQDIq-KGcFaZ59CurNRzpfrPSnFekUE0bI9-hwegBfdO_8WfOJE7BjosGJQPcIofQmFwfFejlXM16jmGoavfw6QAkdED-4Sj6kMDDBxDdK2pTeUASNVJRmZ1LH4edOUNsglEQt-7zvXFf-lStTn7tF7TkBv4lWmdiUl7zjiDffzEmXC7jtT0rDukQp6ba_NQbJdEkQT2Q54xL6olWJJRc-MQOZI9YcOMdCUkUgDrMaLGJ5Ibv9C7YgOA6wE5AxTRGlkpAftzkeRjHRdsRjXooc3CtTKjTzo1mMu-6ViM40mLqFLAXQQhkd7aGDLpZxfBV6JIHkfXlRtICqOInmh4EJh3MLD-apwzg93vs4kDkRnaTlOIxuJMufpmFZRVIfE6eeW5gsCd506BkErPj6rN4Ef-xb655m-3f5G_gwTL8fRToVL5DhsVfwMjl4bdBOhI5mHxyQ8GwaFjG9U0FNUnRXYqRCE3-LYpZQAj3xPXD9hht8H6zefL3ltMPCQJD2UMpVj33k9bcFGRCdE7Z5T5oWq7Ewj3r5zQdxcwPJxYaptXCGa5asHslNuNKE_TZNKAIF7V_5H_9l6at3ku5dFaRKbningz9oXzIVji4wPpsajtQFbpSq-pC4wU_ONOQDlsNUn-2_4M0d_yGHe1yfZ364QSnBVmRZUfdD7-2vE4HNb26PFF6HBl_xhLv__RKWuT-GWyJZ6ksoZBWkvQSAna6Dtyvfr3a5hSxVjovb1s8XTBqgwnn__ByHtfAotYflfdSH5vJ4ymRfJ5otBo4kfdISBf-f3udDiCK0dAc-gDAuM9zyRfrcMZU5LkzbHKcQxZW6yCJztZ4dpkzkISmZUmsuWv_W9pK4XN2SefuvMkLZQL1iYlMaCdjHZZGD0zlBh_iDS8gVJH-0-v3Mrlp3_yrjJMVGSP9exY9Y-zeLig4m_6Z6jWzdRlwSFDfNpFxY9RN5KmtRYzrFfuVxm_5puQm-h6NoTAgWMdcn24VxTM3avAtUbD5qo5TKW-JmhY-hktQbrKqHgJ-nKAgfkKVHFYDEooAF_1m00
\ No newline at end of file
diff --git a/docs/logical_data_model.puml b/docs/logical_data_model.puml
index e9ae59b9f5..0e0e158738 100644
--- a/docs/logical_data_model.puml
+++ b/docs/logical_data_model.puml
@@ -88,11 +88,9 @@ class ActivityReportGoals{
class ActivityReportObjectiveCitations{
* id : integer :
- * activityReportObjectiveId : integer : REFERENCES "ActivityReportObjectives".id
- * citationId : integer : REFERENCES "MonitoringStandards".id
-!issue='column reference does not match model: "MonitoringFindings".id !== "MonitoringFindingLinks"."findingId"' * findingId : integer : REFERENCES "MonitoringFindings".id
-!issue='column reference does not match model: "MonitoringReviews".id !== "MonitoringReviewLinks"."reviewId"' * reviewId : integer : REFERENCES "MonitoringReviews".id
+ * citation : text
* createdAt : timestamp with time zone : now()
+ * monitoringReferences : jsonb
* updatedAt : timestamp with time zone : now()
}
@@ -2503,17 +2501,11 @@ class ZALZAFilter{
Files "1" --[#black,plain,thickness=2]-- "1" ImportFiles : importFile, file
Grants "1" --[#black,plain,thickness=2]-- "1" GrantNumberLinks : grant, grantNumberLink
-!issue='associations need to be defined both directions'
-MonitoringFindingLinks "1" --[#d54309,plain,thickness=2]-- "1" ActivityReportObjectiveCitations : finding
-!issue='associations need to be defined both directions'
-MonitoringReviewLinks "1" --[#d54309,plain,thickness=2]-- "1" ActivityReportObjectiveCitations : review
-!issue='associations need to be defined both directions'
-MonitoringStandardLinks "1" --[#d54309,plain,thickness=2]-- "1" ActivityReportObjectiveCitations : citation
ActivityReportCollaborators "1" --[#black,dashed,thickness=2]--{ "n" CollaboratorRoles : collaboratorRoles, activityReportCollaborator
ActivityReportGoals "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportGoalFieldResponses : activityReportGoal, activityReportGoalFieldResponses
ActivityReportGoals "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportGoalResources : activityReportGoal, activityReportGoalResources
-ActivityReportObjectives "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportObjectiveCitations : activityReportObjective, activityReportObjectiveCitations
+ActivityReportObjectives "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportObjectiveCitations : activityReportObjective, activityReportObjectiveCitation, activityReportObjectiveCitations
ActivityReportObjectives "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportObjectiveCourses : activityReportObjective, activityReportObjectiveCourses
ActivityReportObjectives "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportObjectiveFiles : activityReportObjective, activityReportObjectiveFiles
ActivityReportObjectives "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportObjectiveResources : activityReportObjective, activityReportObjectiveResources
@@ -2591,7 +2583,7 @@ MonitoringReviewLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringReview
MonitoringReviewStatusLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringReviewStatuses : monitoringReviewStatuses, statusLink
MonitoringReviewStatusLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringReviews : monitoringReviews, statusLink
MonitoringStandardLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringFindingStandards : statusLink, monitoringFindingStandards
-MonitoringStandardLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringStandards : monitoringStandardes, statusLink
+MonitoringStandardLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringStandards : monitoringStandards, statusLink
NationalCenters "1" --[#black,dashed,thickness=2]--{ "n" EventReportPilotNationalCenterUsers : nationalCenter, eventReportPilotNationalCenterUsers
NationalCenters "1" --[#black,dashed,thickness=2]--{ "n" NationalCenterUsers : nationalCenter, nationalCenterUsers
NationalCenters "1" --[#black,dashed,thickness=2]--{ "n" NationalCenters : mapsToNationalCenter, mapsFromNationalCenters
@@ -2683,11 +2675,4 @@ Roles "n" }--[#black,dotted,thickness=2]--{ "n" Topics : topics, roles
Roles "n" }--[#black,dotted,thickness=2]--{ "n" Users : users, roles
Scopes "n" }--[#black,dotted,thickness=2]--{ "n" Users : users, scopes
-!issue='association missing from models'!issue='associations need to be defined both directions'
-MonitoringFindings o--[#yellow,bold,thickness=2]--o ActivityReportObjectiveCitations : missing-from-model
-!issue='associations need to be defined both directions'
-MonitoringReviews o--[#yellow,bold,thickness=2]--o ActivityReportObjectiveCitations : missing-from-model
-!issue='associations need to be defined both directions'
-MonitoringStandards o--[#yellow,bold,thickness=2]--o ActivityReportObjectiveCitations : missing-from-model
-
@enduml
diff --git a/src/migrations/20241118093025-add-activity-report-objective-citations.js b/src/migrations/20241118093025-add-activity-report-objective-citations.js
index 4cfda2d629..b995131b8e 100644
--- a/src/migrations/20241118093025-add-activity-report-objective-citations.js
+++ b/src/migrations/20241118093025-add-activity-report-objective-citations.js
@@ -4,48 +4,20 @@ module.exports = {
up: async (queryInterface, Sequelize) => queryInterface.sequelize.transaction(
async (transaction) => {
await prepMigration(queryInterface, transaction, __filename);
- // Create ActivityReportObjectiveCitations table
+ // Create ActivityReportObjectiveCitations table.
await queryInterface.createTable('ActivityReportObjectiveCitations', {
id: {
type: Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true,
},
- activityReportObjectiveId: {
- type: Sequelize.INTEGER,
- allowNull: false,
- references: {
- model: {
- tableName: 'ActivityReportObjectives',
- },
- },
- },
- reviewId: {
- type: Sequelize.INTEGER,
+ citation: {
+ type: Sequelize.TEXT,
allowNull: false,
- references: {
- model: {
- tableName: 'MonitoringReviews',
- },
- },
},
- findingId: {
- type: Sequelize.INTEGER,
- allowNull: false,
- references: {
- model: {
- tableName: 'MonitoringFindings',
- },
- },
- },
- citationId: {
- type: Sequelize.INTEGER,
+ monitoringReferences: {
+ type: Sequelize.JSONB,
allowNull: false,
- references: {
- model: {
- tableName: 'MonitoringStandards',
- },
- },
},
createdAt: {
type: Sequelize.DATE,
diff --git a/src/models/activityReportObjectiveCitation.js b/src/models/activityReportObjectiveCitation.js
index 44264e8901..6e36537764 100644
--- a/src/models/activityReportObjectiveCitation.js
+++ b/src/models/activityReportObjectiveCitation.js
@@ -15,42 +15,8 @@ export default (sequelize, DataTypes) => {
as: 'activityReportObjective',
});
- // Note: We join these to the 'link' tables,
- // that inturn join to the monitoring tables via a GUID.
- // Review.
- ActivityReportObjectiveCitation.belongsTo(models.MonitoringReviewLink, { foreignKey: 'reviewId', as: 'review' });
-
- /*
- ActivityReportObjectiveCitation.belongsToMany(models.MonitoringReview, {
- through: models.MonitoringReviewLink,
- foreignKey: 'reviewId',
- otherKey: 'reviewId',
- as: 'reviews',
- });
- */
-
- // Finding.
- ActivityReportObjectiveCitation.belongsTo(models.MonitoringFindingLink, { foreignKey: 'findingId', as: 'finding' });
-
- /*
- ActivityReportObjectiveCitation.belongsToMany(models.MonitoringFinding, {
- through: models.MonitoringFindingLink,
- foreignKey: 'findingId',
- otherKey: 'findingId',
- as: 'findings',
- });
- */
-
// Citation (standard).
- ActivityReportObjectiveCitation.belongsTo(models.MonitoringStandardLink, { foreignKey: 'citationId', as: 'citation' });
- /*
- ActivityReportObjectiveCitation.belongsToMany(models.MonitoringStandard, {
- through: models.MonitoringStandardLink,
- foreignKey: 'standardId',
- otherKey: 'standardId',
- as: 'citations',
- });
- */
+ ActivityReportObjectiveCitation.belongsTo(models.ActivityReportObjective, { foreignKey: 'activityReportObjectiveId', as: 'activityReportObjectiveCitation' });
}
}
ActivityReportObjectiveCitation.init({
@@ -60,21 +26,23 @@ export default (sequelize, DataTypes) => {
primaryKey: true,
type: DataTypes.INTEGER,
},
- activityReportObjectiveId: {
- type: DataTypes.INTEGER,
+ citation: {
+ type: DataTypes.TEXT,
allowNull: false,
},
- reviewId: {
- type: DataTypes.INTEGER,
+ monitoringReferences: {
+ type: DataTypes.JSONB,
allowNull: false,
},
- findingId: {
- type: DataTypes.INTEGER,
+ createdAt: {
+ type: DataTypes.DATE,
allowNull: false,
+ defaultValue: DataTypes.NOW,
},
- citationId: {
- type: DataTypes.INTEGER,
+ updatedAt: {
+ type: DataTypes.DATE,
allowNull: false,
+ defaultValue: DataTypes.NOW,
},
}, {
sequelize,
diff --git a/src/models/monitoringFinding.js b/src/models/monitoringFinding.js
index c71b2b0be8..3f2d2d0639 100644
--- a/src/models/monitoringFinding.js
+++ b/src/models/monitoringFinding.js
@@ -12,6 +12,7 @@ export default (sequelize, DataTypes) => {
* monitoringFindingLink: MonitoringFindingLink.statusId >- statusId
* status: statusId -< MonitoringFindingLink.statusId
*/
+
models.MonitoringFindingLink.hasMany(
models.MonitoringFinding,
{
diff --git a/src/models/monitoringFindingLink.js b/src/models/monitoringFindingLink.js
index 4297669378..467a74b79c 100644
--- a/src/models/monitoringFindingLink.js
+++ b/src/models/monitoringFindingLink.js
@@ -24,8 +24,6 @@ export default (sequelize, DataTypes) => {
* monitoringClassSummaries: MonitoringClassSummary.findingId >- findingId
* monitoringFindingLink: findingId -< MonitoringClassSummary.findingId
*/
- // eslint-disable-next-line max-len
- // MonitoringFindingLink.hasMany(models.ActivityReportObjectiveCitation, { foreignKey: 'findingId', as: 'activityReportObjectiveCitationFinding' });
}
}
MonitoringFindingLink.init({
diff --git a/src/models/monitoringReviewLinks.js b/src/models/monitoringReviewLinks.js
index 38bd6cdefd..458ad116d3 100644
--- a/src/models/monitoringReviewLinks.js
+++ b/src/models/monitoringReviewLinks.js
@@ -24,8 +24,6 @@ export default (sequelize, DataTypes) => {
* monitoringClassSummaries: MonitoringClassSummary.reviewId >- reviewId
* monitoringReviewLink: reviewId -< MonitoringClassSummary.reviewId
*/
- // eslint-disable-next-line max-len
- // MonitoringReviewLink.hasMany(models.ActivityReportObjectiveCitation, { foreignKey: 'reviewId', as: 'activityReportObjectiveCitationReview' });
}
}
MonitoringReviewLink.init({
diff --git a/src/models/monitoringStandard.js b/src/models/monitoringStandard.js
index 231b743433..8ea9ad920b 100644
--- a/src/models/monitoringStandard.js
+++ b/src/models/monitoringStandard.js
@@ -12,11 +12,12 @@ export default (sequelize, DataTypes) => {
* monitoringStandardLink: MonitoringStandardLink.standardId >- standardId
* status: standardId -< MonitoringStandardLink.standardId
*/
+
models.MonitoringStandardLink.hasMany(
models.MonitoringStandard,
{
foreignKey: 'standardId',
- as: 'monitoringStandardes',
+ as: 'monitoringStandards',
},
);
diff --git a/src/routes/citations/handlers.js b/src/routes/citations/handlers.js
index f83cfc415e..b9ccccc21d 100644
--- a/src/routes/citations/handlers.js
+++ b/src/routes/citations/handlers.js
@@ -16,7 +16,8 @@ const logContext = { namespace };
export const getCitationsByGrants = async (req, res) => {
try {
// Get the grant we need citations for.
- const { regionId } = req.params;
+ const { regionId, reportStartDate } = req.params;
+
const { grantIds } = req.query;
const userId = await currentUserId(req, res);
@@ -28,8 +29,11 @@ export const getCitationsByGrants = async (req, res) => {
return;
}
+ // Convert reportStartDate to the format 'YYYY-MM-DD'.
+ const formattedStartDate = new Date(reportStartDate).toISOString().split('T')[0];
+
// Get the citations for the grant.
- const citations = await getCitationsByGrantIds(grantIds);
+ const citations = await getCitationsByGrantIds(grantIds, formattedStartDate);
// Return the citations.
res.status(httpCodes.OK).send(citations);
diff --git a/src/routes/citations/handlers.test.js b/src/routes/citations/handlers.test.js
index 2551d06bad..d23dbcfcf1 100644
--- a/src/routes/citations/handlers.test.js
+++ b/src/routes/citations/handlers.test.js
@@ -30,6 +30,7 @@ describe('Citation handlers', () => {
},
params: {
regionId: 1,
+ reportStartDate: '2024-10-01',
},
};
@@ -64,7 +65,7 @@ describe('Citation handlers', () => {
expect(currentUserId).toHaveBeenCalledWith(req, res);
expect(userById).toHaveBeenCalledWith(user.id);
- expect(getCitationsByGrantIds).toHaveBeenCalledWith(req.query.grantIds);
+ expect(getCitationsByGrantIds).toHaveBeenCalledWith(req.query.grantIds, '2024-10-01');
expect(res.status).toHaveBeenCalledWith(200);
expect(res.send).toHaveBeenCalledWith(citations);
});
@@ -77,6 +78,7 @@ describe('Citation handlers', () => {
},
params: {
regionId: 1,
+ reportStartDate: '2024-10-01',
},
};
@@ -109,7 +111,7 @@ describe('Citation handlers', () => {
expect(currentUserId).toHaveBeenCalledWith(req, res);
expect(userById).toHaveBeenCalledWith(user.id);
- expect(getCitationsByGrantIds).toHaveBeenCalledWith(req.query.grantIds);
+ expect(getCitationsByGrantIds).toHaveBeenCalledWith(req.query.grantIds, '2024-10-01');
expect(res.sendStatus).toHaveBeenCalledWith(500);
});
@@ -121,6 +123,7 @@ describe('Citation handlers', () => {
},
params: {
regionId: 1,
+ reportStartDate: '2024-10-01',
},
};
diff --git a/src/services/citations.js b/src/services/citations.js
index 73d2c5192b..9518cd8415 100644
--- a/src/services/citations.js
+++ b/src/services/citations.js
@@ -7,13 +7,12 @@ import { sequelize } from '../models';
We then need to format the response for how it needs to be
displayed on the FE for selection on objectives.
*/
-export async function getCitationsByGrantIds(grantIds) {
+export async function getCitationsByGrantIds(grantIds, reportStartDate) {
/*
Questions:
- Do we need to take into account the grant replacements table? (what if a grant was replaced?)
- Is it enough to join on grant number? Or do we need to use links table?
*/
-
const cutOffStartDate = '2024-10-01'; // TODO: Set this before we deploy to prod.
// Query to get the citations by grant id.
@@ -69,7 +68,7 @@ export async function getCitationsByGrantIds(grantIds) {
AND mrg."granteeId" = mfg."granteeId"
WHERE 1 = 1
AND gr.id IN (${grantIds.join(',')}) -- :grantIds
- AND mfh."reportDeliveryDate" BETWEEN '${cutOffStartDate}' AND NOW() -- Between is inclusive.
+ AND mfh."reportDeliveryDate"::date BETWEEN '${cutOffStartDate}' AND '${reportStartDate}'
AND gr.status = 'Active'
AND mfs.name = 'Active'
GROUP BY 1,2
@@ -77,41 +76,5 @@ export async function getCitationsByGrantIds(grantIds) {
`,
);
- // From the response we need to get a list of citations for each grant.
- const citationsByGrant = Object.values(grantsByCitations[0].reduce(
- (acc, citation) => {
- const { grants } = citation;
- // Create a for loop to iterate over every object in the grants array.
- for (let i = 0; i < grants.length; i++) {
- const grant = grants[i];
- // Check if the grant number is already in the accumulator.
- if (!acc[grant.grantId]) {
- acc[grant.grantId] = {
- grantId: grant.grantId,
- citations: [],
- };
- }
-
- // Build a citation object to push into the array.
- const citationObject = {
- findingType: grant.findingType,
- findingSource: grant.findingSource,
- grantId: grant.grantId,
- standardId: citation.standardId,
- citation: citation.citation,
- findingId: grant.findingId,
- reviewName: grant.reviewName,
- grantNumber: grant.grantNumber,
- reportDeliveryDate: grant.reportDeliveryDate,
- monitoringFindingStatusName: grant.monitoringFindingStatusName,
- };
- // Push the citation into the array for the grant number.
- acc[grant.grantId].citations.push(citationObject);
- }
- return acc;
- },
- {},
- ));
-
- return citationsByGrant;
+ return grantsByCitations[0];
}
diff --git a/src/services/citations.test.js b/src/services/citations.test.js
index a3807cb57d..f13520f14c 100644
--- a/src/services/citations.test.js
+++ b/src/services/citations.test.js
@@ -309,47 +309,42 @@ describe('citations service', () => {
it('correctly retrieves citations per grant', async () => {
// Call the service to get the citations by grant ids.
- const citationsToAssert = await getCitationsByGrantIds([grant1.id, grant1a.id, grant2.id, grant3.id]);
- // Get the first citation object for grant 1.
- const grant1Citations = citationsToAssert.find((g) => g.grantId === grant1.id);
- expect(grant1Citations).toBeDefined();
- expect(grant1Citations.citations.length).toBe(2);
-
- // Get the first citation ''Grant 1 - Citation 1 - Good'.
- const citation1 = grant1Citations.citations.find((c) => c.citation === 'Grant 1 - Citation 1 - Good');
- expect(citation1).toBeDefined();
- expect(citation1.findingType).toBe('Citation 1 Monitoring Finding Type');
- expect(citation1.findingSource).toBe('Internal Controls');
- expect(citation1.grantNumber).toBe(grant1.number);
-
- // Get the second citation ''Grant 1 - Citation 3 - Good 2'.
- const citation2 = grant1Citations.citations.find((c) => c.citation === 'Grant 1 - Citation 3 - Good 2');
- expect(citation2).toBeDefined();
- expect(citation2.findingType).toBe('Citation 3 Monitoring Finding Type');
- expect(citation2.findingSource).toBe('Internal Controls');
- expect(citation2.grantNumber).toBe(grant1.number);
-
- // Look for Grant1a.
- const grant1aCitations = citationsToAssert.find((g) => g.grantId === grant1a.id);
- expect(grant1aCitations).toBeDefined();
- expect(grant1aCitations.citations.length).toBe(1);
-
- // Get the first citation ''Grant 1a - Citation 1 - Good'.
- const citation1a = grant1aCitations.citations.find((c) => c.citation === 'Grant 1a - Citation 1 - Good');
- expect(citation1a).toBeDefined();
- expect(citation1a.findingType).toBe('Citation 4 Monitoring Finding Type');
- expect(citation1a.findingSource).toBe('Internal Controls');
- expect(citation1a.grantNumber).toBe(grant1a.number);
-
- // Looks for Grant2.
- const grant2Citations = citationsToAssert.find((g) => g.grantId === grant2.id);
- // Assert we don\'t have any citations for grant 2.
- expect(grant2Citations).toBeUndefined();
-
- // Looks for Grant3.
- const grant3Citations = citationsToAssert.find((g) => g.grantId === grant3.id);
- // Assert we don\'t have any citations for grant 3.
- expect(grant3Citations).toBeUndefined();
+ // get todays date in YYYY-MM-DD for the last possible hour of the day.
+ const reportStartDate = new Date().toISOString().split('T')[0];
+ const citationsToAssert = await getCitationsByGrantIds([grant1.id, grant1a.id, grant2.id, grant3.id], reportStartDate);
+
+ // Assert correct number of citations.
+ expect(citationsToAssert.length).toBe(3);
+
+ // Assert the citations.
+ expect(citationsToAssert[0].citation).toBe('Grant 1 - Citation 1 - Good');
+ expect(citationsToAssert[0].grants.length).toBe(1);
+ expect(citationsToAssert[0].grants[0].findingId).toBeDefined();
+ expect(citationsToAssert[0].grants[0].grantId).toBe(grant1.id);
+ expect(citationsToAssert[0].grants[0].grantNumber).toBe(grant1.number);
+ expect(citationsToAssert[0].grants[0].reviewName).toBeDefined();
+ expect(citationsToAssert[0].grants[0].reportDeliveryDate).toBeDefined();
+ expect(citationsToAssert[0].grants[0].findingType).toBe('Citation 1 Monitoring Finding Type');
+ expect(citationsToAssert[0].grants[0].findingSource).toBe('Internal Controls');
+ expect(citationsToAssert[0].grants[0].monitoringFindingStatusName).toBe('Active');
+
+ expect(citationsToAssert[1].citation).toBe('Grant 1 - Citation 3 - Good 2');
+ expect(citationsToAssert[1].grants.length).toBe(1);
+ expect(citationsToAssert[1].grants[0].findingId).toBeDefined();
+ expect(citationsToAssert[1].grants[0].grantId).toBe(grant1.id);
+ expect(citationsToAssert[1].grants[0].grantNumber).toBe(grant1.number);
+ expect(citationsToAssert[1].grants[0].reviewName).toBeDefined();
+ expect(citationsToAssert[1].grants[0].reportDeliveryDate).toBeDefined();
+ expect(citationsToAssert[1].grants[0].findingType).toBe('Citation 3 Monitoring Finding Type');
+
+ expect(citationsToAssert[2].citation).toBe('Grant 1a - Citation 1 - Good');
+ expect(citationsToAssert[2].grants.length).toBe(1);
+ expect(citationsToAssert[2].grants[0].findingId).toBeDefined();
+ expect(citationsToAssert[2].grants[0].grantId).toBe(grant1a.id);
+ expect(citationsToAssert[2].grants[0].grantNumber).toBe(grant1a.number);
+ expect(citationsToAssert[2].grants[0].reviewName).toBeDefined();
+ expect(citationsToAssert[2].grants[0].reportDeliveryDate).toBeDefined();
+ expect(citationsToAssert[2].grants[0].findingType).toBe('Citation 4 Monitoring Finding Type');
});
});
});
From ab91b39f5dca6078dab71758af38b21525cb5a73 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Mon, 25 Nov 2024 09:31:59 -0500
Subject: [PATCH 028/198] Test fixes and clean up
---
docs/logical_data_model.encoded | 2 +-
docs/logical_data_model.puml | 3 +--
...add-activity-report-objective-citations.js | 4 ++++
src/models/activityReportObjective.js | 7 ------
src/models/activityReportObjectiveCitation.js | 4 ++++
src/models/monitoringStandardLink.js | 10 ---------
.../activityReportObjectiveCitation.test.js | 22 +++++++++----------
7 files changed, 20 insertions(+), 32 deletions(-)
diff --git a/docs/logical_data_model.encoded b/docs/logical_data_model.encoded
index b46490ff53..cfff944431 100644
--- a/docs/logical_data_model.encoded
+++ b/docs/logical_data_model.encoded
@@ -1 +1 @@
-xLrjRzqsilwkNw7h_P1jSBBeo_O2Q-nUhElOQN0Jnx2TBhP5OT0IZmuRYJnBKaxTtlxt2v9w8IbA8YKT9sdroJuKEJDyE2I7Ctpv9-E0yhB8epX-4eDxWj9uJybT3WiMlqTy5mESvJ2D4qhUitWB8O_YcBr3U0yeAE8SOSWVzZ0cbE-8yId90MFnhr4Sn_4tXAQGdfASqFXFNttrtoT_-rnBlzcXv1s6ZCLVHd7ycnJbL9I6B8ehEYS9H_U8Frp11EqHn9pz6iLFOvJ6np7276QGniVntpE88GKSflyJnHa5cByqCroTdPzTdLsSdbt7Ho_4TtRq5vH6CU4xICymH_nXeCIR9XU2LS6dCMWasnCwnE_4oDYKJoWK0f_m-3Zcg82CWsAli1S_ury8Xl5nZCdxFtycyfVxr3__Rr5auoUOd-ptbDn3EWxY_FgL3DA1KcyPf8-3HPp278xbZc9Ck2Z3uSyyYXa7l6JnSGnnMVIXUKho7DmH2ZZvOo7QWySvok5ZuB15EHme9SK8mS6t_dtrVa50VevWdbv1jYUOJG11aF06GcHkAfWHaD_0Ofy33gKGbvGK-y7nprtYC935jCAawbe__3Y4uXLafAJ9em98Avu3fHkn3WEH8YwReXvo54pYF_dpTW2gMwFuhr6CGQ5mb-cHvBIb8h4YwK_jx__txDbGC_Mtv7WB4eeoX44-qd96MyqTER80NiksSP8J1gzBjeSuFILOT8Lcij6_NK702AuddXZYz3cGyv9W4iM8oPh1_E4iHHoAgjmHaaCWYcn1pcIUAyx17ZH4X4AqCvjikUPRCz7T0vhi0Ftpbrz__PccCtMvdaxxvkudA0KwHHnmHB1Fpqsgl8rCuTDLGJ3YX2ASNS4jf10dK5JY9qRmtLBJMLitKj8fWtf3m5jHcnAj5ahU0mGguJxalDz8FSq0M0AcZyqaQ2ISFgCj0SidEBTraUjCSFhKndFnXuDBIkvH2jEUEMwCgcEYwnOPQUeRiaV9XsbdBbWFb5UYVP0Eu6y8-03HhVfSau1CudhO1IkWRuH_n93dYyasH5F3KoNqjv32rcxeDG5OkHT0tGWlJqrAzhU6fzRjHio4wQDWU8D-WcDU_r1tTlhgVq-RpsyPJACOf2aIvXF8BmWNhh94wO9mOM1NWMQyT8-D_aFYo4WuthevOFojml2R1pzNHeZJWy8C4JpOy2B38_NYfx7mj2axjnNdoqZJyCgdMVKni_zPpTpqEg1yUACB3f0dfLZYfzU6BwlMbdRa420GUaNojf-dC4Vta3wyXdn7qeveQKbh0pw52If0_kUlDbzyvbBNTjdVNSiKGcbabifkpm7EIf3PM8Z2ESn0ebJ6bCMfynfNN1PtaBxPNWBAKOBsmlVRDaWKxq461KAm4aEa63bjIVQKP1GKeirSdsaz-7O__AU4Z4ibi4PQuOite67bNO6Oq1shyUG7pg6990SqW_oIx4M32mLti9ScWlC31mxz4r_k4UKbo9zNok3IqcQyrtsXGfN2HtgTWlUeRxXWcs6032u6f7Y7jOLqiQIYZA9HW3sx8GEhF8jWBVyzo55wFeZOTrMMgQDUvJumTVQfw0Txa04Et-3Na6RGS4LmMi83EUera8kYn2YHAl-APBwhpyGgDxYuDhD-HElDZcWUDQVrX3CuHTQ1jaE57KI-570Gwg_Hw-xda1djLBDS9uPRxMcy5vCzu-mDVhlF2N2wvWxR8Sxk8UPgNBn4EU4NGDaFfn1pI6KxUIW9jAWCDBtDi84I6vm7A9WYFagaovJjmZsuXLfwGc0NgTwsyHkez1bHirh4vEnTyTEtLrTd5pUtDozVdrtVdBo-TFK_yXwRYBlDwG1e76ox1j-U97R56iIyXtFF8KSCsTVe5sZMvXryS3H_xP8GClHeY1aX3Xx2HAWXD7wSM4R0uCJLOo-4H40-jGX-Zl2xRq3ohj4l0-tW26q8L1yk2almIRqWlDAzElABbJ8xOXiQems1y91-6pT8-hniWXKwOtlvVEP6RSVYjCx8h4YyHWNA0KNyuGKbvTulF1kk_nQRaQ9MxWEvuk3FILwcC7svlI3y92qGTZJoRmDWZGEr3GrYJxF2byL53UxV0lOIonKaw8abTpnO5bXo4ZpT0PnvZ0SXyydIaVhfSu_jByFm_RNROqxojETBunHI9FgiHVb1kYUwtXW3TzV4uTcZwwAUKrFuK4jJqI5CAsilHTGMIsOWmNiJ1IYDVGDRKNqZGB53k6L3zhKph7O6LhfUfKftdnFNDgPNmt1WT8dDq2SQDRcDmatLcbNev3yLZqyYYEC4PcnYrsVpaSBDhDx9vsgQyIOvH7YaoOS8Rg85zP9e1zXEx_ILf1UcuqHFVarIj4MBDQACDrNnW96Mxbsv-vbBqvLTEQak0OMOXvWH8TlSFoPBkPBaEmJqS9F7ygwx5-wSakAw09Ip7OJyXeFB761zoJEy5OvoTvnzxXOvYVo5Z3kzGlZTiYSM_9ioQyK8dcd378zIlYkupq42YogUETixwoIwaA_sa5vOMsxFvhTgEgODVS1GbPw-258fLGhXxCNYcZZTyAPj5VL-RmBTHJTydZlSWinGmRedplCr0unBa9-NE4KuUqqmttL89Ybo3T2L6hi-jKDh_v0WjHIwtQ7os_KU9WZaYF6p0g1yB9EJ4C8oA2pDYL69zCpMdgSgI3ycKYURKoWzFZzWHeXD1ASaTPPtJ22Q7fDo_q5ymMDk6iCnyplo1ailc7lqS2nZWeR4C4mFj63pSrpvivymUmhRwxAyor5YR3NS0GhJVo4vQ9b7wrxAuD1pm85lu4U5ayw78_4lcqp1e1iHdlWuVlBo-Td5pSlpbsVFdnmV7ybLd2NrhIfpv8-It4Y0jwIDYHcB9WeJGjDJKggzI5KMbknW0RgAhLNMmstfluD18JsASlWk0F8UYr3v2HThG7rVLdmSVruvZXB8M7y6_qQQsa1yVpxOHXRwI_Qx56uHHhgxIWPh8GPtEUpPOB3b-sl8ESAPKSRgmaA3qdA22qwepMl0tZK5zu1teNWDK0wfkIlbJeHKkNiNc3Ppszl2feQzcxa5YfbVhZFi_MrFuv_8dMDBuLEnpKxus6ORQnIoCkVDpa9M9aARjH5HHmnpY05Ee0zidA8iWxJPHjlaqdkbHRFfdO33ZD073V89stQavqPtlVhF2zSAXcsPEv5eZdR9km2kow80547lgTfOyRhcQcrbVlIN8wtCaLeCt2Fu_cNg9l1QEtScyjLKwb97GHECz3ES1kL-wnlUyJoL21YZX1HlQ0Om-iNUrVlrvg_UzXOiYDfOSwqE3dJ861CRmepJCXakGUK8KEy_7RV09KBFWrRhROEU8xcayhOBjoh_kAPKX6jLQSeTDIeBuJYL5ogZeRQYt54x2mvCq1rfLA2gfbQNFmhRK2CfLV6NUXgnVBuVWuuJ1iUNjmcX54fzV0v1-WfXEDz1LYcm1_V6vQWVqfsfoTyY9dvxsOBpLJbS1PLy3mEQ-8XZyTQGgM7vSQYmdxAba4DQ86oUhJRw8mPKdDouRpND4QvTKFdqnbtGBkxgsg4ysyFryZ9A3j4n9YxSFnLDNaG3brOwUv1yD9MxvW2dWAQFPvImYgENqjRYQOfQ5Vx4ZE57rCtVtR1uqF5m7GXdjSsZZXlhCcHTXZXMh0kESlQA-V1Zd5fULDNnzaFDNjH_OjuibUy65V23p_JVF0JhHfrlLFx76BYrrbYmurYwy5ZzvlTZw-ZrMThKwM2rMZh1BUDt17hsWBXeguh31TNsWf2xy3wbfhNtRLeUAKrTISaJec_GL9SMSmwINM7251WQK0res8W16jUzeSEXWyOwUDcHhhCOF_nO5-9qMDHTWBW8RfjLdTDBFhw5W7txfLyloagKYaZk9FtL3p0rZJtjUe2FxMX_-FOeIx2v-vHxwdd8lLLjLU3F_9h3VSYn75oL6MgwAikdTHod0qRtA0xXhgCu8mNbCPnE7hi97kDgEs-Tqg-oLpzlfcpLI4Yn5yEBS-RJhiTz_Ashqsvj_FriOPrmGF5ZEVrrOntzDD8_mFclBgP-fxtypcdffxkcN72wnC7G6O-SXFQ2GnyVquvbJxiCTVBfiosYZ9zM0w9FtfVuKfmkaBjNntDV47uj5Xb-SbHryz9_Jvb5Lc-HHMPlYKLcNu5xZ-JrdS7JnfAs8-kQlD1ApED2l6bNgL-AFHlogaS_zqNO8CuICZZ1Nell7_43CvVgWCubf8nW3FEvnhfDpxX56fgtvOJMi7DGfDh0BXXshjPIjwtEsrqCjrirte-wFw3pz9CQU5pABFktwyEUmYtwsN5NzDQLN7TFzxcdgYrS9_WESRfvkh7MEh2igsVT23NNmmuU97Dy-JS4DFJmsC2xQ1zgQ1iwQ4W7hpmk8RBIn8cE9yV7uZT8nDsvzu0-89pf06IUOoSUkf_lIjtEUqQfiOZfExImxEAsS5-pJFQmkMVPlaz6IliDIH-QxVZrjLtEHEUvn9AY0JpyFGh-ntqAWF2Kq1pDcrBIcb3N3gawDTfUG0xCUmth53y7Ne4Aw5HBjvKXxkdnVl8Xv4hYZ-4n1zyhd7sv8zQUiJLdsHaZZhVfNkQvSpQpZOMs_iekEe3jc4AMKBHtzkg5BSjhACrAxT9HtvEL8MS3MOVBaBdPoNnWFUuohCwQ89orSQK_EYRUrm9FkGjuoMjg9zyzsrOU5TS9eL3UlmgItQAZd70BVM7PiGJaNzhNwbOsqArjhJ4XMUTHXU3sHHoUvD3lAEeXmZ4-jqayZGl8c7hheOdDgTjvwvEyTMImESVRLknMPAs3yHvtCqozSCwwvFSUK0IaEMYUNVSs0mA3GeUS5FldlhVr9OGBspKVFl8dM5I7ez_SGqfH2fVk8sNBOgH8VKhxK1TyteEyXDt8WSxQHsDbeQug1XnX1-qHwBl-AxV3M_AxvZ97jHxuGACT9--WovRZhgM4R_Crbx86u34Em8uKyEh0RoJjc7o2X_OCOMBvzefaQXculAV6xUVAGTO6ZUFZ-EJgwkJVF_nOpoiIl5kPwqZS2o2gS5Bo7Q7IW-ouEQ2kfEiz8gTJQ4Sml71U06M4i3L9HBmQu7jvOGLY72MitaD-AsoAsqAykN9Bgc5CZEVYNgZR0j1tCeEO8-LDnf7sMEgjR7BbL8ZY_vGWH_pX5Z1MKh6tv3O5R7T704thS2785JVj0wS0OK5BRS5YshSweAvNGqRFPKAjg_LxD7DzkNFVjK5CcQlkQDGZtXZt_SdpQbNe0KgxX2IquvFu_kGSvHnIloXyMM3dibZkm1fypUBxarSdzYDc7bupOSxVQZbaMf7V0jP9kdle9BL0Zamv8k_FaarPAjMGIVeEY1P3omZ7yFqj9UzlpPK7oLD7QhNCksKeSxJ4AtCJHnKNfIjCWp3rR2PPeUf2PNbpLeYw40sz-x7YvSRBuVbVeN93jK8q256xGx3QyhpYvOcN_RxTYjO0Mkiyw8_LEhY7uMLxTbUm1i1QSHh4QUrxNZ2jy5Zm33ojlG9a0sGzFR42f00QV2bx1Sa5qWfG1w1rLUA0vVdpLnwGf04LIwsAYGVh0KvlESWwldgHMOBOLAstpkeeqIrAeJmArDrkghuxuvRgnlbRPt1h19-WLybNbN91jW9a0RHEO0vG1fuKNm6g0LgZ0MC8hSlK0N26dz9USUh1QSN8XP7xyUmLe1gWuLUkLvnid8OkXLZHij5QvpaFa5Pm7E2ieDYWz3vulEBJuDF_Fl8AaWka5I0NG2ig7LJwNyzT4MeGMfMd5veL5GkLtgTuLtXQU5PeUg5POR5W2NaEzm-7JCYpjtyuU4RVmbrnCd7gUSLrXSa9a-E9q1Mf0QJSxrcka8r3jghOA5whLlcWv7YOOaMg3wbrh0h2QXsaHe5Q4Uf5oFtUuOgIYLBVOuehJYwSLcNo06cTw0AXMllT64uhGBq0zRnpjyBaXcc5o89eFVyH2D4AZmrF_wjtAq0wG2im5Z1ZRzQjA1aehRj0nn1QaR6HSR-zjO8Zm76-zRL28w7fVUXhnKV3PzKR2vVr-jb-PB9re771iYuQtC0uthDQaQcHMK6nGB5Vo5fHaQYClRcrmkI6QUHPhXKsDsmhH3P40-zxhGWfXBhFWwt826HMwyU5YkCvi1MO6fZ-ryHMZ2oCLcmSsFI_ORPYL6EqWcA1qdr0QyL5uE9-Hsl5Hc5ZlmpsXyObUXAVC4OIfDubYQ98hjFZeo-EucVFVlZJNGwITvjz3X3Uy1rAtc78s3--_F7PCvb5Em8iv6f-RUGpqP4wVUPBpBuz9JwECv6qKN_QBr4qy5QMnKqyoWTJYzslSNo4Z-BEXUBniNWEpapRnA2NMUHu2YU0_zWRJV6nnLjasJZIp6ABIzMuYhq5Qaf4VKyaJ1Qe-vXEHwecowRlGrrsDCD1Aw3kaPuYlYfvGDdgBXqgcIntGBdwLN5SjiTGIhLqdY2SAcX9DasWEX3OaK9dNcTQXAFnhgEBRVrjCMxca3zKqKxknMHmzv5DJDklY_5IW_XOFMMknNXg_MACSPjrCUxzFTrctjUlGtWtEmNZuvZjOO90ZXXlKaMZLZr6IpF5Y3hFRqmqHLJPu-hPN_lBHgQVKr9qoioIe7DhjZ2_hBIDOHu93lDOTgwiaYQVbRWmK_VMn_YuklRV-k1aRhsF4TJCZWT8fDLffcywAMeugKs61MsFHFutQcg4SsvPj0ghMDMABNv5dJl-0rXuFAaW5b4YrqRz6SuyInX7K4eC9deggwHmBhB29vwjN6dfqyqykXthJsTWcNYXdFeqNx5mQtieZvwWSziAycUrDfrFt1cg5SnSwVMAs74dUE0WxRfwlHghhKyRLoAB-gwKNdRbkfT4Q-rf5wSBS1_Xv-8KIjufeGAv_hIfM2x3gBlNlCCk6yo-Y1IwqTNFl3Muvl2iUJ2JU3JVNr5JtiI42gSSoFly5DAm0uoX3CFKGIXOc3uhQjEnfQRHxgYz8cugVIXHkzMLi6OhBpXAEOUbwN1tXbCngJHjtZi21hFHNobeQiBqr4bqvTW5ACILCotPeJoTXeQnO6lCsQf_1LFhxY0HSvj8UOFXHlrtyvuewZ99TkFt9EeuSlsPzbtq9gp8dazTz_0Y_Ctx6Gp-vgSU_lOrrq7h8bVvrewsFjK6jPMyk1YDGrvFstIIMjwptLUL-z_XjGgctmn0LKH1TR1WAWv8gFgPlb2KrS_w6fID-Tb4piogVviYMNEvSRs8Lh-g1WgdQ9z4KJQMaJINDcjfJfUpn2dS8WpJLZrpvRlVeSPB0rzqlaJXrdUllsBJTLjCfM-suKPloaWFj_cJjQunjW9roFnQyGYdRzBUQ3nOPDrk6Z8UMoMFOqvHba4AqsyUDksFoEEYmadJW7r1f34HT2uOstG_DYKVHyJcca6Ne5QMsdoHavRx6huv0ZMQl6q9EW2gT7jv_eDj5jvQi-LfRiBeqI7rSQF3TG0fdDp7k2waeer6iA4UqwSG2manr47yUiP2WlIrX9ErVgdQ8asGjGHzsc0s_m1xkd1nZPnNWYszj6X9RteN70jACZXIkMKP6AZcIZFQHBOiPe759N69m3PEgQA1kz1cIE0SAynFFVrrIIbwS73oDMwbMcgs6XokcuoBIk6UBBLPKgkzTKtGuW4aRAJwHPAco-Gd0puWppRuV65nhJqGLlwe3P-7XznrxoEkqZRDVwqNqk4XNz3LJkp4KN_xB7bwNNKTEQ8bjoSPt5ht-FZsf057kRRtOvAeum7vZJisFvTVwtdLwY_ThtgLz7jSvlgpI4FimUdLcihCR5-TIQjQKSUVCyiLL8y4bnsNa1evxFIeSVaiuO7QT5nHaGRqzAYqykTLXBfXQJQDIq-KGcFaZ59CurNRzpfrPSnFekUE0bI9-hwegBfdO_8WfOJE7BjosGJQPcIofQmFwfFejlXM16jmGoavfw6QAkdED-4Sj6kMDDBxDdK2pTeUASNVJRmZ1LH4edOUNsglEQt-7zvXFf-lStTn7tF7TkBv4lWmdiUl7zjiDffzEmXC7jtT0rDukQp6ba_NQbJdEkQT2Q54xL6olWJJRc-MQOZI9YcOMdCUkUgDrMaLGJ5Ibv9C7YgOA6wE5AxTRGlkpAftzkeRjHRdsRjXooc3CtTKjTzo1mMu-6ViM40mLqFLAXQQhkd7aGDLpZxfBV6JIHkfXlRtICqOInmh4EJh3MLD-apwzg93vs4kDkRnaTlOIxuJMufpmFZRVIfE6eeW5gsCd506BkErPj6rN4Ef-xb655m-3f5G_gwTL8fRToVL5DhsVfwMjl4bdBOhI5mHxyQ8GwaFjG9U0FNUnRXYqRCE3-LYpZQAj3xPXD9hht8H6zefL3ltMPCQJD2UMpVj33k9bcFGRCdE7Z5T5oWq7Ewj3r5zQdxcwPJxYaptXCGa5asHslNuNKE_TZNKAIF7V_5H_9l6at3ku5dFaRKbningz9oXzIVji4wPpsajtQFbpSq-pC4wU_ONOQDlsNUn-2_4M0d_yGHe1yfZ364QSnBVmRZUfdD7-2vE4HNb26PFF6HBl_xhLv__RKWuT-GWyJZ6ksoZBWkvQSAna6Dtyvfr3a5hSxVjovb1s8XTBqgwnn__ByHtfAotYflfdSH5vJ4ymRfJ5otBo4kfdISBf-f3udDiCK0dAc-gDAuM9zyRfrcMZU5LkzbHKcQxZW6yCJztZ4dpkzkISmZUmsuWv_W9pK4XN2SefuvMkLZQL1iYlMaCdjHZZGD0zlBh_iDS8gVJH-0-v3Mrlp3_yrjJMVGSP9exY9Y-zeLig4m_6Z6jWzdRlwSFDfNpFxY9RN5KmtRYzrFfuVxm_5puQm-h6NoTAgWMdcn24VxTM3avAtUbD5qo5TKW-JmhY-hktQbrKqHgJ-nKAgfkKVHFYDEooAF_1m00
\ No newline at end of file
+xLrjRzqsblwkNo5uFhGDRWVJThl06hEBSHmd3JPn3DlfO5eK1YtHzxAHo3iavTJjzhylIEg5f2Y9b7ITfEKd-rBaEOSF3yd3S_Zo3yO1vLLP96dwMGhk2ShJFIMt1InP-XxnNGhmje1vcb7odgLt4F8aJTaxXFU0WZ8j48RyOGzJGlcE69-o0Z2M_fQaQPe-9JI7z9GKXAP_-UQR_ppvhxzfwTSxbBt3aB7qwoHDVvUYd8hIa2LfJUSau-WUyOTBcA4zWfYptvBqIOhoz3X5cCCjfEbn-lSjn9023lF_IT8j1PY_D3DSdZq_kpmxFpsxIezUYE_iwAyeJrB2Tv2UOOxumqY9Dsqk1Ek2JrBGIhOdT8pVYP6nBPzHA0G-uUbnob496GVbNc4lVgQ_4WpJunIJzvz_8V9N-zm__-z9OkCdc9_izvJSGpeDulBwLGpIaL9l6QIFWqMQmm9EvKvIJBWem-4lF4aP1xnYwN4ASLKEeNbAYWBS4Weu-NCXEe37SrJ0ny3X3XLmf9GK8mG7t_eFrVi50SLp18ly4h8zmMm628A4Dn0YSrl1BG75DIpt1U1G2d51IRaV7V_S9Gu3MKfhJEgkzUDFOoXUGaOgcZqgWBJcELAw5Uim42dYiYcQ8KV19FwJFsz6eBenYl-Mf1YK2dUPdaXETrGarYJzgVtz_xxySgoPXbjokWK9HLk4GJ7HSaPRBHqvjW6yrctZh20CNbLi3t5-Ih3e2kraet-xX80Ht4myASJvCo3d9S4aIH6JDOF5mrcEE1HLkIMaW40KkGC5atakEGTxq18H2j7DRBBbsc_jH7SFQBO3zFEll_ts2qrdcd83dVRDxSzG2dIAEE28uF2UsvBG6qOsG4so5JryLYi9HfnGXBUNy0vIY3Cewlep8_XshLclWu1KTCxOxmHWL9_R4fsoIjuZ16hXFgHvq4ezpqvO0wQFpN9e9Pm_yQq1ooSu1VQHwys3-Wp82_67WmjAxb4Eyu6fS8ggOpxi5vbewckoHzc7QMSkM8-KLw9za0xWRo_u0D6l-bBBWSpYUjXrBQ1lX1l5aEUhsQP6MyDJ9VHtaCBEl1Yq55Yr5q1T2sTGGqhsjvQdrkqch8dves1uWtw2OzxmK7Ts-kf_JzlFRndCanJaEHBc4oZE2HSUjKpfWl1qO5U1ThnqZut-G-B4I3ZTkWLW_At2yEc3dySZn3T2uHOHFDhm8iCZzU8dgN38AJkt4-VhHDFmslLPzp6p_rdDtFOwe5nza-iIK6ILy0JaLuRBgz6MRkGG811wIOgkd-UmGFUGFho6V4VodcXvHHi3FeSPAa7n-JURhxvmgMld7UWlUKgXD39BRRSN06yhiBMn48KBk0MPKXbJ5gVEQrnnLTv2-kRk0b2EChGNBc9zER87MoWGWhKO8iN8QKkofsHBGIdQpFMPDeDl_yE_AyYuL09hf1KkUmCQLjsMY0dTiHfFVk0ScaXoGBUGNv2zQ72Xu0xsqaHmVk30eN_YopsYl0B5irePN5hQZlUworIeKlX8hrFm7lKDJsqx393HnO0Kxx2sa2wML9H9D4gmPzTaP9Ld4MpalmS5of-F8lPzdMNkg5Tx6KoTVIBwmHvi0OTly6j8jz3mM71QmGFvypMGoxB2A9Cg_ufiVLUVcLLkT75jPls8rfizqJngJfi9PtIARE9nXufEY7mvu23KNwFNtSyXCzcf5hbE3E_RatGl9dl6sHlyTbyGuFJW7RP3dDr3pDMuU84Ami-1iXzE8UQGodRoK19eKHbfUfjX2oKsE8zGC4Lyb82OATkvUt0BjVI4m2vIWstZDr3fCw9cjOZ9sRlYfsylBy_Ehs-kNxu-kxe-UNtXwd_aFJOHTvlJ0T0usFODlZt9x8grY7aFvvv3ZXYohz2lqApDE_ZWQFx69I5a6D0GCv0SF8I9Y4Hec3cpZ83HYMl0NWW9b7ngWVu2uNTFGVQkrIy3xU10RWXK7ovOI_19io2yrBqwyekLiZYm6nhA3O7ma7uRDqZwl6o25JfZU_bybWPtfyAvpW2kIBn29Ie0HVpX1INLtY-A6wx_5fk9ebRk0xdYu2zPKUKmVtbtJlX9NYBiQEJVHy2Q1kekDDOzpmfV5nSrk7y7s4iiLv1W8EJSys1POCb9y7G7yDPZF0YPJzOEwC-VUsn_6ORlhzmUSrBa0rzQfv0YCMGjoW_IFT7tnXYukoOEpnxT5FMSciA7MfgB3c5QMNifehPOCWC9tdijG6Ni6xWJrJS253k4EpPatpZ36cTWhUjrgdJtEN7jOdeD30T6bzWCVQ9Hiziet5HjM8LE_dFn-2I2E4vWfYPsVJQVADpEwhryhgQPRf93X4VQV8ZWDbhGB8bsWEtqJbzBUcuwJlJaroH7TsYrHfJnfZeUCD7IWojtFtDflh8h9zK5e13p43CYP3VynsGfrv9yZu0URl9OtlMtp3sdfBmg0ULi1o6_vk2Y05XVyeplcAFS7MTVkfDE8dykuqwl47wtxi46_wRSTc84pxHXZaSftoNS5o23PHtFNEoTTH9ToLTxI2-iBRTdyrirdTA68M2eIizV1AcKAeNmziNgcZZVyAPT5VL-RmBTHpTydZlSWinGmQOdBlCr0umhK3olS8xmzZN1VDSZcUN8DK3NQkpwr0sjmqE2r5BeTeVARrTxc25G8CRFIe2AgKnEGWZBeB0q9oObqZDRUvxg8FsKIBriJg7q-Fo86I4s4PoIt5dUCO5eUaog_GVn1K-vQGp7p1_86oozOExHmx6C2HeImp0zq8BDpt7bptx6x2bihulgja2PiTLm1oZC_udae6MUm7ifWqFF00U-WHyLJde-7eb_sMOA1js8yAJ7wMSldvsTNxzy_lBisMV7nqTo5MT9VQFA7FcZ93SIu3lInSICHHE56Q7vAQdKNgGw2yjsi0HznTOgw-6szD_1e92UH3by5m1vZqMe_8IBDQ2-BoizJlzMEuuooDXm1lz6cjj0VFzyj8qizDViTpcyGnZfxYePh8KOt1PmO8F17TzVGSuHtXfbh2uiD2YlPhZWXDgy1knTMt0FU1-Arm0La9gxKcv5I9MxTu5fCxUzBMnksBkTMw2O-UKwmzxVzYJzcTmwjXG-4TliX8zjiR568QiAtkuiP5Kcj5aR4NN4C8SOu0pwmCOfscuXRRVHjhdqNcdHR7idu71Bw8C6-OnjEz9pexjU_UU5AmN3NLKuaMYFTiah0wwgiWGKGU-fsbZnkkPgRML-zDSZhSoHMWpS8_Z-PUucy5exToRoDLIQKaT14upqCvm6vNxh6zxnFBm86AE4b6_e5c3qgxshzzlDVtZRMx0aQcFDbJeuq2DZJ6m8CqwhRBm4bID0i_zqsWAN23qBMgrt3VeSoIMLjrruf_9FQqfDk56LjjnHgx8GJbDveJ8QQYlA7NMpuCW4tPD61QeQQatvex0DDP9I7NzffX7Bv_inw3XXS7fvcn5AeTJ7Pn3ahn26onrajG9xS6zTZlfPteMQ_1rYuhUNBRXSbSDLKCdtCAA9ZpWMtnaojIm_3XNsKxOSSaXBXCsprKP_n82AanltdQOvmhKhAf_Uk2lQnNrTEvHdUxnUNaR98Jh6p8NxfwB9gsZWsafd3vAFfd9N7E0SqFpnZ19MS_HIsaOyJL7hmf-OCVo8klc76nOFEXx6WN1Hsppgk16N8N4PubYnhZZAsIlcmuznQdbHLyVR3xLuKV-9URDKlcjKmWy-qpzz2DQD9j-a_8ynS6kjiMB7iNJXiVhD7yVMqUkpjgdJmofQEi4jutS4Ul60k6ghYiC5zVQ2aBlmFgMcjVTjMXufJLr9nJEYRz1KTnjp3h9TQS8K61fG3MZOY04Qrx-Xmw63nZfutP6kjnW__5WNudHOrLs0k0XkMnT6BkWb7zz2mBxzqgyNRIkKYqZkBFtL3p2rZJtlUe2FxMX_-FOeIx2b-vG7wdd8lLLTLU0l_8h3VSYn75oN6MkwEikdTHod1qR_A0xXhgCu4mNbCPnU7hi97kDgEs-Tqc-yLp-lvspLo4Yv5yEBS-RJhiTz_Ashysvj_EDiOPrnGF5ZEVrrRHtzDD8_mFclFgP-fxtypcdffxlcN72wni7G1M-XXFQ2GnyVquvbpxiCTVRfipj46J-j1qIVl2_nXJXT87UlJkS-9_nAB3ByvAZhvgN_dZABhDuYYypU4elClW8F7iaREeEdZILTHzOrUQMLcCU5UFEkKhyGUpRaLOz-xekmGPnbP7I2lHBVF-9tQ2xK0PqBI1d161PBZNMRdd6BD3HlomcjOU-WIBs1NJ3iNQsbRbjTjhiOtjsfyNxI_G6Tfv_KmEDIPjc_NH_s5DwXNnntHT-vaflkwJxtLBU5wmJV8KutppVMEWUMTVMi6w7sUjZHWsIEB_-2W8OU7XjvFkr33MrZ1mtfw9N75KHMcZZHyTdniVWDCd5txZsW3mXlTG2sdcCd7hkVxrBTpdj6gR686JgqiExYjdHViqpsiBrdsRvFHahxbEGFxNRyvjekPwBpN695qGWU_Xu5_-C-9K1uJcYE5cqfQKseQuTKdLfjBo47vhs6JOgV0wz49NIgfTlA4FVqU3_v478bySVmc8Fl5Sw-t17hpjYQi-o24SVRTA-AmZcRsSP2Ltzb5nt0TimXooZQEtlrofRb5PHcfNRfgE_9of2pWQp7PSpSx6K-i1xt6TPdJH3UTd6bFpictjS2pxaBUCrhQcVVFTjL7fLN6Q5GthyAijsgevnm2trbsR4455_Rr-hMDj2DRQqn8LdlKONWzaKSdXJGxoZg8S8nFhT9F4bBo9Xwww6DpQdRUPkJl4rai3t7srNiHcHjWl4UTpFClN3EkkJt7b04f3bedbttDWC2WqA7d1I7vxwtpIM4Azir7pxo9rZKXwFVt4DAKGxNxYDLnsAYI7DA-r0NVDw3eeBzo87EsaTZPQ6-AWOSOGVT4UZRuIktmrlo-_OoHxKU-42Z7IVleCkMuwwbX6_pjPUo1k0n3S2EbF3om6yaxPXyWeSE365Y-VQARDKoS7rFZTlFLO1j5pHEZzEJoykJV_tuKpElIF1kPAuZSI-0gC99nNU4IW-ou-Q0ffAkzuYSJg4TmV3SUG6K4SBM917nQe3lvOKLY76KiMOF-OsmAMvAyURA3Qc7CJ6UYtkXRaf0tyaCOewKDnadscEfDxB9bLCXIVzTWGBnXnl0M4N5tfAR7B3TBG4qRyA5e53Sj0-S0eG5BRK5YsdVce2wNWqPFPS9ZQusxz4izUKkVTS6CMMkkgDHZtXZt0ydp-fLeGSetHAIq9vFueUJvwZWaFf5uQiIEvV5SWVMw6iMFvoyEh4VCVFmcWbp_aRB8TFAuWQmNjBjGo-f0t9YoX5vVv5igagfXapITq1q65f6E8RlRoXvVsEkF4YHErBhPTuDGrj7IxmoDt7KSL4QOHg5QM0pIOIg3vNbpOaGzI4QU_SZukN6o-7vZr2veOeGZKBKxH2ijpn7l3pnij-tYsYDe1NdGN-iuk0UXfVjsOrWZO0LSHh5AVOzBvWYF1Oy8qzhHu1iWAod9o902g3Hbx8ZI2wG8a0zWEAgn07BisUlF2390ogMMnKJ3zO3dDzmiEtvwaLc2M9RjS4wgwD4DoY7yYXGVxkf-kwCMweRvs-SmQq9V89U9OygvO8i0cG152Sm1oWZJuf7W1g0OZHW2g6uJ1q1P_mfxOZJ04uH8nT6xiUp8q0DGEElN4USR9p6BeM8QDbenNdE0wG8ZWES28eDok23nn6V5f_Xzv4ZI2wG8a0kW28gFLI6Ny-D2BK8HUKPX2QYOg4o-1HlXAU39q8p24s4nO8bv3dSFnqo8S_U_kdWcNmBD-9auZJmYck4oGcJuudGYDG8KkxtB2Daqv155Pl4AsgL3qgEXoaYKaD8nR4g2AiuI4q25H4QHCZztc54oKHfw375Y9CBfwXC7Y35YIuGgjZlfd2A00q2DF3ENOJ93LE4o88edly8X2Z2OyCp_EfT14W7A09Z0JFrPbr4oK0fxWATGYWYOoFZVdihWYF0SRxr5Q6Hq0m-z1NnqV29UcFX4g-V7KPc6mSQ5onnqU06nl6ULKIcGIQ2Oe5Ylv0LKP6eJBsv5M5ompJnB5U4pGrRXCY6QEHzhuYGGgZ_F5X4HY0JhHyNAexdm28m3J33hub5Z2oC8ZOER8RVC8iODHX5K2nG-WzeHLm4Bkvtj28s2XlxPR0_DIx9P-a9OoH3wbsI94daEZqz-kee_VBB7_zoMu3itMPV08GtV8UoTnWozjzV__JbbpABTWHOoDN-siXdeXDr-ioNc7rxIdgSRaNIHltflYJ9o5jP5ZVnA1_CBNQ_fUaHFafx5ugdn-8vE3Djau91PedZAPm0_bDlDAN75c-HPUD8CukjBbNhAlOMg2sH33sHC5kW_cCwFQdQB9kX3qtPoGm7hO2wHtgE-Aha0MLhkdIePBBS0UKQLiLfsHv3ATNIUOPmgg0bsJo1ca1WJmgTUPDf5Ox6kuukjistrRYPG_vOHNkvLvF1tKSsAUs-hiPB3U9JzPIvLkEfzOifnbtMrRWFztIR-L2_3k7TxXHCZrEsXna21MAyJHMDMlSOBSqK8-azlp7J555bZ-jdV-ql6vd-d9AoLsQH1BrRiuNvPQLj3FD8S5Z6idTbbJJvhCILcRgtFgR7zRN_rWSdS-jzZA1cTZn08grECttJIr73IMCoh6XxPFA_ILCZd7F7ebDOnQfHRF4hwTtn7y32uqa5iOWIkJRgptZcHS8uWb9YCD5JMIs5Sv4LFl7SoPMMFjqiezkp_dG6beaNocb8ynKBjxQ7yXW8EdUj87_MQzFbn9sXMi7CbTwiY1rtWWSEqgwhr-knslgqSIMoPEXAwNLNhdj9jD6URd6w0lS9VYvEeUIT4YgGcq-RckKwZFfxpNlijgFPLv52Dxg6cNS6rnAU3Gzc4a-M-oiQofkKKE6K0yMGdq8QTe0njCKmTn0A5cR8YjhqRAbvj7jghuYRIXzAr6vr9QpPoYkEKivXQJxSlU5KJ2fjcm-EWC5iz5SAsfemFNKMNJbs0GfnxKpBTgZ8vw5XBDXQSxRkNy7K-Zj8nDmcKjvWk57_NRydYhfCajtuFKbwZbn_faCNlGahSk1Jtpsy2d_pVWQ3l_bfWBzzpNNGkiXLlhEZhK-DGMqbhov6wv3NqpQzf8xtOlTLxVvtUAs2wRS3K1rHK1qic8e34gf-vYyKPVMpkGPbOtusaJEpwj_cIDPURjmiejMlke72ATedKPIDPIGjfSqwclFbh70Exn26UQUUXVBTxr3hfO4lUb-ISEjxq5-nwRijvj8tst2ZDsMYXzlyoLhN65k1EcH-hNc4qpTfRpGUBB5kDmqPZwsInocdQBk55EPF7pRTZyJZei9Pqu1zGQGn4VGk6DlqFxOb7qV4zff1bw1MbiPyaPEM-ng-9GArcdpN4NG0LEls-ln7sooyjHRBqzw4CQ13wkD6X-i0KZYvJt5TIKLRZM1ZFMP88LOIOgo3-FMCbGGPQWadQ_rIjLMQODiHz6c3nVm3xEdInJLnNWcszjAY8xqPN72iACdeIEMMPMIWMYhDP1FPifW5LfV49GBREALAHkv2Mo21SwmmFmtqrpMb6C31oTUubMgjsMfqk6uphYg5UxBKPacjzjOrGQi7aB2HwXT9cY-JdmpuW3oxuFEvnRNnG5Zvaovy7XvorxsFk4xRjFzrNaY7Xtn5Lpkn4oVzxOla6NJLzUH8rjoUP73htEFJsvD67ENQtezPeau7v3VksFnIVftdLQk_TB_gLTBlSLdgpo8jiGUdLsikCxDzTIQTQYKzVyuYLr0p4bpqN49gvB3JazRbiuO7QjDpHIKQqFEZqigVLnFgXwNPF2u3KGgDaJDACOzNOz_hr9KnFukUEmfG9-hxefBwdetPWvGIUklObSjcq3OZanrbVL2VHRV3joROW1jAoJaDDLH9Vxu9vw1TiwIHtdUfCsWszaWb_cxI7IcX8f9qy_XQUyrf_V_u3lFbV9sxYuUOExOJBvV0X_4oViFORXUZERI3mEJHtpqmXvVBQkpuzAr6TQvZsPaGIjGU9UjBCEtkPfcMA6sJWgcrvv7htbYVLX4K4tKfoUoXWedYvaZXsjssuiwiPcUllb5hTPw36xQSCZXp9tNxhNTGW8j_feu517CrLArceUcQVXmvKENiazuIFvcqaMfeVq_KZB76iG91ljbGrgOlf6ylEdGUvc9Z6H-vZRtYEx6bk02ClztdugIX26BHsiW9PU0oNXiMVSirbBgVQqJ1vUCG2UNlsacbkFLtKisXRU_dwMWRNy9fkuB4DV5-Z3YK-bWhu1LG7roCAnO3w_1GBkPieqZhaqqelUug5x6XdKArSv-rgS44xRbrqysmisWv1isExE8HqtM1GilXtlgGDAVc5vvEkQ_9z4z8p6BH5cbSZz-tz1jNGvCoSVmN7wM_Qpm9vmwUpHpPM6BChCR06Lf_qWxhbFcSrj8zMjxSxCCShBTdVnKs-fRz0x7mmcrTbgQLn49EjYr3RWb1Q_-qxIvPGDW83wzAUi3b_K_4TwHgTcERwRt4LUKndAswtnIjoyXBgPs7z-RgG-PpCZ109sg7fRHVLYS_60fPbutXkBhUKL9jTpqRSsD-xjbhvdUtiUGGV83TICxfGv2ZGhWEASuThGgnUgZt77hJ63qCnzerjkpbLyFdfKHEvpBSVOvgRdzX_aUavh8OE2kqT10ntTahcIbR_VJ1l0vcxx2UFzZK_lNW9hR5KGtR2v7FfOU78-1BuM-Sc6PySwwWMWImzVRxTsFbHwmzWj9qsQh00IcMMLbKzw3EhPiYqZWxvr9HTKkIVq2SLsMH_3y0
\ No newline at end of file
diff --git a/docs/logical_data_model.puml b/docs/logical_data_model.puml
index 0e0e158738..e56801df94 100644
--- a/docs/logical_data_model.puml
+++ b/docs/logical_data_model.puml
@@ -88,6 +88,7 @@ class ActivityReportGoals{
class ActivityReportObjectiveCitations{
* id : integer :
+ * activityReportObjectiveId : integer
* citation : text
* createdAt : timestamp with time zone : now()
* monitoringReferences : jsonb
@@ -2644,8 +2645,6 @@ ActivityReportCollaborators "n" }--[#black,dotted,thickness=2]--{ "n" Roles : ro
ActivityReportGoals "n" }--[#black,dotted,thickness=2]--{ "n" Resources : resources, activityReportGoals
ActivityReportObjectives "n" }--[#black,dotted,thickness=2]--{ "n" Courses : courses, reportObjectives
ActivityReportObjectives "n" }--[#black,dotted,thickness=2]--{ "n" Files : files, reportObjectives
-!issue='associations need to be defined both directions'
-ActivityReportObjectives "n" }--[#d54309,dotted,thickness=2]--{ "n" MonitoringStandards : citations
ActivityReportObjectives "n" }--[#black,dotted,thickness=2]--{ "n" Resources : resources, activityReportObjectives
ActivityReportObjectives "n" }--[#black,dotted,thickness=2]--{ "n" Topics : topics, activityReportObjectives
ActivityReports "n" }--[#black,dotted,thickness=2]--{ "n" Files : files, reports
diff --git a/src/migrations/20241118093025-add-activity-report-objective-citations.js b/src/migrations/20241118093025-add-activity-report-objective-citations.js
index b995131b8e..85f205d118 100644
--- a/src/migrations/20241118093025-add-activity-report-objective-citations.js
+++ b/src/migrations/20241118093025-add-activity-report-objective-citations.js
@@ -11,6 +11,10 @@ module.exports = {
autoIncrement: true,
primaryKey: true,
},
+ activityReportObjectiveId: {
+ type: Sequelize.INTEGER,
+ allowNull: false,
+ },
citation: {
type: Sequelize.TEXT,
allowNull: false,
diff --git a/src/models/activityReportObjective.js b/src/models/activityReportObjective.js
index 4f7eabbcf1..812c8d8e03 100644
--- a/src/models/activityReportObjective.js
+++ b/src/models/activityReportObjective.js
@@ -31,13 +31,6 @@ export default (sequelize, DataTypes) => {
as: 'topics',
});
- ActivityReportObjective.belongsToMany(models.MonitoringStandard, {
- through: models.ActivityReportObjectiveCitation,
- foreignKey: 'activityReportObjectiveId',
- otherKey: 'citationId',
- as: 'citations',
- });
-
ActivityReportObjective.belongsToMany(models.Resource, {
through: models.ActivityReportObjectiveResource,
foreignKey: 'activityReportObjectiveId',
diff --git a/src/models/activityReportObjectiveCitation.js b/src/models/activityReportObjectiveCitation.js
index 6e36537764..850d9226e7 100644
--- a/src/models/activityReportObjectiveCitation.js
+++ b/src/models/activityReportObjectiveCitation.js
@@ -26,6 +26,10 @@ export default (sequelize, DataTypes) => {
primaryKey: true,
type: DataTypes.INTEGER,
},
+ activityReportObjectiveId: {
+ type: DataTypes.INTEGER,
+ allowNull: false,
+ },
citation: {
type: DataTypes.TEXT,
allowNull: false,
diff --git a/src/models/monitoringStandardLink.js b/src/models/monitoringStandardLink.js
index 23ce44e150..927b445e97 100644
--- a/src/models/monitoringStandardLink.js
+++ b/src/models/monitoringStandardLink.js
@@ -10,16 +10,6 @@ import { Model } from 'sequelize';
export default (sequelize, DataTypes) => {
class MonitoringStandardLink extends Model {
static associate(models) {
- // eslint-disable-next-line max-len
- // MonitoringStandardLink.hasMany(models.ActivityReportObjectiveCitation, { foreignKey: 'citationId', as: 'activityReportObjectiveCitations' });
- /*
- MonitoringStandardLink.belongsToMany(models.ActivityReportObjective, {
- through: models.ActivityReportObjectiveCitation,
- foreignKey: 'citationId',
- otherKey: 'activityReportObjectiveId',
- as: 'activityReportObjectives',
- });
- */
}
}
MonitoringStandardLink.init({
diff --git a/src/models/tests/activityReportObjectiveCitation.test.js b/src/models/tests/activityReportObjectiveCitation.test.js
index d23836939b..3162455087 100644
--- a/src/models/tests/activityReportObjectiveCitation.test.js
+++ b/src/models/tests/activityReportObjectiveCitation.test.js
@@ -323,16 +323,14 @@ describe('activityReportObjectiveCitation', () => {
// Create aro citations.
const activityReportObjectiveCitation1 = await ActivityReportObjectiveCitation.create({
activityReportObjectiveId: activityReportObjective.id,
- reviewId: monitoringReviewForLink.id,
- findingId: monitoringFindingForLink.id,
- citationId: monitoringStandardForLink.id,
+ citation: 'Sample Citation 1',
+ monitoringReferences: { standardId: monitoringStandardForLink.standardId },
}, { individualHooks: true });
const activityReportObjectiveCitation2 = await ActivityReportObjectiveCitation.create({
activityReportObjectiveId: activityReportObjective.id,
- reviewId: monitoringReviewForLink.id,
- findingId: monitoringFindingForLink.id,
- citationId: citation2.id,
+ citation: 'Sample Citation 2',
+ monitoringReferences: { standardId: monitoringStandardForLink.standardId },
}, { individualHooks: true });
// Assert citations.
@@ -347,15 +345,15 @@ describe('activityReportObjectiveCitation', () => {
activityReportObjectiveCitationLookUp = activityReportObjectiveCitationLookUp.map((c) => c.get({ plain: true }));
// Citation 1.
- const citation1LookUp = activityReportObjectiveCitationLookUp.find((c) => c.citationId === citation1.id);
+ const citation1LookUp = activityReportObjectiveCitationLookUp.find((c) => c.citation === 'Sample Citation 1');
expect(citation1LookUp).toBeDefined();
- expect(citation1LookUp.reviewId).toBe(monitoringReviewForLink.id);
- expect(citation1LookUp.findingId).toBe(monitoringFindingForLink.id);
+ expect(citation1LookUp.activityReportObjectiveId).toBe(activityReportObjective.id);
+ expect(citation1LookUp.monitoringReferences.standardId).toBe(monitoringStandardForLink.standardId);
// Citation 2.
- const citation2LookUp = activityReportObjectiveCitationLookUp.find((c) => c.citationId === citation2.id);
+ const citation2LookUp = activityReportObjectiveCitationLookUp.find((c) => c.citation === 'Sample Citation 2');
expect(citation2LookUp).toBeDefined();
- expect(citation2LookUp.reviewId).toBe(monitoringReviewForLink.id);
- expect(citation2LookUp.findingId).toBe(monitoringFindingForLink.id);
+ expect(citation2LookUp.activityReportObjectiveId).toBe(activityReportObjective.id);
+ expect(citation2LookUp.monitoringReferences.standardId).toBe(monitoringStandardForLink.standardId);
});
});
From 97964ca1936fa3d00d4c378c3678649cacc66a67 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Mon, 25 Nov 2024 09:58:43 -0500
Subject: [PATCH 029/198] Fix test order issue
---
src/services/citations.test.js | 63 +++++++++++++++++++---------------
1 file changed, 35 insertions(+), 28 deletions(-)
diff --git a/src/services/citations.test.js b/src/services/citations.test.js
index f13520f14c..f4d0ec29ef 100644
--- a/src/services/citations.test.js
+++ b/src/services/citations.test.js
@@ -317,34 +317,41 @@ describe('citations service', () => {
expect(citationsToAssert.length).toBe(3);
// Assert the citations.
- expect(citationsToAssert[0].citation).toBe('Grant 1 - Citation 1 - Good');
- expect(citationsToAssert[0].grants.length).toBe(1);
- expect(citationsToAssert[0].grants[0].findingId).toBeDefined();
- expect(citationsToAssert[0].grants[0].grantId).toBe(grant1.id);
- expect(citationsToAssert[0].grants[0].grantNumber).toBe(grant1.number);
- expect(citationsToAssert[0].grants[0].reviewName).toBeDefined();
- expect(citationsToAssert[0].grants[0].reportDeliveryDate).toBeDefined();
- expect(citationsToAssert[0].grants[0].findingType).toBe('Citation 1 Monitoring Finding Type');
- expect(citationsToAssert[0].grants[0].findingSource).toBe('Internal Controls');
- expect(citationsToAssert[0].grants[0].monitoringFindingStatusName).toBe('Active');
-
- expect(citationsToAssert[1].citation).toBe('Grant 1 - Citation 3 - Good 2');
- expect(citationsToAssert[1].grants.length).toBe(1);
- expect(citationsToAssert[1].grants[0].findingId).toBeDefined();
- expect(citationsToAssert[1].grants[0].grantId).toBe(grant1.id);
- expect(citationsToAssert[1].grants[0].grantNumber).toBe(grant1.number);
- expect(citationsToAssert[1].grants[0].reviewName).toBeDefined();
- expect(citationsToAssert[1].grants[0].reportDeliveryDate).toBeDefined();
- expect(citationsToAssert[1].grants[0].findingType).toBe('Citation 3 Monitoring Finding Type');
-
- expect(citationsToAssert[2].citation).toBe('Grant 1a - Citation 1 - Good');
- expect(citationsToAssert[2].grants.length).toBe(1);
- expect(citationsToAssert[2].grants[0].findingId).toBeDefined();
- expect(citationsToAssert[2].grants[0].grantId).toBe(grant1a.id);
- expect(citationsToAssert[2].grants[0].grantNumber).toBe(grant1a.number);
- expect(citationsToAssert[2].grants[0].reviewName).toBeDefined();
- expect(citationsToAssert[2].grants[0].reportDeliveryDate).toBeDefined();
- expect(citationsToAssert[2].grants[0].findingType).toBe('Citation 4 Monitoring Finding Type');
+ // Get the citation with the text 'Grant 1 - Citation 1 - Good'.
+ const citation1 = citationsToAssert.find((c) => c.citation === 'Grant 1 - Citation 1 - Good');
+ expect(citation1).toBeDefined();
+ expect(citation1.grants.length).toBe(1);
+ expect(citation1.grants[0].findingId).toBeDefined();
+ expect(citation1.grants[0].grantId).toBe(grant1.id);
+ expect(citation1.grants[0].grantNumber).toBe(grant1.number);
+ expect(citation1.grants[0].reviewName).toBeDefined();
+ expect(citation1.grants[0].reportDeliveryDate).toBeDefined();
+ expect(citation1.grants[0].findingType).toBe('Citation 1 Monitoring Finding Type');
+ expect(citation1.grants[0].findingSource).toBe('Internal Controls');
+ expect(citation1.grants[0].monitoringFindingStatusName).toBe('Active');
+
+ // Get the citation with the text 'Grant 1 - Citation 3 - Good 2'.
+ const citation2 = citationsToAssert.find((c) => c.citation === 'Grant 1 - Citation 3 - Good 2');
+
+ expect(citation2).toBeDefined();
+ expect(citation2.grants.length).toBe(1);
+ expect(citation2.grants[0].findingId).toBeDefined();
+ expect(citation2.grants[0].grantId).toBe(grant1.id);
+ expect(citation2.grants[0].grantNumber).toBe(grant1.number);
+ expect(citation2.grants[0].reviewName).toBeDefined();
+ expect(citation2.grants[0].reportDeliveryDate).toBeDefined();
+ expect(citation2.grants[0].findingType).toBe('Citation 3 Monitoring Finding Type');
+
+ // Get the citation with the text 'Grant 1a - Citation 1 - Good'.
+ const citation3 = citationsToAssert.find((c) => c.citation === 'Grant 1a - Citation 1 - Good');
+ expect(citation3).toBeDefined();
+ expect(citation3.grants.length).toBe(1);
+ expect(citation3.grants[0].findingId).toBeDefined();
+ expect(citation3.grants[0].grantId).toBe(grant1a.id);
+ expect(citation3.grants[0].grantNumber).toBe(grant1a.number);
+ expect(citation3.grants[0].reviewName).toBeDefined();
+ expect(citation3.grants[0].reportDeliveryDate).toBeDefined();
+ expect(citation3.grants[0].findingType).toBe('Citation 4 Monitoring Finding Type');
});
});
});
From 25190c854d70507ba6eb1c80c4db4f60167d425e Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Mon, 25 Nov 2024 14:03:18 -0500
Subject: [PATCH 030/198] cleanup tests from old path and add crud for ARO
citation
---
.../activityReportObjectiveCitation.test.js | 176 +--------------
src/services/reportCache.js | 69 ++++++
src/services/reportCache.test.js | 208 ++++++++++++++++++
3 files changed, 287 insertions(+), 166 deletions(-)
diff --git a/src/models/tests/activityReportObjectiveCitation.test.js b/src/models/tests/activityReportObjectiveCitation.test.js
index 3162455087..ee1603bfca 100644
--- a/src/models/tests/activityReportObjectiveCitation.test.js
+++ b/src/models/tests/activityReportObjectiveCitation.test.js
@@ -2,7 +2,6 @@
/* eslint-disable prefer-destructuring */
import { REPORT_STATUSES } from '@ttahub/common';
import { faker } from '@faker-js/faker';
-import { v4 as uuidv4 } from 'uuid';
import db, {
User,
Recipient,
@@ -13,13 +12,6 @@ import db, {
ActivityReportGoal,
ActivityReportObjective,
ActivityReportObjectiveCitation,
- MonitoringReviewGrantee,
- MonitoringReview,
- MonitoringReviewStatus,
- MonitoringFindingHistory,
- MonitoringFinding,
- MonitoringFindingGrant,
- MonitoringStandard,
} from '..';
import { captureSnapshot, rollbackToSnapshot } from '../../lib/programmaticTransaction';
@@ -97,15 +89,6 @@ describe('activityReportObjectiveCitation', () => {
let objective;
let activityReportObjective;
- // Create Citations.
- let citation1;
- let citation2;
- let citation3;
-
- // To assign to citations.
- const monitoringReviewId = uuidv4();
- const monitoringFindingId = uuidv4();
-
beforeAll(async () => {
// Create a snapshot of the database.
snapShot = await captureSnapshot();
@@ -153,7 +136,7 @@ describe('activityReportObjectiveCitation', () => {
report = await ActivityReport.create(sampleReport);
// Create activity report goal.
- const activityReportGoal = await ActivityReportGoal.create({
+ await ActivityReportGoal.create({
activityReportId: report.id,
goalId: goal.id,
isActivelyEdited: false,
@@ -166,128 +149,6 @@ describe('activityReportObjectiveCitation', () => {
ttaProvided: 'ipd aro Goal',
status: objective.status,
});
-
- // Create monitoring data.
- const monitoringGranteeId = uuidv4();
-
- await MonitoringReviewGrantee.create({
- id: faker.datatype.number({ min: 9999 }),
- grantNumber: grantNumberToUse,
- reviewId: monitoringReviewId,
- granteeId: monitoringGranteeId,
- createTime: new Date(),
- updateTime: new Date(),
- updateBy: 'Support Team',
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- }, { individualHooks: true });
-
- const monitoringStatusId = 1;
- const monitoringContentId = uuidv4();
-
- await MonitoringReview.create({
- reviewId: monitoringReviewId,
- contentId: monitoringContentId,
- statusId: monitoringStatusId,
- name: faker.random.words(3),
- startDate: new Date(),
- endDate: new Date(),
- reviewType: 'AIAN-DEF',
- reportDeliveryDate: new Date(),
- reportAttachmentId: faker.datatype.uuid(),
- outcome: faker.random.words(5),
- hash: faker.datatype.uuid(),
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- }, { individualHooks: true });
-
- await MonitoringReviewStatus.create({
- statusId: monitoringStatusId,
- name: 'Complete',
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- }, { individualHooks: true });
-
- await MonitoringFindingHistory.create({
- reviewId: monitoringReviewId,
- findingHistoryId: uuidv4(),
- findingId: monitoringFindingId,
- statusId: monitoringStatusId,
- narrative: faker.random.words(10),
- ordinal: faker.datatype.number({ min: 1, max: 10 }),
- determination: faker.random.words(5),
- hash: faker.datatype.uuid(),
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- sourceDeletedAt: null,
- }, { individualHooks: true });
-
- await MonitoringFinding.create({
- findingId: monitoringFindingId,
- statusId: monitoringStatusId,
- findingType: faker.random.word(),
- hash: faker.datatype.uuid(),
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- }, { individualHooks: true });
-
- await MonitoringFindingGrant.create({
- // 1
- findingId: monitoringFindingId,
- granteeId: monitoringGranteeId,
- statusId: monitoringStatusId,
- findingType: faker.random.word(),
- source: faker.random.word(),
- correctionDeadLine: new Date(),
- reportedDate: new Date(),
- closedDate: null,
- hash: faker.datatype.uuid(),
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- sourceDeletedAt: null,
- }, { individualHooks: true });
-
- // These are the actual citations.
- const citations = await MonitoringStandard.bulkCreate([
- {
- standardId: faker.datatype.number({ min: 1 }),
- contentId: monitoringContentId,
- citation: 'citation 1',
- text: 'This is the text for citation 1',
- guidance: faker.lorem.paragraph(),
- citable: faker.datatype.number({ min: 1, max: 10 }),
- hash: faker.datatype.uuid(),
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- {
- standardId: faker.datatype.number({ min: 1 }),
- contentId: monitoringContentId,
- citation: 'citation 2',
- text: 'This is the text for citation 2',
- guidance: faker.lorem.paragraph(),
- citable: faker.datatype.number({ min: 1, max: 10 }),
- hash: faker.datatype.uuid(),
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- {
- standardId: faker.datatype.number({ min: 1 }),
- contentId: monitoringContentId,
- citation: 'citation 3',
- text: 'This is the text for citation 3',
- guidance: faker.lorem.paragraph(),
- citable: faker.datatype.number({ min: 1, max: 10 }),
- hash: faker.datatype.uuid(),
- sourceCreatedAt: new Date(),
- sourceUpdatedAt: new Date(),
- },
- ], { individualHooks: true });
-
- // populate citations from citations into the citations.
- citation1 = citations[0];
- citation2 = citations[1];
- citation3 = citations[2];
});
afterAll(async () => {
@@ -298,39 +159,18 @@ describe('activityReportObjectiveCitation', () => {
await db.sequelize.close();
});
- it('aro citation', async () => {
- // Get the monitoring review object where the citation will be assigned.
- const monitoringReviewForLink = await MonitoringReview.findOne({
- where: {
- reviewId: monitoringReviewId,
- },
- });
-
- // Get the monitoring finding object where the citation will be assigned.
- const monitoringFindingForLink = await MonitoringFinding.findOne({
- where: {
- findingId: monitoringFindingId,
- },
- });
-
- // Get the monitoring standard object where the citation will be assigned.
- const monitoringStandardForLink = await MonitoringStandard.findOne({
- where: {
- citation: 'citation 1',
- },
- });
-
+ it('create aro citation', async () => {
// Create aro citations.
const activityReportObjectiveCitation1 = await ActivityReportObjectiveCitation.create({
activityReportObjectiveId: activityReportObjective.id,
citation: 'Sample Citation 1',
- monitoringReferences: { standardId: monitoringStandardForLink.standardId },
+ monitoringReferences: { grantId: grant.id, findingId: 1, reviewName: 'Review Name 1' },
}, { individualHooks: true });
const activityReportObjectiveCitation2 = await ActivityReportObjectiveCitation.create({
activityReportObjectiveId: activityReportObjective.id,
citation: 'Sample Citation 2',
- monitoringReferences: { standardId: monitoringStandardForLink.standardId },
+ monitoringReferences: { grantId: grant.id, findingId: 2, reviewName: 'Review Name 2' },
}, { individualHooks: true });
// Assert citations.
@@ -348,12 +188,16 @@ describe('activityReportObjectiveCitation', () => {
const citation1LookUp = activityReportObjectiveCitationLookUp.find((c) => c.citation === 'Sample Citation 1');
expect(citation1LookUp).toBeDefined();
expect(citation1LookUp.activityReportObjectiveId).toBe(activityReportObjective.id);
- expect(citation1LookUp.monitoringReferences.standardId).toBe(monitoringStandardForLink.standardId);
+ expect(citation1LookUp.monitoringReferences.grantId).toBe(grant.id);
+ expect(citation1LookUp.monitoringReferences.findingId).toBe(1);
+ expect(citation1LookUp.monitoringReferences.reviewName).toBe('Review Name 1');
// Citation 2.
const citation2LookUp = activityReportObjectiveCitationLookUp.find((c) => c.citation === 'Sample Citation 2');
expect(citation2LookUp).toBeDefined();
expect(citation2LookUp.activityReportObjectiveId).toBe(activityReportObjective.id);
- expect(citation2LookUp.monitoringReferences.standardId).toBe(monitoringStandardForLink.standardId);
+ expect(citation2LookUp.monitoringReferences.grantId).toBe(grant.id);
+ expect(citation2LookUp.monitoringReferences.findingId).toBe(2);
+ expect(citation2LookUp.monitoringReferences.reviewName).toBe('Review Name 2');
});
});
diff --git a/src/services/reportCache.js b/src/services/reportCache.js
index 9ee76f22fd..e500936fdf 100644
--- a/src/services/reportCache.js
+++ b/src/services/reportCache.js
@@ -12,6 +12,7 @@ const {
ActivityReportObjectiveCourse,
ActivityReportObjectiveResource,
ActivityReportObjectiveTopic,
+ ActivityReportObjectiveCitation,
Goal,
GoalFieldResponse,
GoalTemplateFieldPrompt,
@@ -138,6 +139,74 @@ const cacheTopics = async (objectiveId, activityReportObjectiveId, topics = [])
]);
};
+/*
+ - ActivityReportObjectiveCitation -
+ Each row in this table is per grant.
+ Each row has a json column called 'monitoringReferences', this is an array of objects.
+ Each object is unique by a combination of grantId, findingId, and reviewName (for the same grant).
+ To avoid complex lookups, we will simply UPDATE (by id) existing and CREATE new citations.
+ Citations to remove will be determined by id.
+*/
+export const cacheCitations = async (objectiveId, activityReportObjectiveId, citations = []) => {
+ // Get current report citation ids for the activity report objective.
+ const currentReportCitationIds = citations.map((citation) => citation.id);
+
+ // Get all existing citations for the activity report objective from DB.
+ const existingDBCitations = await ActivityReportObjectiveCitation.findAll({
+ where: { activityReportObjectiveId },
+ raw: true,
+ });
+
+ // Get all existing citation ids from DB.
+ const existingDBCitationIds = existingDBCitations.map((citation) => citation.id) || [];
+
+ // Get citations to remove.
+ const removedCitationIds = existingDBCitationIds.filter(
+ (citationId) => !currentReportCitationIds.includes(citationId),
+ );
+
+ // Get all citations that have an DB id.
+ const citationsToUpdate = citations.filter((citation) => citation.id);
+
+ // Get all citations that do not have a DB id defined.
+ const newCitations = citations.filter((citation) => !citation.id);
+
+ // Do all sql operations in a promise.
+ return Promise.all([
+ // Update existing citations.
+ citationsToUpdate.length > 0
+ ? Promise.all(
+ citationsToUpdate.map(async (citation) => ActivityReportObjectiveCitation.update({
+ citation: citation.citation,
+ monitoringReferences: citation.monitoringReferences,
+ }, {
+ where: { id: citation.id },
+ })),
+ )
+ : Promise.resolve(),
+ // Create new citations.
+ newCitations.length > 0
+ ? Promise.all(
+ newCitations.map(async (citation) => ActivityReportObjectiveCitation.create({
+ activityReportObjectiveId,
+ citation: citation.citation,
+ monitoringReferences: citation.monitoringReferences,
+ })),
+ )
+ : Promise.resolve(),
+ removedCitationIds.length > 0
+ ? ActivityReportObjectiveCitation.destroy({
+ where: {
+ activityReportObjectiveId,
+ id: { [Op.in]: removedCitationIds },
+ },
+ individualHooks: true,
+ hookMetadata: { objectiveId },
+ })
+ : Promise.resolve(),
+ ]);
+};
+
const cacheObjectiveMetadata = async (objective, reportId, metadata) => {
const {
files,
diff --git a/src/services/reportCache.test.js b/src/services/reportCache.test.js
index 7409a2da9f..e8762e730a 100644
--- a/src/services/reportCache.test.js
+++ b/src/services/reportCache.test.js
@@ -18,6 +18,7 @@ import db, {
ActivityReportObjectiveResource,
ActivityReportObjectiveTopic,
ActivityReportObjectiveCourse,
+ ActivityReportObjectiveCitation,
ActivityReportGoalFieldResponse,
GoalTemplateFieldPrompt,
CollaboratorRole,
@@ -27,6 +28,7 @@ import db, {
import {
cacheGoalMetadata,
cacheCourses,
+ cacheCitations,
} from './reportCache';
import {
createReport,
@@ -116,6 +118,212 @@ describe('cacheCourses', () => {
});
});
+describe('activityReportObjectiveCitation', () => {
+ let activityReportObjectiveCitation1;
+ let activityReportObjectiveCitation2;
+ let activityReport;
+ let grant;
+ let recipient;
+ let goal;
+ let objective;
+ let aro;
+
+ beforeAll(async () => {
+ recipient = await createRecipient({});
+ grant = await createGrant({ recipientId: recipient.id });
+
+ activityReport = await createReport({
+ activityRecipients: [
+ {
+ grantId: grant.id,
+ },
+ ],
+ });
+
+ goal = await createGoal({ grantId: grant.id, status: GOAL_STATUS.IN_PROGRESS });
+
+ objective = await Objective.create({
+ goalId: goal.id,
+ title: faker.datatype.string(200),
+ status: 'Not Started',
+ });
+
+ aro = await ActivityReportObjective.create({
+ objectiveId: objective.id,
+ activityReportId: activityReport.id,
+ });
+ });
+
+ afterAll(async () => {
+ await ActivityReportObjectiveCitation.destroy({
+ where: {
+ id: [activityReportObjectiveCitation1.id, activityReportObjectiveCitation2.id],
+ },
+ });
+
+ await ActivityReportObjective.destroy({ where: { objectiveId: objective.id } });
+ await Objective.destroy({ where: { id: objective.id }, force: true });
+ await Goal.destroy({ where: { id: goal.id }, force: true });
+ await destroyReport(activityReport);
+ await Grant.destroy({ where: { id: grant.id }, individualHooks: true });
+ await Recipient.destroy({ where: { id: recipient.id } });
+ });
+
+ it('should create, read, update, and delete', async () => {
+ // Create a new ActivityReportObjectiveCitation.
+ const citationsToCreate = [
+ {
+ citation: 'Citation 1',
+ monitoringReferences: [{
+ grantId: 1,
+ findingId: 1,
+ reviewName: 'Review 1',
+ }],
+ },
+ ];
+
+ // Save the ActivityReportObjectiveCitation.
+ let result = await cacheCitations(objective.id, aro.id, citationsToCreate);
+
+ // Assert updated.
+ expect(result[0]).toBeUndefined();
+
+ // Assert created.
+ expect(result[1]).toBeDefined();
+ const createdAroCitations = await ActivityReportObjectiveCitation.findAll({
+ where: {
+ activityReportObjectiveId: aro.id,
+ },
+ });
+
+ const citation1Id = createdAroCitations[0].id;
+
+ expect(createdAroCitations).toHaveLength(1);
+ expect(createdAroCitations[0].citation).toEqual('Citation 1');
+ expect(createdAroCitations[0].monitoringReferences).toEqual([{
+ grantId: 1,
+ findingId: 1,
+ reviewName: 'Review 1',
+ }]);
+
+ // Assert deleted.
+ expect(result[2]).toBeUndefined();
+
+ // Update the ActivityReportObjectiveCitation.
+ const citationsToUpdate = [
+ {
+ id: createdAroCitations[0].id,
+ citation: 'Citation 1 UPDATED',
+ monitoringReferences: [{
+ grantId: 1,
+ findingId: 1,
+ reviewName: 'Review 1',
+ },
+ {
+ grantId: 2,
+ findingId: 2,
+ reviewName: 'Review 2',
+ }],
+ },
+ {
+ citation: 'Citation 2',
+ monitoringReferences: [{
+ grantId: 3,
+ findingId: 3,
+ reviewName: 'Review 3',
+ }],
+ },
+ ];
+
+ // Save updated ActivityReportObjectiveCitation.
+ result = await cacheCitations(objective.id, aro.id, citationsToUpdate);
+
+ // Assert updated.
+ expect(result[0]).toHaveLength(1);
+
+ // Assert created.
+ expect(result[1]).toHaveLength(1);
+
+ // Assert deleted.
+ expect(result[2]).toBeUndefined();
+
+ const updatedAroCitations = await ActivityReportObjectiveCitation.findAll({
+ where: {
+ activityReportObjectiveId: aro.id,
+ },
+ });
+
+ expect(updatedAroCitations).toHaveLength(2);
+ expect(updatedAroCitations[0].citation).toEqual('Citation 1 UPDATED');
+ expect(updatedAroCitations[0].monitoringReferences).toEqual([{
+ grantId: 1,
+ findingId: 1,
+ reviewName: 'Review 1',
+ },
+ {
+ grantId: 2,
+ findingId: 2,
+ reviewName: 'Review 2',
+ }]);
+ const secondCitationId = updatedAroCitations[1].id;
+ expect(updatedAroCitations[1].citation).toEqual('Citation 2');
+ expect(updatedAroCitations[1].monitoringReferences).toEqual([{
+ grantId: 3,
+ findingId: 3,
+ reviewName: 'Review 3',
+ }]);
+
+ // Delete the ActivityReportObjectiveCitation.
+ const citationsToDelete = [
+ {
+ id: secondCitationId,
+ citation: 'Citation 2',
+ monitoringReferences: [{
+ grantId: 4,
+ findingId: 4,
+ reviewName: 'Review 4',
+ }],
+ },
+ ];
+
+ // Save deleted ActivityReportObjectiveCitation.
+ result = await cacheCitations(objective.id, aro.id, citationsToDelete);
+
+ // Assert updated.
+ expect(result[0]).toBeDefined();
+
+ // Assert created.
+ expect(result[1]).toBeUndefined();
+
+ // Assert deleted.
+ expect(result[2]).toBeDefined();
+
+ // Retrieve deleted citation 1.
+ const deletedCitation = await ActivityReportObjectiveCitation.findOne({
+ where: {
+ id: citation1Id,
+ },
+ });
+
+ expect(deletedCitation).toBeNull();
+
+ // Retrieve citation 2.
+ const remainingCitation = await ActivityReportObjectiveCitation.findOne({
+ where: {
+ id: secondCitationId,
+ },
+ });
+
+ expect(remainingCitation).toBeDefined();
+ expect(remainingCitation.citation).toEqual('Citation 2');
+ expect(remainingCitation.monitoringReferences).toEqual([{
+ grantId: 4,
+ findingId: 4,
+ reviewName: 'Review 4',
+ }]);
+ });
+});
+
describe('cacheGoalMetadata', () => {
let activityReport;
let goal;
From 91d9ff11e0a2cdd7eff9f6e59eacbafa0b573cdc Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Mon, 25 Nov 2024 16:43:39 -0500
Subject: [PATCH 031/198] hook up saving of aro citations add skip and tests
for monitoring goals
---
docs/logical_data_model.encoded | 2 +-
docs/logical_data_model.puml | 22 ++++++--
src/goalServices/getGoalsForReport.ts | 7 +++
src/goalServices/goals.js | 40 ++++++++++++++-
src/goalServices/goals.test.js | 73 ++++++++++++++++++++++++++-
src/services/objectives.ts | 3 +-
src/services/reportCache.js | 2 +
7 files changed, 142 insertions(+), 7 deletions(-)
diff --git a/docs/logical_data_model.encoded b/docs/logical_data_model.encoded
index cfff944431..41322e5ecb 100644
--- a/docs/logical_data_model.encoded
+++ b/docs/logical_data_model.encoded
@@ -1 +1 @@
-xLrjRzqsblwkNo5uFhGDRWVJThl06hEBSHmd3JPn3DlfO5eK1YtHzxAHo3iavTJjzhylIEg5f2Y9b7ITfEKd-rBaEOSF3yd3S_Zo3yO1vLLP96dwMGhk2ShJFIMt1InP-XxnNGhmje1vcb7odgLt4F8aJTaxXFU0WZ8j48RyOGzJGlcE69-o0Z2M_fQaQPe-9JI7z9GKXAP_-UQR_ppvhxzfwTSxbBt3aB7qwoHDVvUYd8hIa2LfJUSau-WUyOTBcA4zWfYptvBqIOhoz3X5cCCjfEbn-lSjn9023lF_IT8j1PY_D3DSdZq_kpmxFpsxIezUYE_iwAyeJrB2Tv2UOOxumqY9Dsqk1Ek2JrBGIhOdT8pVYP6nBPzHA0G-uUbnob496GVbNc4lVgQ_4WpJunIJzvz_8V9N-zm__-z9OkCdc9_izvJSGpeDulBwLGpIaL9l6QIFWqMQmm9EvKvIJBWem-4lF4aP1xnYwN4ASLKEeNbAYWBS4Weu-NCXEe37SrJ0ny3X3XLmf9GK8mG7t_eFrVi50SLp18ly4h8zmMm628A4Dn0YSrl1BG75DIpt1U1G2d51IRaV7V_S9Gu3MKfhJEgkzUDFOoXUGaOgcZqgWBJcELAw5Uim42dYiYcQ8KV19FwJFsz6eBenYl-Mf1YK2dUPdaXETrGarYJzgVtz_xxySgoPXbjokWK9HLk4GJ7HSaPRBHqvjW6yrctZh20CNbLi3t5-Ih3e2kraet-xX80Ht4myASJvCo3d9S4aIH6JDOF5mrcEE1HLkIMaW40KkGC5atakEGTxq18H2j7DRBBbsc_jH7SFQBO3zFEll_ts2qrdcd83dVRDxSzG2dIAEE28uF2UsvBG6qOsG4so5JryLYi9HfnGXBUNy0vIY3Cewlep8_XshLclWu1KTCxOxmHWL9_R4fsoIjuZ16hXFgHvq4ezpqvO0wQFpN9e9Pm_yQq1ooSu1VQHwys3-Wp82_67WmjAxb4Eyu6fS8ggOpxi5vbewckoHzc7QMSkM8-KLw9za0xWRo_u0D6l-bBBWSpYUjXrBQ1lX1l5aEUhsQP6MyDJ9VHtaCBEl1Yq55Yr5q1T2sTGGqhsjvQdrkqch8dves1uWtw2OzxmK7Ts-kf_JzlFRndCanJaEHBc4oZE2HSUjKpfWl1qO5U1ThnqZut-G-B4I3ZTkWLW_At2yEc3dySZn3T2uHOHFDhm8iCZzU8dgN38AJkt4-VhHDFmslLPzp6p_rdDtFOwe5nza-iIK6ILy0JaLuRBgz6MRkGG811wIOgkd-UmGFUGFho6V4VodcXvHHi3FeSPAa7n-JURhxvmgMld7UWlUKgXD39BRRSN06yhiBMn48KBk0MPKXbJ5gVEQrnnLTv2-kRk0b2EChGNBc9zER87MoWGWhKO8iN8QKkofsHBGIdQpFMPDeDl_yE_AyYuL09hf1KkUmCQLjsMY0dTiHfFVk0ScaXoGBUGNv2zQ72Xu0xsqaHmVk30eN_YopsYl0B5irePN5hQZlUworIeKlX8hrFm7lKDJsqx393HnO0Kxx2sa2wML9H9D4gmPzTaP9Ld4MpalmS5of-F8lPzdMNkg5Tx6KoTVIBwmHvi0OTly6j8jz3mM71QmGFvypMGoxB2A9Cg_ufiVLUVcLLkT75jPls8rfizqJngJfi9PtIARE9nXufEY7mvu23KNwFNtSyXCzcf5hbE3E_RatGl9dl6sHlyTbyGuFJW7RP3dDr3pDMuU84Ami-1iXzE8UQGodRoK19eKHbfUfjX2oKsE8zGC4Lyb82OATkvUt0BjVI4m2vIWstZDr3fCw9cjOZ9sRlYfsylBy_Ehs-kNxu-kxe-UNtXwd_aFJOHTvlJ0T0usFODlZt9x8grY7aFvvv3ZXYohz2lqApDE_ZWQFx69I5a6D0GCv0SF8I9Y4Hec3cpZ83HYMl0NWW9b7ngWVu2uNTFGVQkrIy3xU10RWXK7ovOI_19io2yrBqwyekLiZYm6nhA3O7ma7uRDqZwl6o25JfZU_bybWPtfyAvpW2kIBn29Ie0HVpX1INLtY-A6wx_5fk9ebRk0xdYu2zPKUKmVtbtJlX9NYBiQEJVHy2Q1kekDDOzpmfV5nSrk7y7s4iiLv1W8EJSys1POCb9y7G7yDPZF0YPJzOEwC-VUsn_6ORlhzmUSrBa0rzQfv0YCMGjoW_IFT7tnXYukoOEpnxT5FMSciA7MfgB3c5QMNifehPOCWC9tdijG6Ni6xWJrJS253k4EpPatpZ36cTWhUjrgdJtEN7jOdeD30T6bzWCVQ9Hiziet5HjM8LE_dFn-2I2E4vWfYPsVJQVADpEwhryhgQPRf93X4VQV8ZWDbhGB8bsWEtqJbzBUcuwJlJaroH7TsYrHfJnfZeUCD7IWojtFtDflh8h9zK5e13p43CYP3VynsGfrv9yZu0URl9OtlMtp3sdfBmg0ULi1o6_vk2Y05XVyeplcAFS7MTVkfDE8dykuqwl47wtxi46_wRSTc84pxHXZaSftoNS5o23PHtFNEoTTH9ToLTxI2-iBRTdyrirdTA68M2eIizV1AcKAeNmziNgcZZVyAPT5VL-RmBTHpTydZlSWinGmQOdBlCr0umhK3olS8xmzZN1VDSZcUN8DK3NQkpwr0sjmqE2r5BeTeVARrTxc25G8CRFIe2AgKnEGWZBeB0q9oObqZDRUvxg8FsKIBriJg7q-Fo86I4s4PoIt5dUCO5eUaog_GVn1K-vQGp7p1_86oozOExHmx6C2HeImp0zq8BDpt7bptx6x2bihulgja2PiTLm1oZC_udae6MUm7ifWqFF00U-WHyLJde-7eb_sMOA1js8yAJ7wMSldvsTNxzy_lBisMV7nqTo5MT9VQFA7FcZ93SIu3lInSICHHE56Q7vAQdKNgGw2yjsi0HznTOgw-6szD_1e92UH3by5m1vZqMe_8IBDQ2-BoizJlzMEuuooDXm1lz6cjj0VFzyj8qizDViTpcyGnZfxYePh8KOt1PmO8F17TzVGSuHtXfbh2uiD2YlPhZWXDgy1knTMt0FU1-Arm0La9gxKcv5I9MxTu5fCxUzBMnksBkTMw2O-UKwmzxVzYJzcTmwjXG-4TliX8zjiR568QiAtkuiP5Kcj5aR4NN4C8SOu0pwmCOfscuXRRVHjhdqNcdHR7idu71Bw8C6-OnjEz9pexjU_UU5AmN3NLKuaMYFTiah0wwgiWGKGU-fsbZnkkPgRML-zDSZhSoHMWpS8_Z-PUucy5exToRoDLIQKaT14upqCvm6vNxh6zxnFBm86AE4b6_e5c3qgxshzzlDVtZRMx0aQcFDbJeuq2DZJ6m8CqwhRBm4bID0i_zqsWAN23qBMgrt3VeSoIMLjrruf_9FQqfDk56LjjnHgx8GJbDveJ8QQYlA7NMpuCW4tPD61QeQQatvex0DDP9I7NzffX7Bv_inw3XXS7fvcn5AeTJ7Pn3ahn26onrajG9xS6zTZlfPteMQ_1rYuhUNBRXSbSDLKCdtCAA9ZpWMtnaojIm_3XNsKxOSSaXBXCsprKP_n82AanltdQOvmhKhAf_Uk2lQnNrTEvHdUxnUNaR98Jh6p8NxfwB9gsZWsafd3vAFfd9N7E0SqFpnZ19MS_HIsaOyJL7hmf-OCVo8klc76nOFEXx6WN1Hsppgk16N8N4PubYnhZZAsIlcmuznQdbHLyVR3xLuKV-9URDKlcjKmWy-qpzz2DQD9j-a_8ynS6kjiMB7iNJXiVhD7yVMqUkpjgdJmofQEi4jutS4Ul60k6ghYiC5zVQ2aBlmFgMcjVTjMXufJLr9nJEYRz1KTnjp3h9TQS8K61fG3MZOY04Qrx-Xmw63nZfutP6kjnW__5WNudHOrLs0k0XkMnT6BkWb7zz2mBxzqgyNRIkKYqZkBFtL3p2rZJtlUe2FxMX_-FOeIx2b-vG7wdd8lLLTLU0l_8h3VSYn75oN6MkwEikdTHod1qR_A0xXhgCu4mNbCPnU7hi97kDgEs-Tqc-yLp-lvspLo4Yv5yEBS-RJhiTz_Ashysvj_EDiOPrnGF5ZEVrrRHtzDD8_mFclFgP-fxtypcdffxlcN72wni7G1M-XXFQ2GnyVquvbpxiCTVRfipj46J-j1qIVl2_nXJXT87UlJkS-9_nAB3ByvAZhvgN_dZABhDuYYypU4elClW8F7iaREeEdZILTHzOrUQMLcCU5UFEkKhyGUpRaLOz-xekmGPnbP7I2lHBVF-9tQ2xK0PqBI1d161PBZNMRdd6BD3HlomcjOU-WIBs1NJ3iNQsbRbjTjhiOtjsfyNxI_G6Tfv_KmEDIPjc_NH_s5DwXNnntHT-vaflkwJxtLBU5wmJV8KutppVMEWUMTVMi6w7sUjZHWsIEB_-2W8OU7XjvFkr33MrZ1mtfw9N75KHMcZZHyTdniVWDCd5txZsW3mXlTG2sdcCd7hkVxrBTpdj6gR686JgqiExYjdHViqpsiBrdsRvFHahxbEGFxNRyvjekPwBpN695qGWU_Xu5_-C-9K1uJcYE5cqfQKseQuTKdLfjBo47vhs6JOgV0wz49NIgfTlA4FVqU3_v478bySVmc8Fl5Sw-t17hpjYQi-o24SVRTA-AmZcRsSP2Ltzb5nt0TimXooZQEtlrofRb5PHcfNRfgE_9of2pWQp7PSpSx6K-i1xt6TPdJH3UTd6bFpictjS2pxaBUCrhQcVVFTjL7fLN6Q5GthyAijsgevnm2trbsR4455_Rr-hMDj2DRQqn8LdlKONWzaKSdXJGxoZg8S8nFhT9F4bBo9Xwww6DpQdRUPkJl4rai3t7srNiHcHjWl4UTpFClN3EkkJt7b04f3bedbttDWC2WqA7d1I7vxwtpIM4Azir7pxo9rZKXwFVt4DAKGxNxYDLnsAYI7DA-r0NVDw3eeBzo87EsaTZPQ6-AWOSOGVT4UZRuIktmrlo-_OoHxKU-42Z7IVleCkMuwwbX6_pjPUo1k0n3S2EbF3om6yaxPXyWeSE365Y-VQARDKoS7rFZTlFLO1j5pHEZzEJoykJV_tuKpElIF1kPAuZSI-0gC99nNU4IW-ou-Q0ffAkzuYSJg4TmV3SUG6K4SBM917nQe3lvOKLY76KiMOF-OsmAMvAyURA3Qc7CJ6UYtkXRaf0tyaCOewKDnadscEfDxB9bLCXIVzTWGBnXnl0M4N5tfAR7B3TBG4qRyA5e53Sj0-S0eG5BRK5YsdVce2wNWqPFPS9ZQusxz4izUKkVTS6CMMkkgDHZtXZt0ydp-fLeGSetHAIq9vFueUJvwZWaFf5uQiIEvV5SWVMw6iMFvoyEh4VCVFmcWbp_aRB8TFAuWQmNjBjGo-f0t9YoX5vVv5igagfXapITq1q65f6E8RlRoXvVsEkF4YHErBhPTuDGrj7IxmoDt7KSL4QOHg5QM0pIOIg3vNbpOaGzI4QU_SZukN6o-7vZr2veOeGZKBKxH2ijpn7l3pnij-tYsYDe1NdGN-iuk0UXfVjsOrWZO0LSHh5AVOzBvWYF1Oy8qzhHu1iWAod9o902g3Hbx8ZI2wG8a0zWEAgn07BisUlF2390ogMMnKJ3zO3dDzmiEtvwaLc2M9RjS4wgwD4DoY7yYXGVxkf-kwCMweRvs-SmQq9V89U9OygvO8i0cG152Sm1oWZJuf7W1g0OZHW2g6uJ1q1P_mfxOZJ04uH8nT6xiUp8q0DGEElN4USR9p6BeM8QDbenNdE0wG8ZWES28eDok23nn6V5f_Xzv4ZI2wG8a0kW28gFLI6Ny-D2BK8HUKPX2QYOg4o-1HlXAU39q8p24s4nO8bv3dSFnqo8S_U_kdWcNmBD-9auZJmYck4oGcJuudGYDG8KkxtB2Daqv155Pl4AsgL3qgEXoaYKaD8nR4g2AiuI4q25H4QHCZztc54oKHfw375Y9CBfwXC7Y35YIuGgjZlfd2A00q2DF3ENOJ93LE4o88edly8X2Z2OyCp_EfT14W7A09Z0JFrPbr4oK0fxWATGYWYOoFZVdihWYF0SRxr5Q6Hq0m-z1NnqV29UcFX4g-V7KPc6mSQ5onnqU06nl6ULKIcGIQ2Oe5Ylv0LKP6eJBsv5M5ompJnB5U4pGrRXCY6QEHzhuYGGgZ_F5X4HY0JhHyNAexdm28m3J33hub5Z2oC8ZOER8RVC8iODHX5K2nG-WzeHLm4Bkvtj28s2XlxPR0_DIx9P-a9OoH3wbsI94daEZqz-kee_VBB7_zoMu3itMPV08GtV8UoTnWozjzV__JbbpABTWHOoDN-siXdeXDr-ioNc7rxIdgSRaNIHltflYJ9o5jP5ZVnA1_CBNQ_fUaHFafx5ugdn-8vE3Djau91PedZAPm0_bDlDAN75c-HPUD8CukjBbNhAlOMg2sH33sHC5kW_cCwFQdQB9kX3qtPoGm7hO2wHtgE-Aha0MLhkdIePBBS0UKQLiLfsHv3ATNIUOPmgg0bsJo1ca1WJmgTUPDf5Ox6kuukjistrRYPG_vOHNkvLvF1tKSsAUs-hiPB3U9JzPIvLkEfzOifnbtMrRWFztIR-L2_3k7TxXHCZrEsXna21MAyJHMDMlSOBSqK8-azlp7J555bZ-jdV-ql6vd-d9AoLsQH1BrRiuNvPQLj3FD8S5Z6idTbbJJvhCILcRgtFgR7zRN_rWSdS-jzZA1cTZn08grECttJIr73IMCoh6XxPFA_ILCZd7F7ebDOnQfHRF4hwTtn7y32uqa5iOWIkJRgptZcHS8uWb9YCD5JMIs5Sv4LFl7SoPMMFjqiezkp_dG6beaNocb8ynKBjxQ7yXW8EdUj87_MQzFbn9sXMi7CbTwiY1rtWWSEqgwhr-knslgqSIMoPEXAwNLNhdj9jD6URd6w0lS9VYvEeUIT4YgGcq-RckKwZFfxpNlijgFPLv52Dxg6cNS6rnAU3Gzc4a-M-oiQofkKKE6K0yMGdq8QTe0njCKmTn0A5cR8YjhqRAbvj7jghuYRIXzAr6vr9QpPoYkEKivXQJxSlU5KJ2fjcm-EWC5iz5SAsfemFNKMNJbs0GfnxKpBTgZ8vw5XBDXQSxRkNy7K-Zj8nDmcKjvWk57_NRydYhfCajtuFKbwZbn_faCNlGahSk1Jtpsy2d_pVWQ3l_bfWBzzpNNGkiXLlhEZhK-DGMqbhov6wv3NqpQzf8xtOlTLxVvtUAs2wRS3K1rHK1qic8e34gf-vYyKPVMpkGPbOtusaJEpwj_cIDPURjmiejMlke72ATedKPIDPIGjfSqwclFbh70Exn26UQUUXVBTxr3hfO4lUb-ISEjxq5-nwRijvj8tst2ZDsMYXzlyoLhN65k1EcH-hNc4qpTfRpGUBB5kDmqPZwsInocdQBk55EPF7pRTZyJZei9Pqu1zGQGn4VGk6DlqFxOb7qV4zff1bw1MbiPyaPEM-ng-9GArcdpN4NG0LEls-ln7sooyjHRBqzw4CQ13wkD6X-i0KZYvJt5TIKLRZM1ZFMP88LOIOgo3-FMCbGGPQWadQ_rIjLMQODiHz6c3nVm3xEdInJLnNWcszjAY8xqPN72iACdeIEMMPMIWMYhDP1FPifW5LfV49GBREALAHkv2Mo21SwmmFmtqrpMb6C31oTUubMgjsMfqk6uphYg5UxBKPacjzjOrGQi7aB2HwXT9cY-JdmpuW3oxuFEvnRNnG5Zvaovy7XvorxsFk4xRjFzrNaY7Xtn5Lpkn4oVzxOla6NJLzUH8rjoUP73htEFJsvD67ENQtezPeau7v3VksFnIVftdLQk_TB_gLTBlSLdgpo8jiGUdLsikCxDzTIQTQYKzVyuYLr0p4bpqN49gvB3JazRbiuO7QjDpHIKQqFEZqigVLnFgXwNPF2u3KGgDaJDACOzNOz_hr9KnFukUEmfG9-hxefBwdetPWvGIUklObSjcq3OZanrbVL2VHRV3joROW1jAoJaDDLH9Vxu9vw1TiwIHtdUfCsWszaWb_cxI7IcX8f9qy_XQUyrf_V_u3lFbV9sxYuUOExOJBvV0X_4oViFORXUZERI3mEJHtpqmXvVBQkpuzAr6TQvZsPaGIjGU9UjBCEtkPfcMA6sJWgcrvv7htbYVLX4K4tKfoUoXWedYvaZXsjssuiwiPcUllb5hTPw36xQSCZXp9tNxhNTGW8j_feu517CrLArceUcQVXmvKENiazuIFvcqaMfeVq_KZB76iG91ljbGrgOlf6ylEdGUvc9Z6H-vZRtYEx6bk02ClztdugIX26BHsiW9PU0oNXiMVSirbBgVQqJ1vUCG2UNlsacbkFLtKisXRU_dwMWRNy9fkuB4DV5-Z3YK-bWhu1LG7roCAnO3w_1GBkPieqZhaqqelUug5x6XdKArSv-rgS44xRbrqysmisWv1isExE8HqtM1GilXtlgGDAVc5vvEkQ_9z4z8p6BH5cbSZz-tz1jNGvCoSVmN7wM_Qpm9vmwUpHpPM6BChCR06Lf_qWxhbFcSrj8zMjxSxCCShBTdVnKs-fRz0x7mmcrTbgQLn49EjYr3RWb1Q_-qxIvPGDW83wzAUi3b_K_4TwHgTcERwRt4LUKndAswtnIjoyXBgPs7z-RgG-PpCZ109sg7fRHVLYS_60fPbutXkBhUKL9jTpqRSsD-xjbhvdUtiUGGV83TICxfGv2ZGhWEASuThGgnUgZt77hJ63qCnzerjkpbLyFdfKHEvpBSVOvgRdzX_aUavh8OE2kqT10ntTahcIbR_VJ1l0vcxx2UFzZK_lNW9hR5KGtR2v7FfOU78-1BuM-Sc6PySwwWMWImzVRxTsFbHwmzWj9qsQh00IcMMLbKzw3EhPiYqZWxvr9HTKkIVq2SLsMH_3y0
\ No newline at end of file
+xLrlSzqcrd-lJw5rNsIRkTbjq_QPgIVjhElOQUP978_jT6VRwNYmnBsNHeAxWEowVVhTdm7q1n2IGD9rabQlx8k0S_XnWCCv1_WNuu3o8i-YE7uKWrk2qdX7oKq6SnRV8RwD0TvaC8qJIkvOl8QGHt7CtY6y0nJaSOOmvFSx61FAjyJub6I0iVZtA8xZ-3j2KqYFIKPe_CaNN_pFqTUVQUbNMvIymv2n-6aKntz8Lev5QSYYkApdAE7e5l7x2vYW7OAOiz-Z-76CqlWmHfZ33QJnOVni0p6aWCFqsoZUK83vIpF3nSdfoSN9sV79PNpmGdnd1_z0QHGJleNq17F4xtjAlAbp8LWMV1o3chCTeNtqZeoCTVc4Gi7m4OyFOuvoo3Z8TmfxyJF-ZM0O7yQOtFtzKv6_sAN--V-88Xi_GVnejwFa5j9X4AUths2GzfHwoo1z62pY56PmA7SKOy956Gv_vL7CEE05YmzZY8ky2yrZacNWXb30oLyBqGuyJb467mA7DSfWJod9HWWElktlQlSB0h9J1BFq0h8TmMm42A8ARo14vhg16mAoAvZlCi2XPEASadpNE_-vI-m7ifYMc9H3wwUV-r2yW8mKD7bG06b9iwTqpTP-85ALowwefXm5a_XF_hpkWUhQAFvx56EGAznbUYGfhQb8BAlqh-tP_prvqjTDtJqvt8E4eWt281leEQElfiuScm1Qy_PnaX46BmksWpWz9bWCXNgnqRzTGC08hXQU6EBqEP3haY0InOZ9bi7i_YH578gct12IGI2Ah476P9uBpi4Er9K8WMXMDjbpzRTweRi5DDa2-lUdNttrgIOpLRcEGVlcvXSe6Jf67714SFZ8hQlmxiwwXEghlo762lZDeuHaHOvZjYL5behbF0PPHkvYN6JP8pjNZXX3U1ElASdZdAImdON2DQGG9x3A_2X-73zBX39x-4dwrPTVVFRrjqdPx9Vfit_ABzzsDU4rmOWJYl3caWES0fg-V-xN2AS8RqAOFrL5tZ_l5DuYU1V2-eKiyLurf6Qua8AEqHCc3nRHS4cxfuAIkGy2g74wvHmmVMXQPitXU2Lf3bW-GdMfZLnBPzAr9Pw9FnoSKtABKfXsg4r3jJfqftQHWQw-8ZkKl9z-ve9qJtd5sdiP0FwwfGyGxQPFrJB9AAhxjO40-aQOt0n-FX-jbjOjF5PCVmyfR5JHJgsI5Ji1r9Km75PLoV5MqzI6tO3cENuM3E_GZt3EwvxgJevV_Uzn_VajWsaKWpH5Geq5sHdXmlgQA5aGLZ7iAb3Fb-wvqNzA79W9Xtit0uo_LHX-T-zd7Gkner6uaUfzJyUB30yqYX_7mdYBv4Xis7aQQH9UcidBCMQEFwkRwr47b5buS0yBigGGLgFqqZ2FLjBIp1m2148lIDQCynHcw1RI-zUGRqdQOYqjgDn8YPqkJ50Eihz_iVhyKvUuDfQazlOu5q8Zi_p22BuHue4t1TZOM8Z26To0H8cCoOjJvZMgkCXl87spFWUKemJjX3kXwP0ete4D50Z1aWsHOkIqETbHig4W5ttc-aohoRVzuRy5P5mAWJNJ2lUE0GqhRkHcYc2hy-G7pg6990Tq0_avsOaE5mBk82zL1ES73Xpo9xxS8ieBa3qlXS4bfVLyhvlNXIY5p_Hw3Twplc7LhwS1qhiPaEpjLnVIsPA2CeX63dRiYlJiygmAA_vlGORIqrx4VYYpZ9trIekKAREFnJZOWGtWy0r-3Ua66jOjJWku9z-F1dAU5nWbKkHVaOtlxZEnolLkhisi7z1-iyMQHyjfjU3qBf6LxtPVdYoHBmKSDFD-UDlz33A3WsgIwSImr3wETsAnPvozmMzt6G5EQ-z-1yJ9BSHSpOldA2Fy32ZzuHXY3gdi9myXWLOjFLrl4woXn0RbdXfC5X_4UMhoTkQUt48rFS6mgwhEll6RgFGLKTDQnEBitV3ZjnSN9sTNrrSlNvzSNXszFdUDFtA7JSJTwdG0D0woROLlHnAxOJLYtjEvvvGZfknBz1iqM_CEtZkwlp9921uw9CGCEWYUmeJtAZGa8R6KWDw5gqDV276gVMn18Hd2xxu3oRjQlWoqWvt-8L2zFyS-mKVLXl12j-h8BrRAv220KBT_28JxjDkuGTBNPHEiq1NRo-OpDH1WoFNlTFQBn4kKemnGnEzVK5Bi_3p-uV9laHaeQkNUaoaE_ffaHGhJb-ipme_I76579__H0zOmKElGk8XnMlYykAZ9_Nl0NcAvWmGTuGaj2vO5bbm4ZxS0RppcWv3rPExWZ_5hZsqlm_3kqcqn9rdQyQKs2gagkhH5-K6Q9zhM60DtryBXEQAhefvBK_XKIrCn84mjQoTPr3PBfYF1Eou75ANPKPLRY-OR3eejmWqPigkCOPKni9JrCbMQ-vouDZwz2eOzeqjiWZxUgD5z5AwgLgH25_-nU7mKJXmdC4CBkxwQ3nIkL_LMFbVN37V9FjmZTJv4SEqjgBP4My2s-g0lEJqp3ITwyckIeZMQhHEblwdq1mpqj3qN3mT2sK5Eh0mLQuASJ9UedzurVjGeQzSve13p47qav0Z6XwHobvpyW83kj_BO9xRZD4yfoIzpG3dRGiYlE3ZF0DRtJlrXcfIx7VPJmpSbycVIWVKAuNVpdgRnxyfDmqRmaXrcmPJyNS1T1XAObuxHoMPUPwKp6NttQSwr-VRiVgkbGzBG1GnLwUcB895IfH3Ul9XT4iThyBHfeZxkJQ3Rw8OlcFlRsHaYM8t4QKPhW741ijD2XWY_9fXlMrKcAN9DqAKOkpurKsltb22b5BhjglBRvGuc26I8yPCSe6meaxCYXALtMUebWnpfcQtWgTBRFyHIAvkJATtHtwEFY6Y4Ov8wopiM44sEIR5xBqRb8NkFOU7v4vcLfJdDFNiuzaD1Ks8OfdlQ-VbPmFp9ZrWzXUvrNjnaA35Y5hU0mlJVI4vQvh7FbzBBz1nmuEj1KdwfqtwN_iiwKp3eXgieFemVlNn-SdRrylJboVD7XuS7SZjeSNsjoXpueoGZ4l2QDCurO_T5OK9eUamA5THKPc795kQWBTXQOptid_wX40hfGPJ3mmd87HO-zoCkDfDwW5Ty67zMMg0Io5Xt1lzE6jj1V7VQsqSM-dEEknIk4KQwtKjwVI46DndiY5HOyzqbv5oS7zBBM4DOI51QZB3b2R5v3TYxkk0Ey1gAyCttZZ1-hleOuOq84YEODJjnIxvV0vH1QaRDt0YNBtT7gMaAWHssDjOjNozPK2WKSljYnzyzZdyXDuwGnySY353O02VhW1Io8kDrv2Ch4W6WDj7gY64ECS09z86DKxJPG5g7-TQvZDDUPftre0SSRWYzro0V4LOaBKkwQTX_kRbKCAoBp8b4VKPDNdznMUGve0XwQm36Hjgb0cWR_eD_76Xb3jGUGnsbSrTW7w2hQgbA7GIjMz9ySHoK-wsl-ydp82vWZ1AIlw4RWD5lTjJnLwillTKuc1CL-7Ef3ZQqgZ7MIuGPfqNINuBA6Q2AZnrMx2L2puEC0At3dDxgPdrUT4XIoeehKQdB7TGY2-5mBIzKHaDiHRc3Pd7jMVTRt2X7MbLRLVuWBE17rbJwXwhLoR99VmWoJXWStZKdX58evV4v1EahXE5qkrifm1xKQqkcVYvzeYH_ZvZuxQLYJXVbS15K_DttIE83piLhWwUQv8SXmdx2ba8EQOumC7oundZ4W8gJ9RUDf3dSoJCAdzwvCwhRRLdRby6ylrrUIia-1iP8Uy378icpIE3SGcTFaWycSlKL6G-cb5IULY-XhTZJ1BKi_94fmmyect-wQN6ly-4o4KxgcmUTDvPjoB8DSUnOPvpbx1tp_kSujRqec-DjXvgygV_8h5agtnNAuNjVwR-x3vQDAjyW_mynSMkzCUF0iMJXeSRDdqVNqTipjwhJmchHT89xnk-8zkG9STPU5OSVogY5gil-R3icEnpKiaXfuaeAdX0zxbFTqyqvIBQv2LEWQq1reA4Z1aXS-mOHXmmQ--1vPxhMPdp_Srs8qM5MSm7YmcTkDdNAAFdmDWBbBQarKtwZQ5EWL7XoHyclVi2gxknnrnTyQ5SFX4TTMOnD3N_kQES-GxQQfi1V-IM7kv1ZGxWiqzTrPPQFwxXG2upsKI72NKU51WhAOqAjF7ONFCJLUzuofR_Ue7vUZGdLI4Y-4zEBS-NJhkzz-8ZOSRTA_daYOqSSeFnGJgTTMKTJZVJtS3wjZQdV8JA_ur_weR4n5x3ESN1rmhi-eQDWtwVIj6FZuovh7NseROqeuyVbXEYJzoK-5uSMP53iSEupmY_59iEFZx9jNlBloRF8wisooEpDiSZiot3dIR-kettQND8CP5sIPzg9ULpOD-wgztCHio6lelGpNuHDuYmXlGlifVZ-0R-fTAxE_Pn3oWZ6C9kghDlr6RTSG_Csdz0MjiwVb77nWc3hwYzr6-oqw6AuNglvVj3y0Ps7gjJ4un9cERvT7tOKhj6lZhkirwxaQdYwZrELVU4w6BYFGzFZbRN6W6LTZUj6gBw-jlTyckFBVo2WeMVIUfu2V5zJMz_vHTfx9NC1aPKc3eeyEZmKlq4YxlQz1VGUuOqEmCPpxiHzzmTUgDk3JoWrDZ5TXnRctTpcxckWoGxMzq3Rzzr8RhyZwNqTZbzTA-PCT1vtB2ZgmSFVek1_YYW728znpT6iBYMr2NKjacfJBUqvvC2yTx5AyJFWvIY7Jh5qQndY7Y7l9ob2hap-41wRyBrYsfSwOitHNNELdJ3ZUAlkHPOvLpDRMMZQh_IYFUYEAMG9HJldg9vPixo4KgtKBaNrFR0PSkIGjQZqgPkjp7kMupR1ygo9ECHM-vN-wfXuNriyvgxYEQ_H7NqtRjDub5ycX49vFon8jhFESi8izBcbnnAGFRMl_Irre6jRMsE2izQZ3y7zYpWyog3V2TTzk6DyBgbu7EUGCVKMHUsRKxS7LmU4AyzWVJouQTYro5evupskV1Xx8Pvr2V4je0X8TT0y4k-j1WGwXVQvA7QlVM-hIn9drclUl-TFa2ZFNB-vXPIY5CuzHeg6n42Hwl3tlYuWlGLP0TkU0vsqZyJAGcrH33WP3veZq3VTLuA7z-GFzMMJQZ_mdcOxJjv1box7NLi9t-whBuqDm6COW2t9uSMUtt3QClb44EsOmiJsxHNPg6tW_9OQjvqh0vl6QHmVnaSN5qV_-UddQbsHu5tBN0VYNW3HXAE2RmcL5cJ7pG5L8rtl9JcDGbk2uPdpNYd3XQrn8lpL0D_AUp0GuoXXLGpvjT0fhdFn9imrAQSnCP-BwwQkSq3VoGnYZPGtBKTnNWVVNCikLqQ5AFvl0JB4xwy1OmKLiINNAM3R6m9eMhaBGQQeQHyu1GYBkbG85jE-LGvrlHdIUouJAbcjtikPQikT-kuEOafTTNkZ7l0YkX-FdfUxGWzGcYqaeIsRn8z7foZZaFfvuOiSExV5CWAhzxMD7uzU7TcFh7bOpOGwVwtba6bvTWrOA-dclfNK03cmv8YylqeoHQ54GoRf4H0DXfOIZk7TDILtr-REWsIf8xNSvbqpj77qn2ipYAFqYz85fbwOEe995gZQK5dMd0MYDaHzxv8iUBdnSbZ-5wZSK2qGzKBKH0YM8SyBNfvusU_lBMZreBMiW_vOBPDx65wsPNS1gmUi1QTUd9QnvmNJ0ey5Jy_Jtmj0Da3MYnOBG0gWtfUy5v1S82q0jG1QTYKEM9u_V-K1IHhGiDHgM4Bo7E3qceDDorOlCaiGwwPrjbaT9Rf6APH7WThXfkgxqs-fBhA_TZgt1l09UZLz4IaNP0j05a3B0kO0fUV9ymMW2g356p24qB9DxG6d_wdl1QSEd1QCN1Yv7_TU0Ae0wd-5Uy79ngdlKeW5BHkjPPtp06a1nm7E0eeDIkSxoWi-DJxTpoul8Bb0MW1o0RIWqaAb-r7U1Q4QeKLuEg5PKB5GwNpXTu6dWMU1fXEQ1HOBbf3dSFzoo8GyU_kNWsVmBTm59nEdZbTS5vYSC3cU25sGwa7A_Pxb0jamP8k83UArI9KFIew7AHQakf1QvgeWh9P9QX2e1Q5EXDpl7IuecIXrFQQuuEJ2QH4c3v2MXIuGgcNS3U6q0DG9KCSxTmjCRfWMY2o8blCVWH0jyDZmTBzwjm2a0xG0Om4pz6RTWfA1qX85EeJGWeoDZFljhGKS0OxphQy56WbDvqDV2puQFelSMBWiryzs8xDP9XeNBCkcmGsCupsh1PaAcGKK2nJxXQm55OdAq9jR2qnkc0QUvLfWSyEsG6H3rFE-ruAGGgZzFDY2Z41cqNvSg3YU0rk0gO3fVarimSZ2QC76WKtt6sOBHXL62oWMAEq7j1PS12xkTxGMR1GszijWVsbSeaVn4MCaGUfTaYYAv3uzFlZy87xovATFRZAGl5ljCe3mYczHyWv3nhvv-lEJ9pABTWHOy5N_MibdeYDr-iotCllsbFWmteYabVhJV8cYdhUoB6hYKJwOMkH-Z-C3V12tBXIF3yLpS6RQAWQTn2B7KpW1z8VUQ8eFBTeYoygHPXQRdQlQLEpDK5qYwdeYOJH3xSTqMbpLMLPT7wgoWndssW1rZ_GOzbN9FV9M3UbGpcIvFVBLBOh3l3s4KAgjymZcL46Bit4CLOx05XSwyIhJB1IDTnrTPAjlir4pf_o-XhPopyQ3UuoiOjfydOsMciI7sYbppKPJ6nT3X9kiir7l7kasyQvn7KAxZXIC3sEsWma26MAyJXMTMeuOBSra8sezlp5I5L5b3yjdV-qlAvb-IadUAZE9WzQjsKBzij9MXdeaA4mZsRgoIgfyLkBCHDsXZ_5XUUs_zS59t7eV8wYR7G-GIAtJJ5-rKjHqKgaCC-eUYVnlKDK8fzoGQ0HMaQe4M_oAMdVy1v3mU591RA94hfNwqvzv4ZCEfvIOJFHKLdBXt6I5BpnhkTFIbvjfzRQqFvs0fUA5KkgIV4N2BUsXD3gSpaqlo9_btdHwvKvNhI3cIgzsn8uxmGCdQTVLw-LSRNrQkH9Pr7MbzBmxrvsij9AUPdCw0_K1UYvAeTKxELGWLvzM9Sbv2BLZpLlaDhtPP-52LxgwYJSArnIQ5GrcLXvDjrTKDQzYGeLH3cHTTWgfs076q0R3r55HC3DTMjIgfKkDgxibzgfoK-X3AxLxNJ5hzlACivHpMXeFTsiw5OjAtUUTCs0m6VsLWhebJ4yTLQ-Skm25k3Ic9LkKfUFGC3PijJfRZg_WsjeH969j4oblC5of_wxkYAfT9ibXV4UYbY7NdsZNHMz2Y-pur3OFpqBVl9_1e6z-wg1lNtCTj2zobM_qQEjJfL1RoREpKHlazRHLAwcXFTbyLPh_7LQhuFhh2QZqAAW15boB0nAgVgOlLCtgPtKDoihyR29dPjM_ZU6iljousaIhNsK7XTTedqHHEfQHDBLkrjGvdiy2ft2DC4mRySoKxNwBQYiDVD7w4eLhtvtsYbNRR36sVhCFLtfNHhgxpRzQwnfZor67pAybZV3wpRfN6WqMMRURXip7Lid3c5EqsK0Aq-_kLysFoEEYmb7BW7r1fB4Gj2uOMxK_rYKVXiNwca4NerQMIdsHYvRx6huf3DMIl6ugT01KolRyz7lxBBot9lDJtuJH8KFguqg7QmzSEAaFKPr9H6k9O7qZfaiWPcAYRu3uZOoP6Edg2ITf_LArMRfWqrJeqmUJwGVeqxcBQkIy4cpkhKLJzQfpmp4Z9xqpbPkNdebgAZU3NMJpOXQOdHCN2spbv2hgkGjj0WVEkSByr35Uv-IY0mSdNUDMghbMgzxfkiuuAnVknL9R9RNMMzS4pUv0maQgdoTfF4z-FE0zqcoSpsSMrSe2OUMF6lTvkCjTjpxYCcrL_zTO8HqMyXbDxSHE7BItJ-AdqrJNuYEQCpp9eDQOnuV3ateCvLhinoXMXntoAxTlVYgznlAgjL-QN_KcwVUuJBLdaRgoUwTLQomnitzr8ereHIrxpYHJK3KJdBTCGchciDEZskGpXmLgrJ95HHhGuxtIofvNLjHsIxDnL3gy51eXfvTY7ArAlzThLyhygFhSNe0oKO-BYifxDaPFKehfZICgbuses_HCDZNha_geUXjyomehS4DhSYv3LQsalTw4Kz2kMTAgtZMf8-giz952_5t5Mv93LKdKhEDpnPhJyl_-2FFfT9tHnLt47U74qpdm8JoDNZUscybeTaKGc3JQqGDZQBcqnePEMsjKvpgcdGcXHCKeML-2wsvkbgahfNLIS6trUUHoD9QdLKIDaRgKPFHKmKHnSwPmHRVD69bLHVRgWLgpq-mCs7BouCmTbVqttq41BluPEXOGJDKGjPA49clwSUH0aRDFUalyPD9AYg7zFL8hnXp72aJrLK1PdJwnVdlJeTCmDnlJwCWmze9lHKXn1XX_kql5IKFXnAAbaXDAmAMyLYJQccjexNwc4GMNZq4abBuhaafHQqTIZQdRZkTfwZQ-XDVjSXANbRTznA6rz_K2dW7rpisu8j6n3m-bOgusgcfziGcjhxdAHGIqKwWYzrcT6boXlRQkcXLsL5sEIREfTlACwRf0eQPmQts8gbFp4q-dt9RLj4z8Z29H5wbCZp_KzLUcXYfbuFWlFeZ_q6WIpXqqgpMoqiIOsOoECxJ-LJkqK-PpMaktQ7bJimzpi5RC_Ynaz93zFP7mmcrUagQMn49Ejar3Rag5LloqRIwPKB7X7fwrwWwJzxyHtvAYYiOsqrk9i_9Z65jrkocQbv2NK3iFxusLXyJdP620J9G6Iwc_p5vzfojaNZA6uUfgIqcnsRPqp9NvkQMlcT_TofA3yGvoFJc73q7sCk4wf3Xsjih4xg6CEVIke_Sm7Alts7AKNtQVbn8vdTjmzpcgiUE5-H-H6akYuwpGr4d4D6VFfAPizDC7umQO7M4zVh8fxUl1NMopepgsvI8VMn_kze1FXRyoOfh-pR6CQXx0sjhlFuoL7R6q24hLPQCS1oIRfcLHtO8wjcsgaihOEROMrROe-XjYh83VF0BDXKYnt9BYJRoc98zpaiACFNBaOp66C8qvYMzWN8MZtn2-ZLD41Vm4iqUH8mAG28RlOPQHkzKDoMn-YS_bDyyIaX5w-CdJBx_-_FJqsvBb9uBb9vBbP_-KERwDPk2yYaZww1Xl0eeUblNeNn2dHPv5_my0
\ No newline at end of file
diff --git a/docs/logical_data_model.puml b/docs/logical_data_model.puml
index e56801df94..60ec7c1745 100644
--- a/docs/logical_data_model.puml
+++ b/docs/logical_data_model.puml
@@ -88,10 +88,17 @@ class ActivityReportGoals{
class ActivityReportObjectiveCitations{
* id : integer :
- * activityReportObjectiveId : integer
- * citation : text
+ * activityReportObjectiveId : integer : REFERENCES "ActivityReportObjectives".id
+!issue='column should not allow null'
+!issue='column missing from model'
+!issue='column reference missing' * citationId: integer : REFERENCES "MonitoringStandards".id
+!issue='column should not allow null'
+!issue='column missing from model'
+!issue='column reference missing' * findingId: integer : REFERENCES "MonitoringFindings".id
+!issue='column should not allow null'
+!issue='column missing from model'
+!issue='column reference missing' * reviewId: integer : REFERENCES "MonitoringReviews".id
* createdAt : timestamp with time zone : now()
- * monitoringReferences : jsonb
* updatedAt : timestamp with time zone : now()
}
@@ -412,6 +419,7 @@ class GoalTemplates{
* updatedAt : timestamp with time zone
lastUsed : timestamp with time zone
source : varchar(255)
+!issue='column missing from model' standard: text
}
class Goals{
@@ -550,6 +558,7 @@ class Imports{
* updatedAt : timestamp with time zone
fileMask : text
path : text
+!issue='column missing from model' postProcessingActions: jsonb
}
class MailerLogs{
@@ -2674,4 +2683,11 @@ Roles "n" }--[#black,dotted,thickness=2]--{ "n" Topics : topics, roles
Roles "n" }--[#black,dotted,thickness=2]--{ "n" Users : users, roles
Scopes "n" }--[#black,dotted,thickness=2]--{ "n" Users : users, scopes
+!issue='association missing from models'!issue='associations need to be defined both directions'
+MonitoringFindings o--[#yellow,bold,thickness=2]--o ActivityReportObjectiveCitations : missing-from-model
+!issue='associations need to be defined both directions'
+MonitoringReviews o--[#yellow,bold,thickness=2]--o ActivityReportObjectiveCitations : missing-from-model
+!issue='associations need to be defined both directions'
+MonitoringStandards o--[#yellow,bold,thickness=2]--o ActivityReportObjectiveCitations : missing-from-model
+
@enduml
diff --git a/src/goalServices/getGoalsForReport.ts b/src/goalServices/getGoalsForReport.ts
index f6528d8b24..45be9c00d4 100644
--- a/src/goalServices/getGoalsForReport.ts
+++ b/src/goalServices/getGoalsForReport.ts
@@ -20,6 +20,7 @@ const {
ActivityReportObjectiveFile,
ActivityReportObjectiveResource,
ActivityReportObjectiveCourse,
+ ActivityReportObjectiveCitation,
sequelize,
Resource,
ActivityReportGoal,
@@ -130,6 +131,12 @@ export default async function getGoalsForReport(reportId: number) {
},
],
},
+ {
+ separate: true,
+ model: ActivityReportObjectiveCitation,
+ as: 'activityReportObjectiveCitations',
+ required: false,
+ },
{
separate: true,
model: ActivityReportObjectiveFile,
diff --git a/src/goalServices/goals.js b/src/goalServices/goals.js
index 5dca1120bd..fd1c0d6c17 100644
--- a/src/goalServices/goals.js
+++ b/src/goalServices/goals.js
@@ -26,6 +26,7 @@ import {
ActivityReportGoalFieldResponse,
File,
Program,
+ ActivityReportObjectiveCitation,
} from '../models';
import {
OBJECTIVE_STATUS,
@@ -198,6 +199,14 @@ export async function goalsByIdsAndActivityReport(id, activityReportId) {
attributes: [],
},
},
+ {
+ model: ActivityReportObjectiveCitation,
+ as: 'citations',
+ attributes: ['citation', 'monitoringReferences'],
+ through: {
+ attributes: [],
+ },
+ },
{
model: Resource,
as: 'resources',
@@ -1277,7 +1286,34 @@ export async function saveGoalsForReport(goals, report) {
},
});
- const currentGoals = await Promise.all(goals.map(async (goal) => {
+ // Loop and Create or Update goals.
+ const currentGoals = await Promise.all(goals.map(async (goal, index) => {
+ // We need to skip creation of monitoring goals for non monitoring grants.
+ if (goal.standard === 'Monitoring') {
+ // Find the corresponding monitoring goals.
+ const monitoringGoals = await Goal.findAll({
+ attribute: ['grantId'],
+ raw: true,
+ where: {
+ grantId: goal.grantIds,
+ standard: 'Monitoring',
+ status: { [Op.not]: GOAL_STATUS.CLOSED },
+ },
+ });
+ if (monitoringGoals.length > 0) {
+ // Replace the goal granIds only with the grants that should have monitoring goals created.
+ // eslint-disable-next-line no-param-reassign
+ goals[index].grantIds = monitoringGoals;
+ } else {
+ // Do not create monitoring goals for any of these recipients.
+ // eslint-disable-next-line no-param-reassign
+ // delete goals[index];
+ // eslint-disable-next-line no-param-reassign
+ goals[index].grantIds = [];
+ return [];
+ }
+ }
+
const status = goal.status ? goal.status : GOAL_STATUS.DRAFT;
const endDate = goal.endDate && goal.endDate.toLowerCase() !== 'invalid date' ? goal.endDate : null;
const isActivelyBeingEditing = goal.isActivelyBeingEditing
@@ -1414,6 +1450,7 @@ export async function saveGoalsForReport(goals, report) {
supportType,
courses,
objectiveCreatedHere,
+ citations,
} = savedObjective;
// this will link our objective to the activity report through
@@ -1425,6 +1462,7 @@ export async function saveGoalsForReport(goals, report) {
{
resources,
topics,
+ citations,
files,
courses,
status,
diff --git a/src/goalServices/goals.test.js b/src/goalServices/goals.test.js
index 32a4a3c936..f92f1cce65 100644
--- a/src/goalServices/goals.test.js
+++ b/src/goalServices/goals.test.js
@@ -50,7 +50,6 @@ import {
import {
mergeCollaborators,
} from '../models/helpers/genericCollaborator';
-import getGoalsForReport from './getGoalsForReport';
jest.mock('./changeGoalStatus', () => ({
__esModule: true,
@@ -603,6 +602,78 @@ describe('Goals DB service', () => {
}), { individualHooks: true });
});
+ it('creates a monitoring goal only when the grant has an existing monitoring goal', async () => {
+ const mockMonitoringGoal = {
+ id: 1,
+ grantId: mockGrantId,
+ name: 'Monitoring Goal',
+ status: 'In Progress',
+ objectives: [],
+ };
+
+ Goal.findAll = jest.fn().mockResolvedValue([{ ...mockMonitoringGoal }]);
+
+ await saveGoalsForReport([
+ {
+ isNew: true, grantIds: [mockGrantId], name: 'name', status: 'In progress', objectives: [],
+ },
+ ], { id: mockActivityReportId });
+
+ expect(Goal.create).toHaveBeenCalledWith(expect.objectContaining({
+ createdVia: 'activityReport',
+ grantId: mockGrantId,
+ name: 'name',
+ status: 'In progress',
+ }), { individualHooks: true });
+ });
+
+ it('does not create a monitoring goal when the grant does not have an existing monitoring goal', async () => {
+ Goal.findAll = jest.fn().mockResolvedValue([]);
+ await saveGoalsForReport([
+ {
+ isNew: true, grantIds: [mockGrantId], name: 'name', status: 'In progress', objectives: [],
+ },
+ ], { id: mockActivityReportId });
+
+ expect(Goal.create).not.toHaveBeenCalledWith();
+ });
+
+ it('creates a monitoring goal for only the grants that has an existing monitoring goal', async () => {
+ const mockMonitoringGoal = {
+ id: 2,
+ grantId: 2,
+ name: 'Monitoring Goal',
+ status: 'In Progress',
+ objectives: [],
+ };
+
+ Goal.findAll = jest.fn().mockResolvedValue([
+ { ...mockMonitoringGoal, grantId: 2 },
+ { ...mockMonitoringGoal, grantId: 3 },
+ ]);
+
+ await saveGoalsForReport([
+ {
+ isNew: true, grantIds: [1, 2, 3], name: 'name', status: 'In progress', objectives: [],
+ },
+ ], { id: mockActivityReportId });
+
+ expect(Goal.create).toHaveBeenCalledWith(expect.objectContaining(
+ {
+ createdVia: 'activityReport',
+ grantId: 2,
+ name: 'name',
+ status: 'In progress',
+ },
+ {
+ createdVia: 'activityReport',
+ grantId: 3,
+ name: 'name',
+ status: 'In progress',
+ },
+ ), { individualHooks: true });
+ });
+
it('can use existing goals', async () => {
ActivityReportGoal.findOne.mockResolvedValue({
goalId: mockGoalId,
diff --git a/src/services/objectives.ts b/src/services/objectives.ts
index 3e3d96440f..9230c2e696 100644
--- a/src/services/objectives.ts
+++ b/src/services/objectives.ts
@@ -59,7 +59,7 @@ export async function saveObjectivesForReport(objectives, report) {
const updatedObjectives = await Promise.all(objectives.map(async (objective, index) => Promise
.all(objective.recipientIds.map(async (otherEntityId) => {
const {
- topics, files, resources, courses, objectiveCreatedHere,
+ topics, files, resources, courses, objectiveCreatedHere, citations,
} = objective;
// Determine if this objective already exists.
@@ -119,6 +119,7 @@ export async function saveObjectivesForReport(objectives, report) {
resources,
topics,
files,
+ citations,
courses,
ttaProvided: objective.ttaProvided,
supportType: objective.supportType,
diff --git a/src/services/reportCache.js b/src/services/reportCache.js
index e500936fdf..bafae2ad63 100644
--- a/src/services/reportCache.js
+++ b/src/services/reportCache.js
@@ -212,6 +212,7 @@ const cacheObjectiveMetadata = async (objective, reportId, metadata) => {
files,
resources,
topics,
+ citations,
ttaProvided,
status,
courses,
@@ -265,6 +266,7 @@ const cacheObjectiveMetadata = async (objective, reportId, metadata) => {
cacheResources(objectiveId, activityReportObjectiveId, resources),
cacheTopics(objectiveId, activityReportObjectiveId, topics),
cacheCourses(objectiveId, activityReportObjectiveId, courses),
+ cacheCitations(objectiveId, activityReportObjectiveId, citations),
]);
};
From a7147e25ed48f8dca9473d35f3124b53f1ef5808 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Tue, 26 Nov 2024 08:51:17 -0500
Subject: [PATCH 032/198] fix puml
---
docs/logical_data_model.encoded | 2 +-
docs/logical_data_model.puml | 22 +++-------------------
2 files changed, 4 insertions(+), 20 deletions(-)
diff --git a/docs/logical_data_model.encoded b/docs/logical_data_model.encoded
index 41322e5ecb..cfff944431 100644
--- a/docs/logical_data_model.encoded
+++ b/docs/logical_data_model.encoded
@@ -1 +1 @@
-xLrlSzqcrd-lJw5rNsIRkTbjq_QPgIVjhElOQUP978_jT6VRwNYmnBsNHeAxWEowVVhTdm7q1n2IGD9rabQlx8k0S_XnWCCv1_WNuu3o8i-YE7uKWrk2qdX7oKq6SnRV8RwD0TvaC8qJIkvOl8QGHt7CtY6y0nJaSOOmvFSx61FAjyJub6I0iVZtA8xZ-3j2KqYFIKPe_CaNN_pFqTUVQUbNMvIymv2n-6aKntz8Lev5QSYYkApdAE7e5l7x2vYW7OAOiz-Z-76CqlWmHfZ33QJnOVni0p6aWCFqsoZUK83vIpF3nSdfoSN9sV79PNpmGdnd1_z0QHGJleNq17F4xtjAlAbp8LWMV1o3chCTeNtqZeoCTVc4Gi7m4OyFOuvoo3Z8TmfxyJF-ZM0O7yQOtFtzKv6_sAN--V-88Xi_GVnejwFa5j9X4AUths2GzfHwoo1z62pY56PmA7SKOy956Gv_vL7CEE05YmzZY8ky2yrZacNWXb30oLyBqGuyJb467mA7DSfWJod9HWWElktlQlSB0h9J1BFq0h8TmMm42A8ARo14vhg16mAoAvZlCi2XPEASadpNE_-vI-m7ifYMc9H3wwUV-r2yW8mKD7bG06b9iwTqpTP-85ALowwefXm5a_XF_hpkWUhQAFvx56EGAznbUYGfhQb8BAlqh-tP_prvqjTDtJqvt8E4eWt281leEQElfiuScm1Qy_PnaX46BmksWpWz9bWCXNgnqRzTGC08hXQU6EBqEP3haY0InOZ9bi7i_YH578gct12IGI2Ah476P9uBpi4Er9K8WMXMDjbpzRTweRi5DDa2-lUdNttrgIOpLRcEGVlcvXSe6Jf67714SFZ8hQlmxiwwXEghlo762lZDeuHaHOvZjYL5behbF0PPHkvYN6JP8pjNZXX3U1ElASdZdAImdON2DQGG9x3A_2X-73zBX39x-4dwrPTVVFRrjqdPx9Vfit_ABzzsDU4rmOWJYl3caWES0fg-V-xN2AS8RqAOFrL5tZ_l5DuYU1V2-eKiyLurf6Qua8AEqHCc3nRHS4cxfuAIkGy2g74wvHmmVMXQPitXU2Lf3bW-GdMfZLnBPzAr9Pw9FnoSKtABKfXsg4r3jJfqftQHWQw-8ZkKl9z-ve9qJtd5sdiP0FwwfGyGxQPFrJB9AAhxjO40-aQOt0n-FX-jbjOjF5PCVmyfR5JHJgsI5Ji1r9Km75PLoV5MqzI6tO3cENuM3E_GZt3EwvxgJevV_Uzn_VajWsaKWpH5Geq5sHdXmlgQA5aGLZ7iAb3Fb-wvqNzA79W9Xtit0uo_LHX-T-zd7Gkner6uaUfzJyUB30yqYX_7mdYBv4Xis7aQQH9UcidBCMQEFwkRwr47b5buS0yBigGGLgFqqZ2FLjBIp1m2148lIDQCynHcw1RI-zUGRqdQOYqjgDn8YPqkJ50Eihz_iVhyKvUuDfQazlOu5q8Zi_p22BuHue4t1TZOM8Z26To0H8cCoOjJvZMgkCXl87spFWUKemJjX3kXwP0ete4D50Z1aWsHOkIqETbHig4W5ttc-aohoRVzuRy5P5mAWJNJ2lUE0GqhRkHcYc2hy-G7pg6990Tq0_avsOaE5mBk82zL1ES73Xpo9xxS8ieBa3qlXS4bfVLyhvlNXIY5p_Hw3Twplc7LhwS1qhiPaEpjLnVIsPA2CeX63dRiYlJiygmAA_vlGORIqrx4VYYpZ9trIekKAREFnJZOWGtWy0r-3Ua66jOjJWku9z-F1dAU5nWbKkHVaOtlxZEnolLkhisi7z1-iyMQHyjfjU3qBf6LxtPVdYoHBmKSDFD-UDlz33A3WsgIwSImr3wETsAnPvozmMzt6G5EQ-z-1yJ9BSHSpOldA2Fy32ZzuHXY3gdi9myXWLOjFLrl4woXn0RbdXfC5X_4UMhoTkQUt48rFS6mgwhEll6RgFGLKTDQnEBitV3ZjnSN9sTNrrSlNvzSNXszFdUDFtA7JSJTwdG0D0woROLlHnAxOJLYtjEvvvGZfknBz1iqM_CEtZkwlp9921uw9CGCEWYUmeJtAZGa8R6KWDw5gqDV276gVMn18Hd2xxu3oRjQlWoqWvt-8L2zFyS-mKVLXl12j-h8BrRAv220KBT_28JxjDkuGTBNPHEiq1NRo-OpDH1WoFNlTFQBn4kKemnGnEzVK5Bi_3p-uV9laHaeQkNUaoaE_ffaHGhJb-ipme_I76579__H0zOmKElGk8XnMlYykAZ9_Nl0NcAvWmGTuGaj2vO5bbm4ZxS0RppcWv3rPExWZ_5hZsqlm_3kqcqn9rdQyQKs2gagkhH5-K6Q9zhM60DtryBXEQAhefvBK_XKIrCn84mjQoTPr3PBfYF1Eou75ANPKPLRY-OR3eejmWqPigkCOPKni9JrCbMQ-vouDZwz2eOzeqjiWZxUgD5z5AwgLgH25_-nU7mKJXmdC4CBkxwQ3nIkL_LMFbVN37V9FjmZTJv4SEqjgBP4My2s-g0lEJqp3ITwyckIeZMQhHEblwdq1mpqj3qN3mT2sK5Eh0mLQuASJ9UedzurVjGeQzSve13p47qav0Z6XwHobvpyW83kj_BO9xRZD4yfoIzpG3dRGiYlE3ZF0DRtJlrXcfIx7VPJmpSbycVIWVKAuNVpdgRnxyfDmqRmaXrcmPJyNS1T1XAObuxHoMPUPwKp6NttQSwr-VRiVgkbGzBG1GnLwUcB895IfH3Ul9XT4iThyBHfeZxkJQ3Rw8OlcFlRsHaYM8t4QKPhW741ijD2XWY_9fXlMrKcAN9DqAKOkpurKsltb22b5BhjglBRvGuc26I8yPCSe6meaxCYXALtMUebWnpfcQtWgTBRFyHIAvkJATtHtwEFY6Y4Ov8wopiM44sEIR5xBqRb8NkFOU7v4vcLfJdDFNiuzaD1Ks8OfdlQ-VbPmFp9ZrWzXUvrNjnaA35Y5hU0mlJVI4vQvh7FbzBBz1nmuEj1KdwfqtwN_iiwKp3eXgieFemVlNn-SdRrylJboVD7XuS7SZjeSNsjoXpueoGZ4l2QDCurO_T5OK9eUamA5THKPc795kQWBTXQOptid_wX40hfGPJ3mmd87HO-zoCkDfDwW5Ty67zMMg0Io5Xt1lzE6jj1V7VQsqSM-dEEknIk4KQwtKjwVI46DndiY5HOyzqbv5oS7zBBM4DOI51QZB3b2R5v3TYxkk0Ey1gAyCttZZ1-hleOuOq84YEODJjnIxvV0vH1QaRDt0YNBtT7gMaAWHssDjOjNozPK2WKSljYnzyzZdyXDuwGnySY353O02VhW1Io8kDrv2Ch4W6WDj7gY64ECS09z86DKxJPG5g7-TQvZDDUPftre0SSRWYzro0V4LOaBKkwQTX_kRbKCAoBp8b4VKPDNdznMUGve0XwQm36Hjgb0cWR_eD_76Xb3jGUGnsbSrTW7w2hQgbA7GIjMz9ySHoK-wsl-ydp82vWZ1AIlw4RWD5lTjJnLwillTKuc1CL-7Ef3ZQqgZ7MIuGPfqNINuBA6Q2AZnrMx2L2puEC0At3dDxgPdrUT4XIoeehKQdB7TGY2-5mBIzKHaDiHRc3Pd7jMVTRt2X7MbLRLVuWBE17rbJwXwhLoR99VmWoJXWStZKdX58evV4v1EahXE5qkrifm1xKQqkcVYvzeYH_ZvZuxQLYJXVbS15K_DttIE83piLhWwUQv8SXmdx2ba8EQOumC7oundZ4W8gJ9RUDf3dSoJCAdzwvCwhRRLdRby6ylrrUIia-1iP8Uy378icpIE3SGcTFaWycSlKL6G-cb5IULY-XhTZJ1BKi_94fmmyect-wQN6ly-4o4KxgcmUTDvPjoB8DSUnOPvpbx1tp_kSujRqec-DjXvgygV_8h5agtnNAuNjVwR-x3vQDAjyW_mynSMkzCUF0iMJXeSRDdqVNqTipjwhJmchHT89xnk-8zkG9STPU5OSVogY5gil-R3icEnpKiaXfuaeAdX0zxbFTqyqvIBQv2LEWQq1reA4Z1aXS-mOHXmmQ--1vPxhMPdp_Srs8qM5MSm7YmcTkDdNAAFdmDWBbBQarKtwZQ5EWL7XoHyclVi2gxknnrnTyQ5SFX4TTMOnD3N_kQES-GxQQfi1V-IM7kv1ZGxWiqzTrPPQFwxXG2upsKI72NKU51WhAOqAjF7ONFCJLUzuofR_Ue7vUZGdLI4Y-4zEBS-NJhkzz-8ZOSRTA_daYOqSSeFnGJgTTMKTJZVJtS3wjZQdV8JA_ur_weR4n5x3ESN1rmhi-eQDWtwVIj6FZuovh7NseROqeuyVbXEYJzoK-5uSMP53iSEupmY_59iEFZx9jNlBloRF8wisooEpDiSZiot3dIR-kettQND8CP5sIPzg9ULpOD-wgztCHio6lelGpNuHDuYmXlGlifVZ-0R-fTAxE_Pn3oWZ6C9kghDlr6RTSG_Csdz0MjiwVb77nWc3hwYzr6-oqw6AuNglvVj3y0Ps7gjJ4un9cERvT7tOKhj6lZhkirwxaQdYwZrELVU4w6BYFGzFZbRN6W6LTZUj6gBw-jlTyckFBVo2WeMVIUfu2V5zJMz_vHTfx9NC1aPKc3eeyEZmKlq4YxlQz1VGUuOqEmCPpxiHzzmTUgDk3JoWrDZ5TXnRctTpcxckWoGxMzq3Rzzr8RhyZwNqTZbzTA-PCT1vtB2ZgmSFVek1_YYW728znpT6iBYMr2NKjacfJBUqvvC2yTx5AyJFWvIY7Jh5qQndY7Y7l9ob2hap-41wRyBrYsfSwOitHNNELdJ3ZUAlkHPOvLpDRMMZQh_IYFUYEAMG9HJldg9vPixo4KgtKBaNrFR0PSkIGjQZqgPkjp7kMupR1ygo9ECHM-vN-wfXuNriyvgxYEQ_H7NqtRjDub5ycX49vFon8jhFESi8izBcbnnAGFRMl_Irre6jRMsE2izQZ3y7zYpWyog3V2TTzk6DyBgbu7EUGCVKMHUsRKxS7LmU4AyzWVJouQTYro5evupskV1Xx8Pvr2V4je0X8TT0y4k-j1WGwXVQvA7QlVM-hIn9drclUl-TFa2ZFNB-vXPIY5CuzHeg6n42Hwl3tlYuWlGLP0TkU0vsqZyJAGcrH33WP3veZq3VTLuA7z-GFzMMJQZ_mdcOxJjv1box7NLi9t-whBuqDm6COW2t9uSMUtt3QClb44EsOmiJsxHNPg6tW_9OQjvqh0vl6QHmVnaSN5qV_-UddQbsHu5tBN0VYNW3HXAE2RmcL5cJ7pG5L8rtl9JcDGbk2uPdpNYd3XQrn8lpL0D_AUp0GuoXXLGpvjT0fhdFn9imrAQSnCP-BwwQkSq3VoGnYZPGtBKTnNWVVNCikLqQ5AFvl0JB4xwy1OmKLiINNAM3R6m9eMhaBGQQeQHyu1GYBkbG85jE-LGvrlHdIUouJAbcjtikPQikT-kuEOafTTNkZ7l0YkX-FdfUxGWzGcYqaeIsRn8z7foZZaFfvuOiSExV5CWAhzxMD7uzU7TcFh7bOpOGwVwtba6bvTWrOA-dclfNK03cmv8YylqeoHQ54GoRf4H0DXfOIZk7TDILtr-REWsIf8xNSvbqpj77qn2ipYAFqYz85fbwOEe995gZQK5dMd0MYDaHzxv8iUBdnSbZ-5wZSK2qGzKBKH0YM8SyBNfvusU_lBMZreBMiW_vOBPDx65wsPNS1gmUi1QTUd9QnvmNJ0ey5Jy_Jtmj0Da3MYnOBG0gWtfUy5v1S82q0jG1QTYKEM9u_V-K1IHhGiDHgM4Bo7E3qceDDorOlCaiGwwPrjbaT9Rf6APH7WThXfkgxqs-fBhA_TZgt1l09UZLz4IaNP0j05a3B0kO0fUV9ymMW2g356p24qB9DxG6d_wdl1QSEd1QCN1Yv7_TU0Ae0wd-5Uy79ngdlKeW5BHkjPPtp06a1nm7E0eeDIkSxoWi-DJxTpoul8Bb0MW1o0RIWqaAb-r7U1Q4QeKLuEg5PKB5GwNpXTu6dWMU1fXEQ1HOBbf3dSFzoo8GyU_kNWsVmBTm59nEdZbTS5vYSC3cU25sGwa7A_Pxb0jamP8k83UArI9KFIew7AHQakf1QvgeWh9P9QX2e1Q5EXDpl7IuecIXrFQQuuEJ2QH4c3v2MXIuGgcNS3U6q0DG9KCSxTmjCRfWMY2o8blCVWH0jyDZmTBzwjm2a0xG0Om4pz6RTWfA1qX85EeJGWeoDZFljhGKS0OxphQy56WbDvqDV2puQFelSMBWiryzs8xDP9XeNBCkcmGsCupsh1PaAcGKK2nJxXQm55OdAq9jR2qnkc0QUvLfWSyEsG6H3rFE-ruAGGgZzFDY2Z41cqNvSg3YU0rk0gO3fVarimSZ2QC76WKtt6sOBHXL62oWMAEq7j1PS12xkTxGMR1GszijWVsbSeaVn4MCaGUfTaYYAv3uzFlZy87xovATFRZAGl5ljCe3mYczHyWv3nhvv-lEJ9pABTWHOy5N_MibdeYDr-iotCllsbFWmteYabVhJV8cYdhUoB6hYKJwOMkH-Z-C3V12tBXIF3yLpS6RQAWQTn2B7KpW1z8VUQ8eFBTeYoygHPXQRdQlQLEpDK5qYwdeYOJH3xSTqMbpLMLPT7wgoWndssW1rZ_GOzbN9FV9M3UbGpcIvFVBLBOh3l3s4KAgjymZcL46Bit4CLOx05XSwyIhJB1IDTnrTPAjlir4pf_o-XhPopyQ3UuoiOjfydOsMciI7sYbppKPJ6nT3X9kiir7l7kasyQvn7KAxZXIC3sEsWma26MAyJXMTMeuOBSra8sezlp5I5L5b3yjdV-qlAvb-IadUAZE9WzQjsKBzij9MXdeaA4mZsRgoIgfyLkBCHDsXZ_5XUUs_zS59t7eV8wYR7G-GIAtJJ5-rKjHqKgaCC-eUYVnlKDK8fzoGQ0HMaQe4M_oAMdVy1v3mU591RA94hfNwqvzv4ZCEfvIOJFHKLdBXt6I5BpnhkTFIbvjfzRQqFvs0fUA5KkgIV4N2BUsXD3gSpaqlo9_btdHwvKvNhI3cIgzsn8uxmGCdQTVLw-LSRNrQkH9Pr7MbzBmxrvsij9AUPdCw0_K1UYvAeTKxELGWLvzM9Sbv2BLZpLlaDhtPP-52LxgwYJSArnIQ5GrcLXvDjrTKDQzYGeLH3cHTTWgfs076q0R3r55HC3DTMjIgfKkDgxibzgfoK-X3AxLxNJ5hzlACivHpMXeFTsiw5OjAtUUTCs0m6VsLWhebJ4yTLQ-Skm25k3Ic9LkKfUFGC3PijJfRZg_WsjeH969j4oblC5of_wxkYAfT9ibXV4UYbY7NdsZNHMz2Y-pur3OFpqBVl9_1e6z-wg1lNtCTj2zobM_qQEjJfL1RoREpKHlazRHLAwcXFTbyLPh_7LQhuFhh2QZqAAW15boB0nAgVgOlLCtgPtKDoihyR29dPjM_ZU6iljousaIhNsK7XTTedqHHEfQHDBLkrjGvdiy2ft2DC4mRySoKxNwBQYiDVD7w4eLhtvtsYbNRR36sVhCFLtfNHhgxpRzQwnfZor67pAybZV3wpRfN6WqMMRURXip7Lid3c5EqsK0Aq-_kLysFoEEYmb7BW7r1fB4Gj2uOMxK_rYKVXiNwca4NerQMIdsHYvRx6huf3DMIl6ugT01KolRyz7lxBBot9lDJtuJH8KFguqg7QmzSEAaFKPr9H6k9O7qZfaiWPcAYRu3uZOoP6Edg2ITf_LArMRfWqrJeqmUJwGVeqxcBQkIy4cpkhKLJzQfpmp4Z9xqpbPkNdebgAZU3NMJpOXQOdHCN2spbv2hgkGjj0WVEkSByr35Uv-IY0mSdNUDMghbMgzxfkiuuAnVknL9R9RNMMzS4pUv0maQgdoTfF4z-FE0zqcoSpsSMrSe2OUMF6lTvkCjTjpxYCcrL_zTO8HqMyXbDxSHE7BItJ-AdqrJNuYEQCpp9eDQOnuV3ateCvLhinoXMXntoAxTlVYgznlAgjL-QN_KcwVUuJBLdaRgoUwTLQomnitzr8ereHIrxpYHJK3KJdBTCGchciDEZskGpXmLgrJ95HHhGuxtIofvNLjHsIxDnL3gy51eXfvTY7ArAlzThLyhygFhSNe0oKO-BYifxDaPFKehfZICgbuses_HCDZNha_geUXjyomehS4DhSYv3LQsalTw4Kz2kMTAgtZMf8-giz952_5t5Mv93LKdKhEDpnPhJyl_-2FFfT9tHnLt47U74qpdm8JoDNZUscybeTaKGc3JQqGDZQBcqnePEMsjKvpgcdGcXHCKeML-2wsvkbgahfNLIS6trUUHoD9QdLKIDaRgKPFHKmKHnSwPmHRVD69bLHVRgWLgpq-mCs7BouCmTbVqttq41BluPEXOGJDKGjPA49clwSUH0aRDFUalyPD9AYg7zFL8hnXp72aJrLK1PdJwnVdlJeTCmDnlJwCWmze9lHKXn1XX_kql5IKFXnAAbaXDAmAMyLYJQccjexNwc4GMNZq4abBuhaafHQqTIZQdRZkTfwZQ-XDVjSXANbRTznA6rz_K2dW7rpisu8j6n3m-bOgusgcfziGcjhxdAHGIqKwWYzrcT6boXlRQkcXLsL5sEIREfTlACwRf0eQPmQts8gbFp4q-dt9RLj4z8Z29H5wbCZp_KzLUcXYfbuFWlFeZ_q6WIpXqqgpMoqiIOsOoECxJ-LJkqK-PpMaktQ7bJimzpi5RC_Ynaz93zFP7mmcrUagQMn49Ejar3Rag5LloqRIwPKB7X7fwrwWwJzxyHtvAYYiOsqrk9i_9Z65jrkocQbv2NK3iFxusLXyJdP620J9G6Iwc_p5vzfojaNZA6uUfgIqcnsRPqp9NvkQMlcT_TofA3yGvoFJc73q7sCk4wf3Xsjih4xg6CEVIke_Sm7Alts7AKNtQVbn8vdTjmzpcgiUE5-H-H6akYuwpGr4d4D6VFfAPizDC7umQO7M4zVh8fxUl1NMopepgsvI8VMn_kze1FXRyoOfh-pR6CQXx0sjhlFuoL7R6q24hLPQCS1oIRfcLHtO8wjcsgaihOEROMrROe-XjYh83VF0BDXKYnt9BYJRoc98zpaiACFNBaOp66C8qvYMzWN8MZtn2-ZLD41Vm4iqUH8mAG28RlOPQHkzKDoMn-YS_bDyyIaX5w-CdJBx_-_FJqsvBb9uBb9vBbP_-KERwDPk2yYaZww1Xl0eeUblNeNn2dHPv5_my0
\ No newline at end of file
+xLrjRzqsblwkNo5uFhGDRWVJThl06hEBSHmd3JPn3DlfO5eK1YtHzxAHo3iavTJjzhylIEg5f2Y9b7ITfEKd-rBaEOSF3yd3S_Zo3yO1vLLP96dwMGhk2ShJFIMt1InP-XxnNGhmje1vcb7odgLt4F8aJTaxXFU0WZ8j48RyOGzJGlcE69-o0Z2M_fQaQPe-9JI7z9GKXAP_-UQR_ppvhxzfwTSxbBt3aB7qwoHDVvUYd8hIa2LfJUSau-WUyOTBcA4zWfYptvBqIOhoz3X5cCCjfEbn-lSjn9023lF_IT8j1PY_D3DSdZq_kpmxFpsxIezUYE_iwAyeJrB2Tv2UOOxumqY9Dsqk1Ek2JrBGIhOdT8pVYP6nBPzHA0G-uUbnob496GVbNc4lVgQ_4WpJunIJzvz_8V9N-zm__-z9OkCdc9_izvJSGpeDulBwLGpIaL9l6QIFWqMQmm9EvKvIJBWem-4lF4aP1xnYwN4ASLKEeNbAYWBS4Weu-NCXEe37SrJ0ny3X3XLmf9GK8mG7t_eFrVi50SLp18ly4h8zmMm628A4Dn0YSrl1BG75DIpt1U1G2d51IRaV7V_S9Gu3MKfhJEgkzUDFOoXUGaOgcZqgWBJcELAw5Uim42dYiYcQ8KV19FwJFsz6eBenYl-Mf1YK2dUPdaXETrGarYJzgVtz_xxySgoPXbjokWK9HLk4GJ7HSaPRBHqvjW6yrctZh20CNbLi3t5-Ih3e2kraet-xX80Ht4myASJvCo3d9S4aIH6JDOF5mrcEE1HLkIMaW40KkGC5atakEGTxq18H2j7DRBBbsc_jH7SFQBO3zFEll_ts2qrdcd83dVRDxSzG2dIAEE28uF2UsvBG6qOsG4so5JryLYi9HfnGXBUNy0vIY3Cewlep8_XshLclWu1KTCxOxmHWL9_R4fsoIjuZ16hXFgHvq4ezpqvO0wQFpN9e9Pm_yQq1ooSu1VQHwys3-Wp82_67WmjAxb4Eyu6fS8ggOpxi5vbewckoHzc7QMSkM8-KLw9za0xWRo_u0D6l-bBBWSpYUjXrBQ1lX1l5aEUhsQP6MyDJ9VHtaCBEl1Yq55Yr5q1T2sTGGqhsjvQdrkqch8dves1uWtw2OzxmK7Ts-kf_JzlFRndCanJaEHBc4oZE2HSUjKpfWl1qO5U1ThnqZut-G-B4I3ZTkWLW_At2yEc3dySZn3T2uHOHFDhm8iCZzU8dgN38AJkt4-VhHDFmslLPzp6p_rdDtFOwe5nza-iIK6ILy0JaLuRBgz6MRkGG811wIOgkd-UmGFUGFho6V4VodcXvHHi3FeSPAa7n-JURhxvmgMld7UWlUKgXD39BRRSN06yhiBMn48KBk0MPKXbJ5gVEQrnnLTv2-kRk0b2EChGNBc9zER87MoWGWhKO8iN8QKkofsHBGIdQpFMPDeDl_yE_AyYuL09hf1KkUmCQLjsMY0dTiHfFVk0ScaXoGBUGNv2zQ72Xu0xsqaHmVk30eN_YopsYl0B5irePN5hQZlUworIeKlX8hrFm7lKDJsqx393HnO0Kxx2sa2wML9H9D4gmPzTaP9Ld4MpalmS5of-F8lPzdMNkg5Tx6KoTVIBwmHvi0OTly6j8jz3mM71QmGFvypMGoxB2A9Cg_ufiVLUVcLLkT75jPls8rfizqJngJfi9PtIARE9nXufEY7mvu23KNwFNtSyXCzcf5hbE3E_RatGl9dl6sHlyTbyGuFJW7RP3dDr3pDMuU84Ami-1iXzE8UQGodRoK19eKHbfUfjX2oKsE8zGC4Lyb82OATkvUt0BjVI4m2vIWstZDr3fCw9cjOZ9sRlYfsylBy_Ehs-kNxu-kxe-UNtXwd_aFJOHTvlJ0T0usFODlZt9x8grY7aFvvv3ZXYohz2lqApDE_ZWQFx69I5a6D0GCv0SF8I9Y4Hec3cpZ83HYMl0NWW9b7ngWVu2uNTFGVQkrIy3xU10RWXK7ovOI_19io2yrBqwyekLiZYm6nhA3O7ma7uRDqZwl6o25JfZU_bybWPtfyAvpW2kIBn29Ie0HVpX1INLtY-A6wx_5fk9ebRk0xdYu2zPKUKmVtbtJlX9NYBiQEJVHy2Q1kekDDOzpmfV5nSrk7y7s4iiLv1W8EJSys1POCb9y7G7yDPZF0YPJzOEwC-VUsn_6ORlhzmUSrBa0rzQfv0YCMGjoW_IFT7tnXYukoOEpnxT5FMSciA7MfgB3c5QMNifehPOCWC9tdijG6Ni6xWJrJS253k4EpPatpZ36cTWhUjrgdJtEN7jOdeD30T6bzWCVQ9Hiziet5HjM8LE_dFn-2I2E4vWfYPsVJQVADpEwhryhgQPRf93X4VQV8ZWDbhGB8bsWEtqJbzBUcuwJlJaroH7TsYrHfJnfZeUCD7IWojtFtDflh8h9zK5e13p43CYP3VynsGfrv9yZu0URl9OtlMtp3sdfBmg0ULi1o6_vk2Y05XVyeplcAFS7MTVkfDE8dykuqwl47wtxi46_wRSTc84pxHXZaSftoNS5o23PHtFNEoTTH9ToLTxI2-iBRTdyrirdTA68M2eIizV1AcKAeNmziNgcZZVyAPT5VL-RmBTHpTydZlSWinGmQOdBlCr0umhK3olS8xmzZN1VDSZcUN8DK3NQkpwr0sjmqE2r5BeTeVARrTxc25G8CRFIe2AgKnEGWZBeB0q9oObqZDRUvxg8FsKIBriJg7q-Fo86I4s4PoIt5dUCO5eUaog_GVn1K-vQGp7p1_86oozOExHmx6C2HeImp0zq8BDpt7bptx6x2bihulgja2PiTLm1oZC_udae6MUm7ifWqFF00U-WHyLJde-7eb_sMOA1js8yAJ7wMSldvsTNxzy_lBisMV7nqTo5MT9VQFA7FcZ93SIu3lInSICHHE56Q7vAQdKNgGw2yjsi0HznTOgw-6szD_1e92UH3by5m1vZqMe_8IBDQ2-BoizJlzMEuuooDXm1lz6cjj0VFzyj8qizDViTpcyGnZfxYePh8KOt1PmO8F17TzVGSuHtXfbh2uiD2YlPhZWXDgy1knTMt0FU1-Arm0La9gxKcv5I9MxTu5fCxUzBMnksBkTMw2O-UKwmzxVzYJzcTmwjXG-4TliX8zjiR568QiAtkuiP5Kcj5aR4NN4C8SOu0pwmCOfscuXRRVHjhdqNcdHR7idu71Bw8C6-OnjEz9pexjU_UU5AmN3NLKuaMYFTiah0wwgiWGKGU-fsbZnkkPgRML-zDSZhSoHMWpS8_Z-PUucy5exToRoDLIQKaT14upqCvm6vNxh6zxnFBm86AE4b6_e5c3qgxshzzlDVtZRMx0aQcFDbJeuq2DZJ6m8CqwhRBm4bID0i_zqsWAN23qBMgrt3VeSoIMLjrruf_9FQqfDk56LjjnHgx8GJbDveJ8QQYlA7NMpuCW4tPD61QeQQatvex0DDP9I7NzffX7Bv_inw3XXS7fvcn5AeTJ7Pn3ahn26onrajG9xS6zTZlfPteMQ_1rYuhUNBRXSbSDLKCdtCAA9ZpWMtnaojIm_3XNsKxOSSaXBXCsprKP_n82AanltdQOvmhKhAf_Uk2lQnNrTEvHdUxnUNaR98Jh6p8NxfwB9gsZWsafd3vAFfd9N7E0SqFpnZ19MS_HIsaOyJL7hmf-OCVo8klc76nOFEXx6WN1Hsppgk16N8N4PubYnhZZAsIlcmuznQdbHLyVR3xLuKV-9URDKlcjKmWy-qpzz2DQD9j-a_8ynS6kjiMB7iNJXiVhD7yVMqUkpjgdJmofQEi4jutS4Ul60k6ghYiC5zVQ2aBlmFgMcjVTjMXufJLr9nJEYRz1KTnjp3h9TQS8K61fG3MZOY04Qrx-Xmw63nZfutP6kjnW__5WNudHOrLs0k0XkMnT6BkWb7zz2mBxzqgyNRIkKYqZkBFtL3p2rZJtlUe2FxMX_-FOeIx2b-vG7wdd8lLLTLU0l_8h3VSYn75oN6MkwEikdTHod1qR_A0xXhgCu4mNbCPnU7hi97kDgEs-Tqc-yLp-lvspLo4Yv5yEBS-RJhiTz_Ashysvj_EDiOPrnGF5ZEVrrRHtzDD8_mFclFgP-fxtypcdffxlcN72wni7G1M-XXFQ2GnyVquvbpxiCTVRfipj46J-j1qIVl2_nXJXT87UlJkS-9_nAB3ByvAZhvgN_dZABhDuYYypU4elClW8F7iaREeEdZILTHzOrUQMLcCU5UFEkKhyGUpRaLOz-xekmGPnbP7I2lHBVF-9tQ2xK0PqBI1d161PBZNMRdd6BD3HlomcjOU-WIBs1NJ3iNQsbRbjTjhiOtjsfyNxI_G6Tfv_KmEDIPjc_NH_s5DwXNnntHT-vaflkwJxtLBU5wmJV8KutppVMEWUMTVMi6w7sUjZHWsIEB_-2W8OU7XjvFkr33MrZ1mtfw9N75KHMcZZHyTdniVWDCd5txZsW3mXlTG2sdcCd7hkVxrBTpdj6gR686JgqiExYjdHViqpsiBrdsRvFHahxbEGFxNRyvjekPwBpN695qGWU_Xu5_-C-9K1uJcYE5cqfQKseQuTKdLfjBo47vhs6JOgV0wz49NIgfTlA4FVqU3_v478bySVmc8Fl5Sw-t17hpjYQi-o24SVRTA-AmZcRsSP2Ltzb5nt0TimXooZQEtlrofRb5PHcfNRfgE_9of2pWQp7PSpSx6K-i1xt6TPdJH3UTd6bFpictjS2pxaBUCrhQcVVFTjL7fLN6Q5GthyAijsgevnm2trbsR4455_Rr-hMDj2DRQqn8LdlKONWzaKSdXJGxoZg8S8nFhT9F4bBo9Xwww6DpQdRUPkJl4rai3t7srNiHcHjWl4UTpFClN3EkkJt7b04f3bedbttDWC2WqA7d1I7vxwtpIM4Azir7pxo9rZKXwFVt4DAKGxNxYDLnsAYI7DA-r0NVDw3eeBzo87EsaTZPQ6-AWOSOGVT4UZRuIktmrlo-_OoHxKU-42Z7IVleCkMuwwbX6_pjPUo1k0n3S2EbF3om6yaxPXyWeSE365Y-VQARDKoS7rFZTlFLO1j5pHEZzEJoykJV_tuKpElIF1kPAuZSI-0gC99nNU4IW-ou-Q0ffAkzuYSJg4TmV3SUG6K4SBM917nQe3lvOKLY76KiMOF-OsmAMvAyURA3Qc7CJ6UYtkXRaf0tyaCOewKDnadscEfDxB9bLCXIVzTWGBnXnl0M4N5tfAR7B3TBG4qRyA5e53Sj0-S0eG5BRK5YsdVce2wNWqPFPS9ZQusxz4izUKkVTS6CMMkkgDHZtXZt0ydp-fLeGSetHAIq9vFueUJvwZWaFf5uQiIEvV5SWVMw6iMFvoyEh4VCVFmcWbp_aRB8TFAuWQmNjBjGo-f0t9YoX5vVv5igagfXapITq1q65f6E8RlRoXvVsEkF4YHErBhPTuDGrj7IxmoDt7KSL4QOHg5QM0pIOIg3vNbpOaGzI4QU_SZukN6o-7vZr2veOeGZKBKxH2ijpn7l3pnij-tYsYDe1NdGN-iuk0UXfVjsOrWZO0LSHh5AVOzBvWYF1Oy8qzhHu1iWAod9o902g3Hbx8ZI2wG8a0zWEAgn07BisUlF2390ogMMnKJ3zO3dDzmiEtvwaLc2M9RjS4wgwD4DoY7yYXGVxkf-kwCMweRvs-SmQq9V89U9OygvO8i0cG152Sm1oWZJuf7W1g0OZHW2g6uJ1q1P_mfxOZJ04uH8nT6xiUp8q0DGEElN4USR9p6BeM8QDbenNdE0wG8ZWES28eDok23nn6V5f_Xzv4ZI2wG8a0kW28gFLI6Ny-D2BK8HUKPX2QYOg4o-1HlXAU39q8p24s4nO8bv3dSFnqo8S_U_kdWcNmBD-9auZJmYck4oGcJuudGYDG8KkxtB2Daqv155Pl4AsgL3qgEXoaYKaD8nR4g2AiuI4q25H4QHCZztc54oKHfw375Y9CBfwXC7Y35YIuGgjZlfd2A00q2DF3ENOJ93LE4o88edly8X2Z2OyCp_EfT14W7A09Z0JFrPbr4oK0fxWATGYWYOoFZVdihWYF0SRxr5Q6Hq0m-z1NnqV29UcFX4g-V7KPc6mSQ5onnqU06nl6ULKIcGIQ2Oe5Ylv0LKP6eJBsv5M5ompJnB5U4pGrRXCY6QEHzhuYGGgZ_F5X4HY0JhHyNAexdm28m3J33hub5Z2oC8ZOER8RVC8iODHX5K2nG-WzeHLm4Bkvtj28s2XlxPR0_DIx9P-a9OoH3wbsI94daEZqz-kee_VBB7_zoMu3itMPV08GtV8UoTnWozjzV__JbbpABTWHOoDN-siXdeXDr-ioNc7rxIdgSRaNIHltflYJ9o5jP5ZVnA1_CBNQ_fUaHFafx5ugdn-8vE3Djau91PedZAPm0_bDlDAN75c-HPUD8CukjBbNhAlOMg2sH33sHC5kW_cCwFQdQB9kX3qtPoGm7hO2wHtgE-Aha0MLhkdIePBBS0UKQLiLfsHv3ATNIUOPmgg0bsJo1ca1WJmgTUPDf5Ox6kuukjistrRYPG_vOHNkvLvF1tKSsAUs-hiPB3U9JzPIvLkEfzOifnbtMrRWFztIR-L2_3k7TxXHCZrEsXna21MAyJHMDMlSOBSqK8-azlp7J555bZ-jdV-ql6vd-d9AoLsQH1BrRiuNvPQLj3FD8S5Z6idTbbJJvhCILcRgtFgR7zRN_rWSdS-jzZA1cTZn08grECttJIr73IMCoh6XxPFA_ILCZd7F7ebDOnQfHRF4hwTtn7y32uqa5iOWIkJRgptZcHS8uWb9YCD5JMIs5Sv4LFl7SoPMMFjqiezkp_dG6beaNocb8ynKBjxQ7yXW8EdUj87_MQzFbn9sXMi7CbTwiY1rtWWSEqgwhr-knslgqSIMoPEXAwNLNhdj9jD6URd6w0lS9VYvEeUIT4YgGcq-RckKwZFfxpNlijgFPLv52Dxg6cNS6rnAU3Gzc4a-M-oiQofkKKE6K0yMGdq8QTe0njCKmTn0A5cR8YjhqRAbvj7jghuYRIXzAr6vr9QpPoYkEKivXQJxSlU5KJ2fjcm-EWC5iz5SAsfemFNKMNJbs0GfnxKpBTgZ8vw5XBDXQSxRkNy7K-Zj8nDmcKjvWk57_NRydYhfCajtuFKbwZbn_faCNlGahSk1Jtpsy2d_pVWQ3l_bfWBzzpNNGkiXLlhEZhK-DGMqbhov6wv3NqpQzf8xtOlTLxVvtUAs2wRS3K1rHK1qic8e34gf-vYyKPVMpkGPbOtusaJEpwj_cIDPURjmiejMlke72ATedKPIDPIGjfSqwclFbh70Exn26UQUUXVBTxr3hfO4lUb-ISEjxq5-nwRijvj8tst2ZDsMYXzlyoLhN65k1EcH-hNc4qpTfRpGUBB5kDmqPZwsInocdQBk55EPF7pRTZyJZei9Pqu1zGQGn4VGk6DlqFxOb7qV4zff1bw1MbiPyaPEM-ng-9GArcdpN4NG0LEls-ln7sooyjHRBqzw4CQ13wkD6X-i0KZYvJt5TIKLRZM1ZFMP88LOIOgo3-FMCbGGPQWadQ_rIjLMQODiHz6c3nVm3xEdInJLnNWcszjAY8xqPN72iACdeIEMMPMIWMYhDP1FPifW5LfV49GBREALAHkv2Mo21SwmmFmtqrpMb6C31oTUubMgjsMfqk6uphYg5UxBKPacjzjOrGQi7aB2HwXT9cY-JdmpuW3oxuFEvnRNnG5Zvaovy7XvorxsFk4xRjFzrNaY7Xtn5Lpkn4oVzxOla6NJLzUH8rjoUP73htEFJsvD67ENQtezPeau7v3VksFnIVftdLQk_TB_gLTBlSLdgpo8jiGUdLsikCxDzTIQTQYKzVyuYLr0p4bpqN49gvB3JazRbiuO7QjDpHIKQqFEZqigVLnFgXwNPF2u3KGgDaJDACOzNOz_hr9KnFukUEmfG9-hxefBwdetPWvGIUklObSjcq3OZanrbVL2VHRV3joROW1jAoJaDDLH9Vxu9vw1TiwIHtdUfCsWszaWb_cxI7IcX8f9qy_XQUyrf_V_u3lFbV9sxYuUOExOJBvV0X_4oViFORXUZERI3mEJHtpqmXvVBQkpuzAr6TQvZsPaGIjGU9UjBCEtkPfcMA6sJWgcrvv7htbYVLX4K4tKfoUoXWedYvaZXsjssuiwiPcUllb5hTPw36xQSCZXp9tNxhNTGW8j_feu517CrLArceUcQVXmvKENiazuIFvcqaMfeVq_KZB76iG91ljbGrgOlf6ylEdGUvc9Z6H-vZRtYEx6bk02ClztdugIX26BHsiW9PU0oNXiMVSirbBgVQqJ1vUCG2UNlsacbkFLtKisXRU_dwMWRNy9fkuB4DV5-Z3YK-bWhu1LG7roCAnO3w_1GBkPieqZhaqqelUug5x6XdKArSv-rgS44xRbrqysmisWv1isExE8HqtM1GilXtlgGDAVc5vvEkQ_9z4z8p6BH5cbSZz-tz1jNGvCoSVmN7wM_Qpm9vmwUpHpPM6BChCR06Lf_qWxhbFcSrj8zMjxSxCCShBTdVnKs-fRz0x7mmcrTbgQLn49EjYr3RWb1Q_-qxIvPGDW83wzAUi3b_K_4TwHgTcERwRt4LUKndAswtnIjoyXBgPs7z-RgG-PpCZ109sg7fRHVLYS_60fPbutXkBhUKL9jTpqRSsD-xjbhvdUtiUGGV83TICxfGv2ZGhWEASuThGgnUgZt77hJ63qCnzerjkpbLyFdfKHEvpBSVOvgRdzX_aUavh8OE2kqT10ntTahcIbR_VJ1l0vcxx2UFzZK_lNW9hR5KGtR2v7FfOU78-1BuM-Sc6PySwwWMWImzVRxTsFbHwmzWj9qsQh00IcMMLbKzw3EhPiYqZWxvr9HTKkIVq2SLsMH_3y0
\ No newline at end of file
diff --git a/docs/logical_data_model.puml b/docs/logical_data_model.puml
index 60ec7c1745..e56801df94 100644
--- a/docs/logical_data_model.puml
+++ b/docs/logical_data_model.puml
@@ -88,17 +88,10 @@ class ActivityReportGoals{
class ActivityReportObjectiveCitations{
* id : integer :
- * activityReportObjectiveId : integer : REFERENCES "ActivityReportObjectives".id
-!issue='column should not allow null'
-!issue='column missing from model'
-!issue='column reference missing' * citationId: integer : REFERENCES "MonitoringStandards".id
-!issue='column should not allow null'
-!issue='column missing from model'
-!issue='column reference missing' * findingId: integer : REFERENCES "MonitoringFindings".id
-!issue='column should not allow null'
-!issue='column missing from model'
-!issue='column reference missing' * reviewId: integer : REFERENCES "MonitoringReviews".id
+ * activityReportObjectiveId : integer
+ * citation : text
* createdAt : timestamp with time zone : now()
+ * monitoringReferences : jsonb
* updatedAt : timestamp with time zone : now()
}
@@ -419,7 +412,6 @@ class GoalTemplates{
* updatedAt : timestamp with time zone
lastUsed : timestamp with time zone
source : varchar(255)
-!issue='column missing from model' standard: text
}
class Goals{
@@ -558,7 +550,6 @@ class Imports{
* updatedAt : timestamp with time zone
fileMask : text
path : text
-!issue='column missing from model' postProcessingActions: jsonb
}
class MailerLogs{
@@ -2683,11 +2674,4 @@ Roles "n" }--[#black,dotted,thickness=2]--{ "n" Topics : topics, roles
Roles "n" }--[#black,dotted,thickness=2]--{ "n" Users : users, roles
Scopes "n" }--[#black,dotted,thickness=2]--{ "n" Users : users, scopes
-!issue='association missing from models'!issue='associations need to be defined both directions'
-MonitoringFindings o--[#yellow,bold,thickness=2]--o ActivityReportObjectiveCitations : missing-from-model
-!issue='associations need to be defined both directions'
-MonitoringReviews o--[#yellow,bold,thickness=2]--o ActivityReportObjectiveCitations : missing-from-model
-!issue='associations need to be defined both directions'
-MonitoringStandards o--[#yellow,bold,thickness=2]--o ActivityReportObjectiveCitations : missing-from-model
-
@enduml
From 730617c8dca70f975c49a0a73718ed9f97e54170 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Tue, 26 Nov 2024 12:10:29 -0500
Subject: [PATCH 033/198] hook up more citation stuff for ARs
---
...veAssociationsFromActivityReportObjectives.ts | 9 +++++++--
src/goalServices/goals.js | 9 +++++----
src/goalServices/reduceGoals.ts | 16 ++++++++++++++++
src/goalServices/types.ts | 15 +++++++++++++++
4 files changed, 43 insertions(+), 6 deletions(-)
diff --git a/src/goalServices/extractObjectiveAssociationsFromActivityReportObjectives.ts b/src/goalServices/extractObjectiveAssociationsFromActivityReportObjectives.ts
index b977533326..6a7b298edd 100644
--- a/src/goalServices/extractObjectiveAssociationsFromActivityReportObjectives.ts
+++ b/src/goalServices/extractObjectiveAssociationsFromActivityReportObjectives.ts
@@ -4,6 +4,7 @@ import {
IResourceModelInstance,
ITopicModelInstance,
ICourseModelInstance,
+ ICitationModelInstance,
} from './types';
/**
@@ -14,9 +15,13 @@ import {
*/
export default function extractObjectiveAssociationsFromActivityReportObjectives(
activityReportObjectives: IActivityReportObjectivesModelInstance[],
- associationName: 'topics' | 'resources' | 'files' | 'courses',
+ associationName: 'topics' | 'resources' | 'files' | 'courses' | 'activityReportObjectiveCitations',
) {
return activityReportObjectives.map((aro) => aro[associationName].map((
- a: ITopicModelInstance | IResourceModelInstance | IFileModelInstance | ICourseModelInstance,
+ a: ITopicModelInstance |
+ IResourceModelInstance |
+ IFileModelInstance |
+ ICourseModelInstance |
+ ICitationModelInstance,
) => a.toJSON())).flat();
}
diff --git a/src/goalServices/goals.js b/src/goalServices/goals.js
index fd1c0d6c17..6755b227df 100644
--- a/src/goalServices/goals.js
+++ b/src/goalServices/goals.js
@@ -201,11 +201,8 @@ export async function goalsByIdsAndActivityReport(id, activityReportId) {
},
{
model: ActivityReportObjectiveCitation,
- as: 'citations',
+ as: 'activityReportObjectiveCitations',
attributes: ['citation', 'monitoringReferences'],
- through: {
- attributes: [],
- },
},
{
model: Resource,
@@ -311,6 +308,10 @@ export async function goalsByIdsAndActivityReport(id, activityReportId) {
objective.activityReportObjectives,
'files',
),
+ citations: extractObjectiveAssociationsFromActivityReportObjectives(
+ objective.activityReportObjectives,
+ 'activityReportObjectiveCitations',
+ ),
})),
}));
diff --git a/src/goalServices/reduceGoals.ts b/src/goalServices/reduceGoals.ts
index 8e8a0d3dda..2638dd8ed0 100644
--- a/src/goalServices/reduceGoals.ts
+++ b/src/goalServices/reduceGoals.ts
@@ -179,6 +179,13 @@ export function reduceObjectivesForActivityReport(
exists,
);
+ exists.citations = uniq(objective.activityReportObjectives
+ && objective.activityReportObjectives.length > 0
+ ? objective.activityReportObjectives[0].activityReportObjectiveCitations.map(
+ (c) => c.citation,
+ )
+ : []);
+
exists.files = uniqBy([
...exists.files,
...(objective.activityReportObjectives
@@ -187,6 +194,7 @@ export function reduceObjectivesForActivityReport(
.map((f) => ({ ...f.file.dataValues, url: f.file.url }))
: []),
], (e: IFile) => e.key);
+
return objectives;
}
@@ -253,6 +261,14 @@ export function reduceObjectivesForActivityReport(
'activityReportObjectiveCourses',
'course',
),
+ citations: uniq(
+ objective.activityReportObjectives
+ && objective.activityReportObjectives.length > 0
+ ? objective.activityReportObjectives[0].activityReportObjectiveCitations.map(
+ (c) => c.citation,
+ )
+ : [],
+ ),
}];
}, currentObjectives);
diff --git a/src/goalServices/types.ts b/src/goalServices/types.ts
index 8afa675ddb..120bd058f7 100644
--- a/src/goalServices/types.ts
+++ b/src/goalServices/types.ts
@@ -67,11 +67,21 @@ interface ICourse {
name: string;
}
+interface ICitation {
+ citation: string;
+ monitoringReferences: JSON;
+}
+
interface ICourseModelInstance extends ICourse {
dataValues?: ICourse;
toJSON?: () => ICourse;
}
+interface ICitationModelInstance extends ICourse {
+ dataValues?: ICitation;
+ toJSON?: () => ICitation;
+}
+
interface IActivityReportObjective {
id: number;
objectiveId: number;
@@ -102,6 +112,9 @@ interface IActivityReportObjective {
activityReportObjectiveCourses: {
course: ICourse;
}[];
+ activityReportObjectiveCitations: {
+ citation: ICitation;
+ }[];
}
interface IActivityReportObjectivesModelInstance extends IActivityReportObjective {
@@ -339,6 +352,7 @@ export {
IActivityReportObjective,
IObjective,
IGoal,
+ ICitation,
// -- model version of the above -- //
IGoalModelInstance,
IGrantModelInstance,
@@ -348,6 +362,7 @@ export {
IFileModelInstance,
IObjectiveModelInstance,
IActivityReportObjectivesModelInstance,
+ ICitationModelInstance,
// -- after going through reduceGoals -- //
IReducedObjective,
IReducedGoal,
From 8db40d324314b6e110bfdab1c88f1726ee5c2e31 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Tue, 26 Nov 2024 15:00:11 -0500
Subject: [PATCH 034/198] remove unused items
---
src/migrations/20241115143738-AddMonitoringEnum.js | 1 -
.../20241115203616-add-post-processing-to-import.js | 5 +----
src/tools/createMonitoringGoals.js | 2 +-
3 files changed, 2 insertions(+), 6 deletions(-)
diff --git a/src/migrations/20241115143738-AddMonitoringEnum.js b/src/migrations/20241115143738-AddMonitoringEnum.js
index 31cb2a0dc3..769f957e7c 100644
--- a/src/migrations/20241115143738-AddMonitoringEnum.js
+++ b/src/migrations/20241115143738-AddMonitoringEnum.js
@@ -1,4 +1,3 @@
-const { GOAL_CREATED_VIA } = require('../constants');
const { prepMigration } = require('../lib/migration');
/** @type {import('sequelize-cli').Migration} */
diff --git a/src/migrations/20241115203616-add-post-processing-to-import.js b/src/migrations/20241115203616-add-post-processing-to-import.js
index 14a65e9190..b875fffe5e 100644
--- a/src/migrations/20241115203616-add-post-processing-to-import.js
+++ b/src/migrations/20241115203616-add-post-processing-to-import.js
@@ -1,9 +1,6 @@
-const { GROUP_SHARED_WITH } = require('@ttahub/common');
const {
prepMigration,
- removeTables,
} = require('../lib/migration');
-const { GROUP_COLLABORATORS } = require('../constants');
/** @type {import('sequelize-cli').Migration} */
module.exports = {
@@ -32,7 +29,7 @@ module.exports = {
});
},
- down: async (queryInterface, Sequelize) => {
+ down: async (queryInterface) => {
await queryInterface.sequelize.transaction(async (transaction) => {
const sessionSig = __filename;
await prepMigration(queryInterface, transaction, sessionSig);
diff --git a/src/tools/createMonitoringGoals.js b/src/tools/createMonitoringGoals.js
index c5e536fc51..18bf07586b 100644
--- a/src/tools/createMonitoringGoals.js
+++ b/src/tools/createMonitoringGoals.js
@@ -5,7 +5,7 @@ import {
import { auditLogger } from '../logger';
const createMonitoringGoals = async () => {
- const cutOffDate = '2024-10-01'; // TODO: Set this before we deploy to prod.
+ const cutOffDate = '2024-11-26'; // TODO: Set this before we deploy to prod.
// Verify that the monitoring goal template exists.
const monitoringGoalTemplate = await GoalTemplate.findOne({
From 937e01ef6d889610d1dffd3408cd4ec1c7a82c8d Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Wed, 27 Nov 2024 15:05:06 -0500
Subject: [PATCH 035/198] updates for grant goal retrieval
---
src/goalServices/goals.js | 23 +-
src/goalServices/goals.test.js | 41 +++
src/routes/activityReports/handlers.js | 3 +-
src/services/citations.js | 78 +++-
src/services/citations.test.js | 486 +++++++++++++++----------
5 files changed, 429 insertions(+), 202 deletions(-)
diff --git a/src/goalServices/goals.js b/src/goalServices/goals.js
index 6755b227df..67190652a7 100644
--- a/src/goalServices/goals.js
+++ b/src/goalServices/goals.js
@@ -40,6 +40,7 @@ import {
destroyActivityReportObjectiveMetadata,
} from '../services/reportCache';
import { setFieldPromptsForCuratedTemplate } from '../services/goalTemplates';
+import { getMonitoringGoals } from '../services/citations';
import { auditLogger } from '../logger';
import {
mergeCollaborators,
@@ -691,7 +692,7 @@ export async function createOrUpdateGoals(goals) {
return goalsByIdAndRecipient(goalIds, recipient);
}
-export async function goalsForGrants(grantIds) {
+export async function goalsForGrants(grantIds, reportStartDate, user = null) {
/**
* get all the matching grants
*/
@@ -734,10 +735,10 @@ export async function goalsForGrants(grantIds) {
.filter((g) => g)));
/*
- * finally, return all matching goals
+ * Get all matching goals
*/
- return Goal.findAll({
+ const regularGoals = Goal.findAll({
attributes: [
[sequelize.fn(
'ARRAY_AGG',
@@ -835,6 +836,22 @@ export async function goalsForGrants(grantIds) {
),
), 'desc']],
});
+
+ /*
+ * Get all monitoring goals
+ */
+ let goalsToReturn = [regularGoals];
+ const hasGoalMonitoringOverride = !!(user && new Users(user).canSeeBehindFeatureFlag('monitoring_integration'));
+ if (hasGoalMonitoringOverride) {
+ const monitoringGoals = await getMonitoringGoals(ids, reportStartDate);
+
+ // Combine goalsToReturn with monitoringGoals.
+ const allGoals = await Promise.all([regularGoals, monitoringGoals]);
+
+ // Flatten the array of arrays.
+ goalsToReturn = allGoals.flat();
+ }
+ return goalsToReturn;
}
async function removeActivityReportObjectivesFromReport(reportId, objectiveIdsToRemove) {
diff --git a/src/goalServices/goals.test.js b/src/goalServices/goals.test.js
index f92f1cce65..435077461c 100644
--- a/src/goalServices/goals.test.js
+++ b/src/goalServices/goals.test.js
@@ -50,11 +50,15 @@ import {
import {
mergeCollaborators,
} from '../models/helpers/genericCollaborator';
+import { getMonitoringGoals } from '../services/citations';
jest.mock('./changeGoalStatus', () => ({
__esModule: true,
default: jest.fn(),
}));
+
+jest.mock('../services/citations');
+
jest.mock('./wasGoalPreviouslyClosed');
jest.mock('./extractObjectiveAssociationsFromActivityReportObjectives');
jest.mock('./reduceGoals');
@@ -1521,6 +1525,43 @@ describe('Goals DB service', () => {
506,
]);
});
+
+ it('does not return monitoring goals if the user is missing the feature flag', async () => {
+ Grant.findAll = jest.fn();
+ Grant.findAll.mockResolvedValue([{ id: 505, oldGrantId: 506 }]);
+ Goal.findAll = jest.fn();
+ Goal.findAll.mockResolvedValue([{ id: 505 }, { id: 506 }]);
+
+ await goalsForGrants([506]);
+
+ const { where } = Goal.findAll.mock.calls[0][0];
+ expect(where['$grant.id$']).toStrictEqual([
+ 505,
+ 506,
+ ]);
+ });
+
+ it('returns monitoring goals if the user has the feature flag', async () => {
+ Grant.findAll = jest.fn();
+ Grant.findAll.mockResolvedValue([{ id: 505, oldGrantId: 506 }]);
+ Goal.findAll = jest.fn();
+ Goal.findAll.mockResolvedValue([{ id: 505 }, { id: 506 }]);
+
+ // Mock getMonitoringGoals to return a list of monitoring goals.
+ getMonitoringGoals.mockResolvedValue([{ id: 507 }]);
+
+ // Mock the feature flag function canSeeBehindFeatureFlag in user to return true.
+ const result = await goalsForGrants([506], '2024-11-27', {
+ flags: ['monitoring_integration'],
+ });
+
+ // Assert result contains the goals we expect including the monitoring goal.
+ expect(result).toEqual([
+ { id: 505 },
+ { id: 506 },
+ { id: 507 },
+ ]);
+ });
});
describe('createMultiRecipientGoalsFromAdmin', () => {
diff --git a/src/routes/activityReports/handlers.js b/src/routes/activityReports/handlers.js
index 7b6189879d..b88987bae2 100644
--- a/src/routes/activityReports/handlers.js
+++ b/src/routes/activityReports/handlers.js
@@ -294,7 +294,8 @@ export async function getLegacyReport(req, res) {
export async function getGoals(req, res) {
try {
const { grantIds } = req.query;
- const goals = await goalsForGrants(grantIds);
+ const { reportStartDate } = req.params;
+ const goals = await goalsForGrants(grantIds, reportStartDate);
res.json(goals);
} catch (error) {
await handleErrors(req, res, error, logContext);
diff --git a/src/services/citations.js b/src/services/citations.js
index 9518cd8415..7ea148201d 100644
--- a/src/services/citations.js
+++ b/src/services/citations.js
@@ -2,6 +2,7 @@
/* eslint-disable import/prefer-default-export */
import { sequelize } from '../models';
+const cutOffStartDate = '2024-10-01'; // TODO: Set this before we deploy to prod.
/*
The purpose of this function is to get citations by grant id.
We then need to format the response for how it needs to be
@@ -13,8 +14,6 @@ export async function getCitationsByGrantIds(grantIds, reportStartDate) {
- Do we need to take into account the grant replacements table? (what if a grant was replaced?)
- Is it enough to join on grant number? Or do we need to use links table?
*/
- const cutOffStartDate = '2024-10-01'; // TODO: Set this before we deploy to prod.
-
// Query to get the citations by grant id.
const grantsByCitations = await sequelize.query(
/* sql */
@@ -78,3 +77,78 @@ export async function getCitationsByGrantIds(grantIds, reportStartDate) {
return grantsByCitations[0];
}
+
+/* Get the monitoring goals for the given grant ids and report start date */
+/* We need to produce the same objects that come for the goals endpoint */
+export async function getMonitoringGoals(grantIds, reportStartDate) {
+ const monitoringGoals = await sequelize.query(
+ /* sql */
+ `SELECT
+
+ g.id,
+ g."name",
+ g."status",
+ g."endDate",
+ g."onApprovedAR",
+ g."source",
+ g."createdVia",
+
+ ARRAY_AGG(DISTINCT gr.id) AS "grantIds",
+ ARRAY_AGG(DISTINCT g.id) AS "goalIds",
+ ARRAY_AGG(DISTINCT grta."grantId") AS "oldGrantIds",
+ MAX(g."createdAt") AS created,
+ MAX(g."goalTemplateId") AS "goalTemplateId"
+
+ FROM "Grants" gr
+ JOIN "MonitoringReviewGrantees" mrg
+ ON gr.number = mrg."grantNumber"
+ JOIN "GrantRelationshipToActive" grta
+ ON gr.id = grta."grantId"
+ JOIN "Goals" g
+ ON gr.id = g."grantId"
+ JOIN "GoalTemplates" gt
+ ON g."goalTemplateId" = gt.id
+ JOIN (
+ -- The below 'DISTINCT ON' determines the single record to return values from by the 'ORDER BY' clause.
+ SELECT DISTINCT ON (mfh."findingId", gr.id)
+ mfh."findingId",
+ gr.id AS "grantId",
+ mr."reviewId",
+ mr."name",
+ mr."reportDeliveryDate"
+ FROM "MonitoringFindingHistories" mfh
+ JOIN "MonitoringReviews" mr
+ ON mfh."reviewId" = mr."reviewId"
+ JOIN "MonitoringReviewGrantees" mrg
+ ON mrg."reviewId" = mr."reviewId"
+ JOIN "Grants" gr
+ ON gr.number = mrg."grantNumber"
+ ORDER BY mfh."findingId", gr.id, mr."reportDeliveryDate" DESC
+ ) mfh -- Subquery ensures only the most recent history for each finding-grant combination
+ ON mfh."grantId" = gr.id
+ JOIN "MonitoringFindings" mf
+ ON mfh."findingId" = mf."findingId"
+ JOIN "MonitoringFindingStatuses" mfs
+ ON mf."statusId" = mfs."statusId"
+ JOIN "MonitoringFindingStandards" mfs2
+ ON mf."findingId" = mfs2."findingId"
+ JOIN "MonitoringStandards" ms
+ ON mfs2."standardId" = ms."standardId"
+ JOIN "MonitoringFindingGrants" mfg
+ ON mf."findingId" = mfg."findingId"
+ AND mrg."granteeId" = mfg."granteeId"
+ WHERE 1 = 1
+ AND gr.id IN (${grantIds.join(',')}) -- :grantIds
+ AND mfh."reportDeliveryDate"::date BETWEEN '${cutOffStartDate}' AND '${reportStartDate}'
+ AND gr.status = 'Active'
+ AND mfs.name = 'Active'
+ AND g."createdVia" = 'monitoring'
+ AND g.status NOT IN ('Closed', 'Suspended')
+ AND gt.standard = 'Monitoring'
+ AND g.name != ''
+ GROUP BY 1, 2, 3, 4, 5, 6, 7
+ ORDER BY 11 DESC;
+ `,
+ );
+ return monitoringGoals[0];
+}
diff --git a/src/services/citations.test.js b/src/services/citations.test.js
index f4d0ec29ef..660338729f 100644
--- a/src/services/citations.test.js
+++ b/src/services/citations.test.js
@@ -2,7 +2,7 @@
/* eslint-disable prefer-destructuring */
import { v4 as uuidv4 } from 'uuid';
import faker from '@faker-js/faker';
-import { getCitationsByGrantIds } from './citations';
+import { getCitationsByGrantIds, getMonitoringGoals } from './citations';
import db, {
Recipient,
Grant,
@@ -15,6 +15,9 @@ import db, {
MonitoringFindingStandard,
MonitoringStandard,
MonitoringFindingGrant,
+ GoalTemplate,
+ Goal,
+ GrantRelationshipToActive,
} from '../models';
import { captureSnapshot, rollbackToSnapshot } from '../lib/programmaticTransaction';
@@ -143,96 +146,160 @@ const createMonitoringData = async (
citable,
}, { individualHooks: true });
}));
+
+ // Refresh the materialized view.
+ await GrantRelationshipToActive.refresh();
};
describe('citations service', () => {
afterAll(async () => {
await db.sequelize.close();
});
- describe('getCitationsByGrantIds()', () => {
- let snapShot;
-
- let recipient1;
- let recipient2;
-
- let grant1; // Recipient 1
- let grant1a; // Recipient 1
- let grant2; // Recipient 2
- let grant3; // Recipient 2 (Inactive)
-
- beforeAll(async () => {
- // Capture a snapshot of the database before running the test.
- snapShot = await captureSnapshot();
-
- // Grant Numbers.
- const grantNumber1 = faker.datatype.string(8);
- const grantNumber1a = faker.datatype.string(8);
- const grantNumber2 = faker.datatype.string(8);
- const grantNumber3 = faker.datatype.string(8);
-
- // Recipients 1.
- recipient1 = await Recipient.create({
- id: faker.datatype.number({ min: 64000 }),
- name: faker.random.alphaNumeric(6),
- });
-
- // Recipients 2.
- recipient2 = await Recipient.create({
- id: faker.datatype.number({ min: 64000 }),
- name: faker.random.alphaNumeric(6),
- });
-
- // Grants.
- const grants = await Grant.bulkCreate([
- {
- // Grant 1 for Recipient 1.
- id: faker.datatype.number({ min: 9999 }),
- number: grantNumber1,
- recipientId: recipient1.id,
- regionId: 1,
- startDate: new Date(),
- endDate: new Date(),
- status: 'Active',
- },
- {
- // Grant 1a for Recipient 1.
- id: faker.datatype.number({ min: 9999 }),
- number: grantNumber1a,
- recipientId: recipient1.id,
- regionId: 1,
- startDate: new Date(),
- endDate: new Date(),
- status: 'Active',
- },
- {
- // Grant 2 for Recipient 2.
- id: faker.datatype.number({ min: 9999 }),
- number: grantNumber2,
- recipientId: recipient2.id,
- regionId: 1,
- startDate: new Date(),
- endDate: new Date(),
- status: 'Active',
- },
- {
- // Grant 3 for Recipient 2 (Inactive).
- id: faker.datatype.number({ min: 9999 }),
- number: grantNumber3,
- recipientId: recipient2.id,
- regionId: 1,
- startDate: new Date(),
- endDate: new Date(),
- status: 'Inactive',
- },
- ]);
-
- // set the grants.
- grant1 = grants[0];
- grant1a = grants[1];
- grant2 = grants[2];
- grant3 = grants[3];
-
- /*
+ let snapShot;
+
+ let recipient1;
+ let recipient2;
+
+ let grant1; // Recipient 1
+ let grant1a; // Recipient 1
+ let grant2; // Recipient 2
+ let grant3; // Recipient 2 (Inactive)
+
+ // Goals.
+ let monitoringGoal;
+ let grant1aMonitoringGoal;
+
+ beforeAll(async () => {
+ // Capture a snapshot of the database before running the test.
+ snapShot = await captureSnapshot();
+
+ // Get Monitoring Goal Template.
+ const monitoringGoalTemplate = await GoalTemplate.findOne({
+ where: {
+ standard: 'Monitoring',
+ },
+ });
+
+ // Grant Numbers.
+ const grantNumber1 = faker.datatype.string(8);
+ const grantNumber1a = faker.datatype.string(8);
+ const grantNumber2 = faker.datatype.string(8);
+ const grantNumber3 = faker.datatype.string(8);
+
+ // Recipients 1.
+ recipient1 = await Recipient.create({
+ id: faker.datatype.number({ min: 64000 }),
+ name: faker.random.alphaNumeric(6),
+ });
+
+ // Recipients 2.
+ recipient2 = await Recipient.create({
+ id: faker.datatype.number({ min: 64000 }),
+ name: faker.random.alphaNumeric(6),
+ });
+
+ // Grants.
+ const grants = await Grant.bulkCreate([
+ {
+ // Grant 1 for Recipient 1.
+ id: faker.datatype.number({ min: 9999 }),
+ number: grantNumber1,
+ recipientId: recipient1.id,
+ regionId: 1,
+ startDate: new Date(),
+ endDate: new Date(),
+ status: 'Active',
+ },
+ {
+ // Grant 1a for Recipient 1.
+ id: faker.datatype.number({ min: 9999 }),
+ number: grantNumber1a,
+ recipientId: recipient1.id,
+ regionId: 1,
+ startDate: new Date(),
+ endDate: new Date(),
+ status: 'Active',
+ },
+ {
+ // Grant 2 for Recipient 2.
+ id: faker.datatype.number({ min: 9999 }),
+ number: grantNumber2,
+ recipientId: recipient2.id,
+ regionId: 1,
+ startDate: new Date(),
+ endDate: new Date(),
+ status: 'Active',
+ },
+ {
+ // Grant 3 for Recipient 2 (Inactive).
+ id: faker.datatype.number({ min: 9999 }),
+ number: grantNumber3,
+ recipientId: recipient2.id,
+ regionId: 1,
+ startDate: new Date(),
+ endDate: new Date(),
+ status: 'Inactive',
+ },
+ ]);
+
+ // set the grants.
+ grant1 = grants[0];
+ grant1a = grants[1];
+ grant2 = grants[2];
+ grant3 = grants[3];
+
+ // Create Goals and Link them to Grants.
+ monitoringGoal = await Goal.create({
+ name: 'Monitoring Goal 1',
+ status: 'Not started',
+ timeframe: '12 months',
+ isFromSmartsheetTtaPlan: false,
+ grantId: grant1.id,
+ createdAt: '2024-11-26T19:16:15.842Z',
+ onApprovedAR: true,
+ createdVia: 'monitoring',
+ goalTemplateId: monitoringGoalTemplate.id,
+ });
+
+ // closed Monitoring Goal.
+ await Goal.create({
+ name: 'Monitoring Goal 2',
+ status: 'Closed',
+ timeframe: '12 months',
+ isFromSmartsheetTtaPlan: false,
+ grantId: grant1.id,
+ createdAt: '2024-11-26T19:16:15.842Z',
+ onApprovedAR: true,
+ createdVia: 'monitoring',
+ goalTemplateId: monitoringGoalTemplate.id,
+ });
+
+ // Regular Goal.
+ await Goal.create({
+ name: 'Regular Goal 1',
+ status: 'Not started',
+ timeframe: '12 months',
+ isFromSmartsheetTtaPlan: false,
+ grantId: grant1.id,
+ createdAt: '2024-11-26T19:16:15.842Z',
+ onApprovedAR: true,
+ createdVia: 'activityReport',
+ });
+
+ // Create monitoring goal for grant 2.
+ grant1aMonitoringGoal = await Goal.create({
+ name: 'Monitoring Goal 3',
+ status: 'Not started',
+ timeframe: '12 months',
+ isFromSmartsheetTtaPlan: false,
+ grantId: grant1a.id,
+ createdAt: '2024-11-26T19:16:15.842Z',
+ onApprovedAR: true,
+ createdVia: 'monitoring',
+ goalTemplateId: monitoringGoalTemplate.id,
+ });
+
+ /*
Citation Object Properties:
citationText, // Citation Text
monitoringFindingType, // Monitoring Finding ('Deficiency', 'Significant Deficiency', 'Material Weakness', 'No Finding').
@@ -240,118 +307,145 @@ describe('citations service', () => {
monitoringFindingGrantFindingType, // Monitoring Finding Grant Finding Type must be in ('Corrective Action', 'Management Decision', 'No Finding').
*/
- // Create Monitoring Review Citations.
- const grant1Citations1 = [
- {
- citationText: 'Grant 1 - Citation 1 - Good',
- monitoringFindingType: 'Citation 1 Monitoring Finding Type',
- monitoringFindingStatusName: 'Active',
- monitoringFindingGrantFindingType: 'Corrective Action',
- },
- {
- citationText: 'Grant 1 - Citation 2 - Bad MFS name',
- monitoringFindingType: 'Citation 2 Monitoring Finding Type',
- monitoringFindingStatusName: 'Abandoned',
- monitoringFindingGrantFindingType: 'Corrective Action',
- },
- {
- citationText: 'Grant 1 - Citation 3 - Good 2',
- monitoringFindingType: 'Citation 3 Monitoring Finding Type',
- monitoringFindingStatusName: 'Active',
- monitoringFindingGrantFindingType: 'Corrective Action',
- },
- ];
-
- // Grant 1.
- await createMonitoringData(grant1.number, 1, new Date(), 'AIAN-DEF', 'Complete', grant1Citations1);
-
- // Grant 1a (make sure other grant citations comeback).
- const grant1Citations1a = [
- {
- citationText: 'Grant 1a - Citation 1 - Good',
- monitoringFindingType: 'Citation 4 Monitoring Finding Type',
- monitoringFindingStatusName: 'Active',
- monitoringFindingGrantFindingType: 'Grant 1a Corrective Action',
- },
- ];
- await createMonitoringData(grant1a.number, 2, new Date(), 'AIAN-DEF', 'Complete', grant1Citations1a);
-
- // Grant 2.
- const grant1Citations2 = [
- {
- citationText: 'Grant 2 - Citation 1 - Good',
- monitoringFindingType: 'citation 5 Monitoring Finding Type',
- monitoringFindingStatusName: 'Active',
- monitoringFindingGrantFindingType: 'Corrective Action',
- },
- ];
- // Before delivery date.
- await createMonitoringData(grant2.number, 3, new Date('2024-09-01'), 'AIAN-DEF', 'Complete', grant1Citations2);
- // After delivery date (tomorrow).
- await createMonitoringData(grant2.number, 4, new Date(new Date().setDate(new Date().getDate() + 1)), 'AIAN-DEF', 'Complete', grant1Citations2);
-
- // Grant 3 (inactive).
- const grant1Citations3 = [
- {
- citationText: 'Grant 3 - Citation 1 - Good',
- monitoringFindingType: 'Material Weakness',
- monitoringFindingStatusName: 'Active',
- monitoringFindingGrantFindingType: 'Corrective Action',
- },
- ];
- await createMonitoringData(grant3.number, 5, new Date(), 'AIAN-DEF', 'Complete', grant1Citations3);
- });
+ // Create Monitoring Review Citations.
+ const grant1Citations1 = [
+ {
+ citationText: 'Grant 1 - Citation 1 - Good',
+ monitoringFindingType: 'Citation 1 Monitoring Finding Type',
+ monitoringFindingStatusName: 'Active',
+ monitoringFindingGrantFindingType: 'Corrective Action',
+ },
+ {
+ citationText: 'Grant 1 - Citation 2 - Bad MFS name',
+ monitoringFindingType: 'Citation 2 Monitoring Finding Type',
+ monitoringFindingStatusName: 'Abandoned',
+ monitoringFindingGrantFindingType: 'Corrective Action',
+ },
+ {
+ citationText: 'Grant 1 - Citation 3 - Good 2',
+ monitoringFindingType: 'Citation 3 Monitoring Finding Type',
+ monitoringFindingStatusName: 'Active',
+ monitoringFindingGrantFindingType: 'Corrective Action',
+ },
+ ];
+
+ // Grant 1.
+ await createMonitoringData(grant1.number, 1, new Date(), 'AIAN-DEF', 'Complete', grant1Citations1);
+
+ // Grant 1a (make sure other grant citations comeback).
+ const grant1Citations1a = [
+ {
+ citationText: 'Grant 1a - Citation 1 - Good',
+ monitoringFindingType: 'Citation 4 Monitoring Finding Type',
+ monitoringFindingStatusName: 'Active',
+ monitoringFindingGrantFindingType: 'Grant 1a Corrective Action',
+ },
+ ];
+ await createMonitoringData(grant1a.number, 2, new Date(), 'AIAN-DEF', 'Complete', grant1Citations1a);
+
+ // Grant 2.
+ const grant1Citations2 = [
+ {
+ citationText: 'Grant 2 - Citation 1 - Good',
+ monitoringFindingType: 'citation 5 Monitoring Finding Type',
+ monitoringFindingStatusName: 'Active',
+ monitoringFindingGrantFindingType: 'Corrective Action',
+ },
+ ];
+ // Before delivery date.
+ await createMonitoringData(grant2.number, 3, new Date('2024-09-01'), 'AIAN-DEF', 'Complete', grant1Citations2);
+ // After delivery date (tomorrow).
+ await createMonitoringData(grant2.number, 4, new Date(new Date().setDate(new Date().getDate() + 1)), 'AIAN-DEF', 'Complete', grant1Citations2);
+
+ // Grant 3 (inactive).
+ const grant1Citations3 = [
+ {
+ citationText: 'Grant 3 - Citation 1 - Good',
+ monitoringFindingType: 'Material Weakness',
+ monitoringFindingStatusName: 'Active',
+ monitoringFindingGrantFindingType: 'Corrective Action',
+ },
+ ];
+ await createMonitoringData(grant3.number, 5, new Date(), 'AIAN-DEF', 'Complete', grant1Citations3);
+ });
- afterAll(async () => {
- // Rollback any changes made to the database during the test.
- await rollbackToSnapshot(snapShot);
- });
+ afterAll(async () => {
+ // Rollback any changes made to the database during the test.
+ await rollbackToSnapshot(snapShot);
+ });
- it('correctly retrieves citations per grant', async () => {
- // Call the service to get the citations by grant ids.
- // get todays date in YYYY-MM-DD for the last possible hour of the day.
- const reportStartDate = new Date().toISOString().split('T')[0];
- const citationsToAssert = await getCitationsByGrantIds([grant1.id, grant1a.id, grant2.id, grant3.id], reportStartDate);
-
- // Assert correct number of citations.
- expect(citationsToAssert.length).toBe(3);
-
- // Assert the citations.
- // Get the citation with the text 'Grant 1 - Citation 1 - Good'.
- const citation1 = citationsToAssert.find((c) => c.citation === 'Grant 1 - Citation 1 - Good');
- expect(citation1).toBeDefined();
- expect(citation1.grants.length).toBe(1);
- expect(citation1.grants[0].findingId).toBeDefined();
- expect(citation1.grants[0].grantId).toBe(grant1.id);
- expect(citation1.grants[0].grantNumber).toBe(grant1.number);
- expect(citation1.grants[0].reviewName).toBeDefined();
- expect(citation1.grants[0].reportDeliveryDate).toBeDefined();
- expect(citation1.grants[0].findingType).toBe('Citation 1 Monitoring Finding Type');
- expect(citation1.grants[0].findingSource).toBe('Internal Controls');
- expect(citation1.grants[0].monitoringFindingStatusName).toBe('Active');
-
- // Get the citation with the text 'Grant 1 - Citation 3 - Good 2'.
- const citation2 = citationsToAssert.find((c) => c.citation === 'Grant 1 - Citation 3 - Good 2');
-
- expect(citation2).toBeDefined();
- expect(citation2.grants.length).toBe(1);
- expect(citation2.grants[0].findingId).toBeDefined();
- expect(citation2.grants[0].grantId).toBe(grant1.id);
- expect(citation2.grants[0].grantNumber).toBe(grant1.number);
- expect(citation2.grants[0].reviewName).toBeDefined();
- expect(citation2.grants[0].reportDeliveryDate).toBeDefined();
- expect(citation2.grants[0].findingType).toBe('Citation 3 Monitoring Finding Type');
-
- // Get the citation with the text 'Grant 1a - Citation 1 - Good'.
- const citation3 = citationsToAssert.find((c) => c.citation === 'Grant 1a - Citation 1 - Good');
- expect(citation3).toBeDefined();
- expect(citation3.grants.length).toBe(1);
- expect(citation3.grants[0].findingId).toBeDefined();
- expect(citation3.grants[0].grantId).toBe(grant1a.id);
- expect(citation3.grants[0].grantNumber).toBe(grant1a.number);
- expect(citation3.grants[0].reviewName).toBeDefined();
- expect(citation3.grants[0].reportDeliveryDate).toBeDefined();
- expect(citation3.grants[0].findingType).toBe('Citation 4 Monitoring Finding Type');
- });
+ it('getCitationsByGrantIds', async () => {
+ // Call the service to get the citations by grant ids.
+ // get todays date in YYYY-MM-DD for the last possible hour of the day.
+ const reportStartDate = new Date().toISOString().split('T')[0];
+ const citationsToAssert = await getCitationsByGrantIds([grant1.id, grant1a.id, grant2.id, grant3.id], reportStartDate);
+
+ // Assert correct number of citations.
+ expect(citationsToAssert.length).toBe(3);
+
+ // Assert the citations.
+ // Get the citation with the text 'Grant 1 - Citation 1 - Good'.
+ const citation1 = citationsToAssert.find((c) => c.citation === 'Grant 1 - Citation 1 - Good');
+ expect(citation1).toBeDefined();
+ expect(citation1.grants.length).toBe(1);
+ expect(citation1.grants[0].findingId).toBeDefined();
+ expect(citation1.grants[0].grantId).toBe(grant1.id);
+ expect(citation1.grants[0].grantNumber).toBe(grant1.number);
+ expect(citation1.grants[0].reviewName).toBeDefined();
+ expect(citation1.grants[0].reportDeliveryDate).toBeDefined();
+ expect(citation1.grants[0].findingType).toBe('Citation 1 Monitoring Finding Type');
+ expect(citation1.grants[0].findingSource).toBe('Internal Controls');
+ expect(citation1.grants[0].monitoringFindingStatusName).toBe('Active');
+
+ // Get the citation with the text 'Grant 1 - Citation 3 - Good 2'.
+ const citation2 = citationsToAssert.find((c) => c.citation === 'Grant 1 - Citation 3 - Good 2');
+
+ expect(citation2).toBeDefined();
+ expect(citation2.grants.length).toBe(1);
+ expect(citation2.grants[0].findingId).toBeDefined();
+ expect(citation2.grants[0].grantId).toBe(grant1.id);
+ expect(citation2.grants[0].grantNumber).toBe(grant1.number);
+ expect(citation2.grants[0].reviewName).toBeDefined();
+ expect(citation2.grants[0].reportDeliveryDate).toBeDefined();
+ expect(citation2.grants[0].findingType).toBe('Citation 3 Monitoring Finding Type');
+
+ // Get the citation with the text 'Grant 1a - Citation 1 - Good'.
+ const citation3 = citationsToAssert.find((c) => c.citation === 'Grant 1a - Citation 1 - Good');
+ expect(citation3).toBeDefined();
+ expect(citation3.grants.length).toBe(1);
+ expect(citation3.grants[0].findingId).toBeDefined();
+ expect(citation3.grants[0].grantId).toBe(grant1a.id);
+ expect(citation3.grants[0].grantNumber).toBe(grant1a.number);
+ expect(citation3.grants[0].reviewName).toBeDefined();
+ expect(citation3.grants[0].reportDeliveryDate).toBeDefined();
+ expect(citation3.grants[0].findingType).toBe('Citation 4 Monitoring Finding Type');
+ });
+
+ it('getMonitoringGoals', async () => {
+ const goalsToAssert = await getMonitoringGoals([grant1.id, grant1a.id], new Date().toISOString().split('T')[0]);
+ expect(goalsToAssert.length).toBe(2);
+
+ // Assert the goals.
+ // Assert the monitoring goal.
+ const monitoringGoalToAssert = goalsToAssert.find((g) => g.id === monitoringGoal.id);
+ expect(monitoringGoalToAssert).toBeDefined();
+ expect(monitoringGoalToAssert.name).toBe('Monitoring Goal 1');
+ expect(monitoringGoalToAssert.status).toBe('Not started');
+ expect(monitoringGoalToAssert.grantIds).toStrictEqual([grant1.id]);
+ expect(monitoringGoalToAssert.created).toBeDefined();
+ expect(monitoringGoalToAssert.onApprovedAR).toBe(true);
+ expect(monitoringGoalToAssert.createdVia).toBe('monitoring');
+ expect(monitoringGoalToAssert.goalTemplateId).toBe(monitoringGoal.goalTemplateId);
+
+ // Assert the grant1a monitoring goal.
+ const monitoringGoalToAssert1a = goalsToAssert.find((g) => g.id === grant1aMonitoringGoal.id);
+ expect(monitoringGoalToAssert1a).toBeDefined();
+ expect(monitoringGoalToAssert1a.name).toBe('Monitoring Goal 3');
+ expect(monitoringGoalToAssert1a.status).toBe('Not started');
+ expect(monitoringGoalToAssert1a.grantIds).toStrictEqual([grant1a.id]);
+ expect(monitoringGoalToAssert1a.created).toBeDefined();
+ expect(monitoringGoalToAssert1a.onApprovedAR).toBe(true);
+ expect(monitoringGoalToAssert1a.createdVia).toBe('monitoring');
+ expect(monitoringGoalToAssert1a.goalTemplateId).toBe(monitoringGoal.goalTemplateId);
});
});
From cb95166553c44a642366d5df5de4a8cdc1215a22 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Wed, 27 Nov 2024 16:08:02 -0500
Subject: [PATCH 036/198] fix api test
---
src/goalServices/goals.js | 2 +-
src/routes/activityReports/handlers.js | 7 ++++---
tests/api/activityReport.spec.ts | 2 +-
3 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/src/goalServices/goals.js b/src/goalServices/goals.js
index 67190652a7..0697440fc4 100644
--- a/src/goalServices/goals.js
+++ b/src/goalServices/goals.js
@@ -692,7 +692,7 @@ export async function createOrUpdateGoals(goals) {
return goalsByIdAndRecipient(goalIds, recipient);
}
-export async function goalsForGrants(grantIds, reportStartDate, user = null) {
+export async function goalsForGrants(grantIds, reportStartDate, user) {
/**
* get all the matching grants
*/
diff --git a/src/routes/activityReports/handlers.js b/src/routes/activityReports/handlers.js
index b88987bae2..1b0e94086e 100644
--- a/src/routes/activityReports/handlers.js
+++ b/src/routes/activityReports/handlers.js
@@ -293,9 +293,10 @@ export async function getLegacyReport(req, res) {
*/
export async function getGoals(req, res) {
try {
- const { grantIds } = req.query;
- const { reportStartDate } = req.params;
- const goals = await goalsForGrants(grantIds, reportStartDate);
+ const { grantIds, reportStartDate } = req.query;
+ const userId = await currentUserId(req, res);
+ const user = await userById(userId);
+ const goals = await goalsForGrants(grantIds, reportStartDate, user);
res.json(goals);
} catch (error) {
await handleErrors(req, res, error, logContext);
diff --git a/tests/api/activityReport.spec.ts b/tests/api/activityReport.spec.ts
index fbd540fe36..3afa319c81 100644
--- a/tests/api/activityReport.spec.ts
+++ b/tests/api/activityReport.spec.ts
@@ -4,7 +4,7 @@ import { root, validateSchema } from './common';
test.describe('get /activity-reports/goals', () => {
test('200', async ({ request }) => {
- const response = await request.get(`${root}/activity-reports/goals?grantIds=1`, { headers: { 'playwright-user-id': '1' } });
+ const response = await request.get(`${root}/activity-reports/goals?grantIds=1&reportStartDate=2023-01-01`, { headers: { 'playwright-user-id': '1' } });
const goalForGrant = Joi.object({
grantIds: Joi.array().items(Joi.number()).required(),
goalIds: Joi.array().items(Joi.number()).required(),
From 2570a216c423ec2ea0a62aa58829337f05952ee4 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Wed, 27 Nov 2024 17:11:57 -0500
Subject: [PATCH 037/198] see if hardcoding fetcher value gets e2e to pass
---
frontend/src/fetchers/activityReports.js | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/frontend/src/fetchers/activityReports.js b/frontend/src/fetchers/activityReports.js
index 6f98a9dcd8..a397dcb9fd 100644
--- a/frontend/src/fetchers/activityReports.js
+++ b/frontend/src/fetchers/activityReports.js
@@ -109,8 +109,9 @@ export const getRecipientsForExistingAR = async (reportId) => {
};
export const getGoals = async (grantIds) => {
+ const startDate = '2021-01-01';
const params = grantIds.map((grantId) => `grantIds=${grantId}`);
- const url = join(activityReportUrl, 'goals', `?${params.join('&')}`);
+ const url = join(activityReportUrl, 'goals', `?${params.join('&')}&reportStartDate=${startDate}`);
const goals = await get(url);
return goals.json();
};
From 703bffb99bd34b014fb8aecc720684e26b40bde1 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Mon, 2 Dec 2024 09:35:23 -0500
Subject: [PATCH 038/198] fix FE tests
---
.../ActivityReport/Pages/__tests__/goalsObjectives.js | 2 +-
frontend/src/pages/ActivityReport/__tests__/index.js | 8 ++++----
frontend/src/pages/Admin/Goals/__tests__/Close.js | 8 ++++----
3 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/frontend/src/pages/ActivityReport/Pages/__tests__/goalsObjectives.js b/frontend/src/pages/ActivityReport/Pages/__tests__/goalsObjectives.js
index bd30dc0d3e..766777780c 100644
--- a/frontend/src/pages/ActivityReport/Pages/__tests__/goalsObjectives.js
+++ b/frontend/src/pages/ActivityReport/Pages/__tests__/goalsObjectives.js
@@ -104,7 +104,7 @@ const renderGoals = (
const query = grantIds.map((id) => `grantIds=${id}`).join('&');
const fetchResponse = throwFetchError ? 500 : goals;
- fetchMock.get(join(goalUrl, `?${query}`), fetchResponse);
+ fetchMock.get(join(goalUrl, `?${query}&reportStartDate=2021-01-01`), fetchResponse);
render(
diff --git a/frontend/src/pages/ActivityReport/__tests__/index.js b/frontend/src/pages/ActivityReport/__tests__/index.js
index a362305a38..cdb97c9597 100644
--- a/frontend/src/pages/ActivityReport/__tests__/index.js
+++ b/frontend/src/pages/ActivityReport/__tests__/index.js
@@ -663,7 +663,7 @@ describe('ActivityReport', () => {
const data = formData();
fetchMock.get('/api/topic', []);
fetchMock.get('/api/goal-templates?grantIds=12539', []);
- fetchMock.get('/api/activity-reports/goals?grantIds=12539', []);
+ fetchMock.get('/api/activity-reports/goals?grantIds=12539&reportStartDate=2021-01-01', []);
fetchMock.get('/api/goals?reportId=1&goalIds=37499', mockGoalsAndObjectives(true));
fetchMock.get('/api/activity-reports/1', {
...data,
@@ -696,7 +696,7 @@ describe('ActivityReport', () => {
const data = formData();
fetchMock.get('/api/topic', []);
fetchMock.get('/api/goal-templates?grantIds=12539', []);
- fetchMock.get('/api/activity-reports/goals?grantIds=12539', []);
+ fetchMock.get('/api/activity-reports/goals?grantIds=12539&reportStartDate=2021-01-01', []);
// fetchMock.get('/api/goals?reportId=1&goalIds=37499', mockGoalsAndObjectives(true));
fetchMock.get('/api/activity-reports/1', {
...data,
@@ -752,7 +752,7 @@ describe('ActivityReport', () => {
});
fetchMock.get('/api/topic', [{ id: 64, name: 'Communication' }]);
- fetchMock.get('/api/activity-reports/goals?grantIds=10431', [{
+ fetchMock.get('/api/activity-reports/goals?grantIds=10431&reportStartDate=2021-01-01', [{
endDate: null,
grantIds: [10431],
goalIds: [37502],
@@ -955,7 +955,7 @@ describe('ActivityReport', () => {
it('you can add a goal and objective and add a file after saving', async () => {
const data = formData();
fetchMock.get('/api/topic', [{ id: 64, name: 'Communication' }]);
- fetchMock.get('/api/activity-reports/goals?grantIds=12539', []);
+ fetchMock.get('/api/activity-reports/goals?grantIds=12539&reportStartDate=2021-01-01', []);
fetchMock.get('/api/goal-templates?grantIds=12539', []);
fetchMock.put('/api/activity-reports/1/goals/edit?goalIds=37504', {});
fetchMock.get('/api/activity-reports/1', {
diff --git a/frontend/src/pages/Admin/Goals/__tests__/Close.js b/frontend/src/pages/Admin/Goals/__tests__/Close.js
index 4ec314434c..a86b345eaf 100644
--- a/frontend/src/pages/Admin/Goals/__tests__/Close.js
+++ b/frontend/src/pages/Admin/Goals/__tests__/Close.js
@@ -82,7 +82,7 @@ describe('Close', () => {
userEvent.selectOptions(region, '1');
});
- fetchMock.get(`${goalsForGrantsUrl}?grantIds=2&grantIds=4`, []);
+ fetchMock.get(`${goalsForGrantsUrl}?grantIds=2&grantIds=4&reportStartDate=2021-01-01`, []);
expect(fetchMock.called(getGroupsByRegionUrl)).toBe(true);
@@ -130,7 +130,7 @@ describe('Close', () => {
userEvent.selectOptions(region, '1');
});
- fetchMock.get(`${goalsForGrantsUrl}?grantIds=2&grantIds=4`, 500);
+ fetchMock.get(`${goalsForGrantsUrl}?grantIds=2&grantIds=4&reportStartDate=2021-01-01`, 500);
expect(fetchMock.called(getGroupsByRegionUrl)).toBe(true);
@@ -178,7 +178,7 @@ describe('Close', () => {
userEvent.selectOptions(region, '1');
});
- fetchMock.get(`${goalsForGrantsUrl}?grantIds=2&grantIds=4`, [{
+ fetchMock.get(`${goalsForGrantsUrl}?grantIds=2&grantIds=4&reportStartDate=2021-01-01`, [{
status: 'In Progress',
name: 'Hey hey hey hey hey',
goalIds: [1],
@@ -272,7 +272,7 @@ describe('Close', () => {
userEvent.selectOptions(region, '1');
});
- fetchMock.get(`${goalsForGrantsUrl}?grantIds=2&grantIds=4`, [{
+ fetchMock.get(`${goalsForGrantsUrl}?grantIds=2&grantIds=4&reportStartDate=2021-01-01`, [{
status: 'In Progress',
name: 'Hey hey hey hey hey',
goalIds: [1],
From 5a3e907fcd3d05e9b5640d83dc886da96303c850 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Mon, 2 Dec 2024 10:33:07 -0500
Subject: [PATCH 039/198] fix e2e
---
src/goalServices/goals.js | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/goalServices/goals.js b/src/goalServices/goals.js
index 0697440fc4..ca6ce8a100 100644
--- a/src/goalServices/goals.js
+++ b/src/goalServices/goals.js
@@ -738,7 +738,7 @@ export async function goalsForGrants(grantIds, reportStartDate, user) {
* Get all matching goals
*/
- const regularGoals = Goal.findAll({
+ const regularGoals = await Goal.findAll({
attributes: [
[sequelize.fn(
'ARRAY_AGG',
@@ -840,8 +840,9 @@ export async function goalsForGrants(grantIds, reportStartDate, user) {
/*
* Get all monitoring goals
*/
- let goalsToReturn = [regularGoals];
+ let goalsToReturn = regularGoals;
const hasGoalMonitoringOverride = !!(user && new Users(user).canSeeBehindFeatureFlag('monitoring_integration'));
+
if (hasGoalMonitoringOverride) {
const monitoringGoals = await getMonitoringGoals(ids, reportStartDate);
From f84ee3aa0f6df703e9dcfc9f7845a2216914feb2 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Mon, 2 Dec 2024 11:26:20 -0500
Subject: [PATCH 040/198] dont require start date simply dont return monitoring
goals
---
frontend/src/fetchers/activityReports.js | 8 ++++----
.../ActivityReport/Pages/__tests__/goalsObjectives.js | 2 +-
frontend/src/pages/ActivityReport/__tests__/index.js | 8 ++++----
frontend/src/pages/Admin/Goals/__tests__/Close.js | 8 ++++----
src/goalServices/goals.js | 2 +-
5 files changed, 14 insertions(+), 14 deletions(-)
diff --git a/frontend/src/fetchers/activityReports.js b/frontend/src/fetchers/activityReports.js
index a397dcb9fd..0c02becc1a 100644
--- a/frontend/src/fetchers/activityReports.js
+++ b/frontend/src/fetchers/activityReports.js
@@ -108,10 +108,10 @@ export const getRecipientsForExistingAR = async (reportId) => {
return recipients.json();
};
-export const getGoals = async (grantIds) => {
- const startDate = '2021-01-01';
- const params = grantIds.map((grantId) => `grantIds=${grantId}`);
- const url = join(activityReportUrl, 'goals', `?${params.join('&')}&reportStartDate=${startDate}`);
+export const getGoals = async (grantIds, reportStartDate = null) => {
+ const reportStartDateParam = reportStartDate ? `&reportStartDate=${reportStartDate}` : '';
+ const params = grantIds.map((grantId) => `grantIds=${grantId}${reportStartDateParam}`);
+ const url = join(activityReportUrl, 'goals', `?${params.join('&')}`);
const goals = await get(url);
return goals.json();
};
diff --git a/frontend/src/pages/ActivityReport/Pages/__tests__/goalsObjectives.js b/frontend/src/pages/ActivityReport/Pages/__tests__/goalsObjectives.js
index 766777780c..bd30dc0d3e 100644
--- a/frontend/src/pages/ActivityReport/Pages/__tests__/goalsObjectives.js
+++ b/frontend/src/pages/ActivityReport/Pages/__tests__/goalsObjectives.js
@@ -104,7 +104,7 @@ const renderGoals = (
const query = grantIds.map((id) => `grantIds=${id}`).join('&');
const fetchResponse = throwFetchError ? 500 : goals;
- fetchMock.get(join(goalUrl, `?${query}&reportStartDate=2021-01-01`), fetchResponse);
+ fetchMock.get(join(goalUrl, `?${query}`), fetchResponse);
render(
diff --git a/frontend/src/pages/ActivityReport/__tests__/index.js b/frontend/src/pages/ActivityReport/__tests__/index.js
index cdb97c9597..a362305a38 100644
--- a/frontend/src/pages/ActivityReport/__tests__/index.js
+++ b/frontend/src/pages/ActivityReport/__tests__/index.js
@@ -663,7 +663,7 @@ describe('ActivityReport', () => {
const data = formData();
fetchMock.get('/api/topic', []);
fetchMock.get('/api/goal-templates?grantIds=12539', []);
- fetchMock.get('/api/activity-reports/goals?grantIds=12539&reportStartDate=2021-01-01', []);
+ fetchMock.get('/api/activity-reports/goals?grantIds=12539', []);
fetchMock.get('/api/goals?reportId=1&goalIds=37499', mockGoalsAndObjectives(true));
fetchMock.get('/api/activity-reports/1', {
...data,
@@ -696,7 +696,7 @@ describe('ActivityReport', () => {
const data = formData();
fetchMock.get('/api/topic', []);
fetchMock.get('/api/goal-templates?grantIds=12539', []);
- fetchMock.get('/api/activity-reports/goals?grantIds=12539&reportStartDate=2021-01-01', []);
+ fetchMock.get('/api/activity-reports/goals?grantIds=12539', []);
// fetchMock.get('/api/goals?reportId=1&goalIds=37499', mockGoalsAndObjectives(true));
fetchMock.get('/api/activity-reports/1', {
...data,
@@ -752,7 +752,7 @@ describe('ActivityReport', () => {
});
fetchMock.get('/api/topic', [{ id: 64, name: 'Communication' }]);
- fetchMock.get('/api/activity-reports/goals?grantIds=10431&reportStartDate=2021-01-01', [{
+ fetchMock.get('/api/activity-reports/goals?grantIds=10431', [{
endDate: null,
grantIds: [10431],
goalIds: [37502],
@@ -955,7 +955,7 @@ describe('ActivityReport', () => {
it('you can add a goal and objective and add a file after saving', async () => {
const data = formData();
fetchMock.get('/api/topic', [{ id: 64, name: 'Communication' }]);
- fetchMock.get('/api/activity-reports/goals?grantIds=12539&reportStartDate=2021-01-01', []);
+ fetchMock.get('/api/activity-reports/goals?grantIds=12539', []);
fetchMock.get('/api/goal-templates?grantIds=12539', []);
fetchMock.put('/api/activity-reports/1/goals/edit?goalIds=37504', {});
fetchMock.get('/api/activity-reports/1', {
diff --git a/frontend/src/pages/Admin/Goals/__tests__/Close.js b/frontend/src/pages/Admin/Goals/__tests__/Close.js
index a86b345eaf..4ec314434c 100644
--- a/frontend/src/pages/Admin/Goals/__tests__/Close.js
+++ b/frontend/src/pages/Admin/Goals/__tests__/Close.js
@@ -82,7 +82,7 @@ describe('Close', () => {
userEvent.selectOptions(region, '1');
});
- fetchMock.get(`${goalsForGrantsUrl}?grantIds=2&grantIds=4&reportStartDate=2021-01-01`, []);
+ fetchMock.get(`${goalsForGrantsUrl}?grantIds=2&grantIds=4`, []);
expect(fetchMock.called(getGroupsByRegionUrl)).toBe(true);
@@ -130,7 +130,7 @@ describe('Close', () => {
userEvent.selectOptions(region, '1');
});
- fetchMock.get(`${goalsForGrantsUrl}?grantIds=2&grantIds=4&reportStartDate=2021-01-01`, 500);
+ fetchMock.get(`${goalsForGrantsUrl}?grantIds=2&grantIds=4`, 500);
expect(fetchMock.called(getGroupsByRegionUrl)).toBe(true);
@@ -178,7 +178,7 @@ describe('Close', () => {
userEvent.selectOptions(region, '1');
});
- fetchMock.get(`${goalsForGrantsUrl}?grantIds=2&grantIds=4&reportStartDate=2021-01-01`, [{
+ fetchMock.get(`${goalsForGrantsUrl}?grantIds=2&grantIds=4`, [{
status: 'In Progress',
name: 'Hey hey hey hey hey',
goalIds: [1],
@@ -272,7 +272,7 @@ describe('Close', () => {
userEvent.selectOptions(region, '1');
});
- fetchMock.get(`${goalsForGrantsUrl}?grantIds=2&grantIds=4&reportStartDate=2021-01-01`, [{
+ fetchMock.get(`${goalsForGrantsUrl}?grantIds=2&grantIds=4`, [{
status: 'In Progress',
name: 'Hey hey hey hey hey',
goalIds: [1],
diff --git a/src/goalServices/goals.js b/src/goalServices/goals.js
index ca6ce8a100..e960f2050c 100644
--- a/src/goalServices/goals.js
+++ b/src/goalServices/goals.js
@@ -843,7 +843,7 @@ export async function goalsForGrants(grantIds, reportStartDate, user) {
let goalsToReturn = regularGoals;
const hasGoalMonitoringOverride = !!(user && new Users(user).canSeeBehindFeatureFlag('monitoring_integration'));
- if (hasGoalMonitoringOverride) {
+ if (hasGoalMonitoringOverride && reportStartDate) {
const monitoringGoals = await getMonitoringGoals(ids, reportStartDate);
// Combine goalsToReturn with monitoringGoals.
From 8f251e82e65c93b27a94a7345eaa7bb6660908ad Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Mon, 2 Dec 2024 15:14:43 -0500
Subject: [PATCH 041/198] fix model relationships
---
docs/logical_data_model.encoded | 2 +-
docs/logical_data_model.puml | 2 +-
src/models/activityReportObjective.js | 1 -
src/models/activityReportObjectiveCitation.js | 8 ++++++--
4 files changed, 8 insertions(+), 5 deletions(-)
diff --git a/docs/logical_data_model.encoded b/docs/logical_data_model.encoded
index c26b9430d4..799d0f05b4 100644
--- a/docs/logical_data_model.encoded
+++ b/docs/logical_data_model.encoded
@@ -1 +1 @@
-xLrVS-Kcbd_Nfr1v7f9qtKnDairMhMjsQznkTwThkzqksvsfIIhbmXA-bxG4Tm2vusJptRS0_G64991qtTq9J_P5m3dyEC3X73ZyWt50ULKMIPf-be9xWl9qJybj0KkMlaTybmAyBM2UPfIyP-aTX3n9K_OEuJsWe4mBX25_sCEKKBuZngUi08obloLfcgPF2SqXFIK5eUbVlldcFq_-wszQ-lKEPUymP2pzEadJtoKhfw8qP4bQrtEISNIF-CCbpD0UGSpPRqdw94LvUfmYpE4MqlGu_ViMOaW1X_d_9EcM0inVcXakpvwVNPwTdvvTfKSln7Tsz5UK9odXEqZFC4VyOQJ4cpONWdN19obeETiJEaRlnCZObiye50oVyFGuvQY4Z8Dohx0NFzDV2ORfSOh9-y-_4FchVUwV__SaiV4Jn4_sUqhk8Pq6SNdxAWRfIAct3DB7mIBDOG6dSYSf9boKuV0NdgIC0rwnz3Y5k2g7q3mbHG5k2GMS_BaG7S3ZEIhWO-1mXmeuKago4OA3R_k7sls2WE8vWaL-2TcUO3O31565Dn0YSzl0BG75DIpt1U1GCd51IRaVdV_S9Gu3MKfhJEgXzUDFOoXUGaOgcZqgWBIcELAwPUim4AdAPLSqKuw2IVmdVzwCG7LZ5FyjIJ4e5UuoFP6KxgX8hAlqh-tl__VbbsFTDDoJqpr8ADeY38g9dZFwQkdCiGsWjMuVPmLXyAfYUuZpKuB5K6WN6_tR9GICu6RXII7Edm6vBWaYIOgOR1ai7ivonA5eoYqX1GIYo1qecCnpoJdOWxOI8H3ToYPxhltMBdJtW6OxG3__-jjllz1afYatC6ZVtFuC9KEdY0EE20uVkMqLUiV83gYhR5aFNzLAWX4d5E7j9Ro35E8CYhR-p0Y-NMlLwo0W5Pqpzpi169NdjaGdh9BsI0KrSnzIFMYvdgSdh07JnwOvj1pEN_ZMWEKJN0BxoFNMGNq5v8Numy459VSeX_d0KZXLgsCzx1UPwEfhiaVPX-bdBaYFb5UiVP01uAyl-03HR_fIpO7CedhOVIsWRuGPnU3dgzcgHjl2KyNqTv32JhqOr1HOjHT0NGjZK5EL76yjJMtOJMWJysV1y0xz1EUyuQdkxFJL_vwsdzyoc2Sfo7Car2TGd1CkB6gPaWLXwS2k0krywPuR_QV4YPbmkdSBmFXRXU7J1p-BHuXjXC8j8dYquqM6Hnh5Jr9Xa5DwRiVEruacuRNfinvZvlYpkhaTTK2P-YRt9Q38Ac49oAyCarSZBTtC8K0Wz98KtJZFOO7k8Nru3VcEv3tMyees2dmECrI2ulDlDbzzuHBNpZhGt_AKGcXabbflBm3ULc1hOo4A5t0BCYKo9YzEcTUeuggyXVJDtGMW76LeBqp4-dPa3xPG80HhDaIAaTFcP4_9be9Iz9dhCsqct_w7VrUGSIa4ramhJFO6DAowBH4JicDrdlp0EJIHv81k8RyWUz7XGi0TvAMAuFp0WKF-n9TxH7a5YcUrCBYaj9tlTPEf4ANmcLwlmNlMDoorxJH0HZS3aht3wa8wMr9G9j4emPvTav5TdbLXm_yzA53-V1Inx-iiSsUzsifWokmJCGxsO0iuV8DVGxg5XiM2qmeUo9wlWLoM5KQP5FbNPEkx-yoiSgERQpFhHxJVxB7cqNBQB3YZIsJZZpkKTqBaom47eVOQlUr-1fd9GRN8T69OtvwaUx7OCyxUuBVxWW2d1U-m0-Bi7cAkviKBL11-3fJ-S0enXrJsauUGGAkMaQwt2RPGOeFoI0qcYu_Y43DvEtTFRg6Qdc3OLTLWt_XDLFgAg6ajOd7sRlZfsylBi_Fhc-kNhy-khazUNxZ67tcFJSJTwdG0D0woVOLlJpAxOIrYtjEvvvGZfkmhz2iqM_CEFZYwlp5921u69CHq90TFOS8Z4UfcpYmbO7JX6j0NGXmbdvgZlq3utLEGlMlboq0xs94R0jM7SzOIVD9i22zrguxyOgKiTko6kd9383oatSODahukcs25hjXUDf-bpjqfTwxJWIiIhr292a0HVtX1IRNty-A6oxz5PgAebVj0fZZuIrPKESnVtfqJVfAN23iw-VSHiAQ1kWlrTS_JmjV5HKtktm7s4iiT91XmEVSiM1POSX4ytG6yzPWFGjQJjHtwyzSUsrw6uVihjyMS5Fd0bzOefAeOqXRb1scUw5jZ3DnT2uVdY6wAUYvDuLCjpSM0CBMilMHHsooPZWJlinQWCiuDt8dc6mwA7S8T6R9lZ66DCR2MzJfLclkSk7O-lGQ60wDBR88-tgZPVHIkgZQaGXV_kNXy4auS9p1J2xk-cayKRbVrNZvNrqntoI7S8sq-H73hBMYsH3l0TlgWhpazDmqdUl9hagCxj5edIdzJxG-Ow6cNLHxdWFPTctxwqtpfLrQh2q0XvY5wHSZn-8z9TQuv-Hu1tV_aiT_hdw5xJabvLGBAsGv2Vit1HG6mlWSQFq97kTlnllHedKJ-N3gYNY7yRjsR3lvTkUtO25vwmooHahzBk2z01ilQmRdONEkiT2NZxI7DiZRVdjrljNHA6eQ0eIezVn4aKgeKmjiNgqlYV4sSTatKnxq9T1_Ty8NlS3iqGmIRaRZC-0mmha3nlC8vmjlN1FDThyeiHwu9kXPZrwTkQXsU4gIKGhTzL7wxsiCCWG8nVbO2L4nbSbQ4iFGII_rYaYCzitK8glRQJx7Ko-PaIdVOZ-WLubJ1AScTvHqBY2R7fDf_4EFmbDt6iDJyWSp3qYpcdZqS-vgWAJ4CqmFjuFmiMVxaPwoUGlUwg6uBb1bd3tU0mloVI4vQvfMybz9Rz0nmuEi9Klwbv-Ek-PTjfc3G3PTFUfn-zlBPsVdro-SlpvvzTdny9BTr5jStgioHFqhoP06-8vsJpDX5KPWHcf-IIXrEghEmR0TBq1TihR4UTap_8mqAwKcKmuy5aFTO-CvFkDWKwWUrqkFqRxKbAOEC3MVmxwIv7SJtpqVxo49_pN6Nmpk4aMw_ajvRYC5j0GTE7Dpn_HNa7E6jKSOQmaA2ycg6JKw8pMl0thK5zu1tuhedZ5-eBaDuAyvkwQotIBq6g83KFCLoAromt7rgJ5lOxMDaSztaxT83XM8Wjo7sURWdwS_ard44yOaubYTiSejrEGfPLV3s92AhCe6SDc7gY64ECS0Pz86DKxJTGjeUksrpwOFMghCTl833BQ4F6kGpZlb8IuzkglLVDguK37TLuKIYlGecGmnSLMK9A8BUAxcn8NOjvTh6_UYlHrgP8xLNk4Vm_SlSNS5hxPwQoDVKqfGw2bf_f1dYD2ZtMrztaETbHy0O9QHyGxS0er_jKy3VRlx3Mns39L47SQxKHODUDSQwXp3ZjCh6Ir0v0pJdLQSLT8dGiw3DVDUXpv7SSNchZNSg_xIar1KtejAMEsf55iBfMbweZ8ROYd97tI8vidxtk56EhQQsflnHs04SMLFjhsbM6Sj6_378E65mUlUR4KgXbCTd4EIl48RBxMUr07jGRosI-jtVXPBy7MBYjvUgk5oKmqLGyVSm8ecFE1VV6Jor93yE4VOJjXnoI4k4pRlOndZ4W8gR7lUTf3dSkoiAdzwvAwhDVLaxbE5xl5rUIiaX1iRCVVsd8ichIE3QGcTFaeycSbUru_Bnb9B6LUebj0rxcg3MXJynPVWHPVCFDoqUT3wC4-6YlddKUIDkG-8snB5ZNN6MitVCXv_ZrFQYRewt7MhnfluJisQfVDUeX1zyfN_w42mRJRv9_XzZujPwOyM1Oyd2OusRFuwkexTdRLMdXrNKT89xnk_AzE89STLU5PSHwjC78R7pFgMkjOzrMXOfJLn9nJEYdzzKiP0v1zckD64AT0se3ZHqn029wqVqSCYWiGzUTwRhRSOFFzU561rMDHSWBdpVLe391QFauzi8sD4o_KRHjaAvyEJEab_zmbFRsVF22NnieG32JqzPZ2qznZpGpaCUlUgQ0d_XLnpkGwxnuBhCMDVLMJwjwpizCFfNTmXtNTsPA2YlxVJos5tm6A7ml6LAZv2lVbxFEQgHaN8lfXVdogU5qttyawtpeZp-SKwuphZG-B7S5hksheeQwH_WlbWVKx_BO7xNkFJBNtC2egvnaNJ1socXe-3GjykqQzppWgVElfnsXoZZf_M4w9Ft9RuGbm_auTRftEU4NubDXXyVTTkyvD-JPv7LcsMHsPjZaTcNuS4RUqDNzBIvf1l8kYIlj1Fp2A0tB_JwXJYp8KD3-falmYPnbf7H1VPAVF-8twsvKFTtBY1b16DOB5NMxVgCgDFGZAuJsiBUFPBwVBiWs0l5fQApBZjT3E_kr7m_QVw0pijHQk9n2RCStwuFUmhlq2-EsD9thkHcUBglHTLzuRhWTuZBp_ELjSw0POMgTIFKXfCTlMVc-F8l26ZetRAHvzO-rDGsTW-HZeP8hY2oqiI5Zy-EZyLla8bOk_U0FY2yrG7OUiwTU2oUtwMAvdj6gR696RgqiExcjdI7PIPxsCPbjkFFI6wValoXFOxVXFbo9ZglBLOKZU3XRr7mlmrK0eJdkEPeqPQIseGAKwJQr4fx2Jamuo0sANu6l1ALq2cMRYr3nFNnVY8Y98Jnn_3uWs_8Pr_gY7LNSCrLTi6CuwswLqNXNCriPQ5hlzAB3g0xvf1b5EsTetfbpV8AIhLI2rBgU_4ovEeWwr4PKpTxcGyints2zLqJSNkjzIlzsJ7n3YCy9tRmcc5jPyUgxHgFogkCK2ZZjO9ij-ei9yn2NrbwB075rtOOtbe7kh6rDIOuwpt24Do_YCEJ0lhTL3s4EyOdsCIJj8ICgRSXMj-SYkRRNFHjF8FjDT-kOJSYQ-ECzmZTC3T0dHKK-1vG1AGwQ5vjzrO30fr2XrmKXzU-jqtGmfNrcW_l-HCawhlJR-uXfIY7Qu-HgYCnKQIvn7seWA3teAXWttEWa_QHM5ceBue1Dzn1TmNwTZX4TtXFVjy6VrJxWGyqst7o3hfqEUxQIlXsNnl4DG2FQG1sf8IN1tvJQSVaaprqO0eJsxTNPAksWUjRQjnshW9iEwTnUfoUN5wU_El7dvfrHO9t8tCTYRW0eWad5TyHAYt8ZiW1JIDTxnSvZK8xWk6vCpwKuSBME17-Qe3lPJmBn3ZAM7E6_4Re53UbUAFbXjJJc9ZFHTYecnBGTpA3M2DbvED4UrprHdQvCYeawRyhK23-S0COgwWuMtAJ0xQx9O3cZRiG58gQze5JWB2eInt1Ojgtng6kDqDwpsL28wlD-pLBRRbBty51JAchRc_K8mQK--7aMRqhz02bsq8IshD9_73o71KSKZylV5LYvxPOdi0QVVtYXvDN9_QZPnvMCw7EtsWvP5fPt03MIxfzw2LrG4uiEIBlBv9CLIdHa4dw2OWEGqi9n_3z3INlRyoT1oboHsgzpRjXQEleYLVc8OxQBqeZJACm3HmcYL3reRAiEH6YFaHZxuH4lDnuEIp_4IetL16YKOYw4uBraUU8boTUzdju4QqHjE8ww8zLdDp3yB8jkn6i4R0YJgCunR7d1LE4nu9dv2dk290DaFKoHGH80JJwidO4oGLI1AW7KDoLE61vzkoL1oHvGCDogs7YWNk0yniEjZrRlSWiGMoRDjXcTPNe1kLGNWNg7pTLtvr-InMG_iRft9h09-WLyoXIBiWYG1P0SG5pW3Bo17a4g04eUWEc48hRr15mnf_UZZWDu1GnSc7aVcmx0jG0DFugTiJ9ncaiA4H4oqOhhdCU84Lm7E14K6nG1jz3ZlYq-0m_dnn1Se4K0N81557feJByk6v4g4Ke2iyWD14L2vN1bteZF1qy4Pf1Q28i5YmXp-7-wP09UVRsJmVFv5ku4ISJfu5NN2DCJfWSJuH6f4QGShzd6I6RXYoAs2HUKQfyK78yJ16H6a8gpbL1MCL9QXAeYD0WGkxt3YDA9af3Zob6d5muHM7o02akT85GnN4tXLC4Q12WWNVk8ankc2945aJn_aSWH14U6vxXL-yYG3f04HWBcAatwo9A1ahn5EWKGX4P6ndtsriH706Eywsl28s4Pl2XhuYFXa-K6mkN67vs69bjd6XSiCHDWniOntjM4Pa6cGYA1OhzGbP4HQ8ozUPMXCaDqyIpN14sDsmH8XkYaVUz8a8AeVxpO14PWKpKVboeE9u3Yy0qm0o_9XOniZ2As3Ym6Nx3B6BKO1H1iK3fFw0LSH6ukD_HYjWeREoNmVxKk2IVfIUCaGofTqcI997xzFJegwFqooz__Cjj0R9tctq14Dxm7ShUOSZOVt_zqvTVoYpQ5M31L_jh8v-9JjJjCz_AxEWfwN6w5KaRzQVxaYGZRsLPrCIZV32toFsMfaVuAEq554-FnNDmPjec1OF44iTJE07qfzveIeyjsYBBnf5c5bkTgzPKxCrGMo8PUY9XDaFzntHwNBLPDaCVchAJ6GxQ01L7UWxxgkG1UMk6T2Xdijm1-HeMnMdU7aCerTPv1dEgeCNPF8OQHs2F2vruasaLYiQxZYwopRVLg9bJ_bX3UxbNuy4zHZOfxRwkHajDubFjbBbMewcDYod2NTRLg0-FT9luqBWEeTsU54oFKxQ76G85OhnD5PrQZnWjpMGZwJs_CLCLKMMFwsT_xIyRcVwSah9NPX47lQZPGlsoqZQ6UYGeB2FP2rabLJuhSMMYxgEFwN4Tzj_wu2HklOyHrCsE1qYarcccRtefQZffL8OLTOzC_ZUfQeHJxaXq0gj8DO8jVYMz6Fy193oUL12RA95he_wCPrx4ZCCf9IOJFPNLN3WtMU4BpvtSQUdBpR9wUzgVJi1IyK8f3Kc-8k6Mzb0Q0qxdhXVaptflkhnofwjM47EbDpjYHqpmm4dQ5NgzdgkjJnjN8ak6GaglktFToqeRwUasfojGdw3deXHQlPD31NBpQLEJN8TG_wnvZznsDFigNEXArp3nJk5QGhEXmSpAa-NsYgRcJKgeCCg1OiXE8HKxm1ZQOfWxAWfcPaYBslHqgNbrzyHULMuAVMYbTgyhPirUNd6MSavBy-7kJQTYKMdxV70632QZlrBGjeJfgRFgbNCNe13NnhHiXufy7Xh6XgrLP-ln5LnR_mI9MDj4oXiCD-g_w_T4rIwJv31-JvAU8TUVw52WDw553dpgcuTNeM_6Pp3ecvy6wDjXheFsHQxodHvjVMeKQYjvTJPQWxoUjkabTRGNcwzgyx_3QXLSVpi1rHu5DS22Qt50GjNF_8MgMVrimg1ipCycScPM_TiSpjhWniqcMlsgEoYyHlicYjAnZAIbTREkplbyLZY7TmZ3l1FFGjdkTwYrqi0NlIz9kFOzQA_OzCqMS-it-t2ZTsMYkzlygxN265kEEcH-hMc4rs_MlT5eiCIARXeo7ribZrDEqNOBASoVFcow7ub7eC9Pou0T8D9O27e1nZRzZst9ns6n3JFeOjJAqgWlSj5oX-5Rmb0hoNTTXGw0QjbUd_veduNtrc9-saWO0pAaFcvqk0x0XPFw5DMLHBIL23OsOeO4OYKcnWQ8tyXOYP6Xdd2IreEghT85swv2dpvOJ3z0dovTL1DdIB2rjnPTrPl13SUAd8pEL6vUHYQgfTnCTf7DYrbWTKvSBB2Dawja91RQ0WwSSuNvQM2yhid500vEkaPKghLMgz7fkisuAXVknL9R9RNMMs62LdSWOIFLpvCqdoS_7l01QNREvtEBQcE1iF8ddViytEMkMnzndRPf_-kia0uB-OgcTc8dJjhR5_4pQAhhyP6iUJp9eDQPnwUF9uqOohMp7xDOdNR8Rzo--QNqEikhrdvfVjMRfCTnMMhF8crb3qwhrbnYPl_gH3hHIdhsd4Mce6OckEYPXDJCOQSdhSbd3GlKfcUAIZIWvtkbbJslhgZlbcRpg0nuAJH2JY_5F5gDVg_NhfJvMVMvlG1redukAgdZRCoUf1JJMyUfN3QZRjCpsTMiJ-gpwktmhIjim0sjoRaCDRMI_deJJa4BiwHLlUzIPzJPw2EL-Bj9TwI4gfAaMyVNEcjEw__7JvWlf-iyNJn4t76SU3a7FeINqHl6ThiOpQ4J12QD_kc1ERHSMcD7fwyrgdiTo-w4KABEA5bVWklk8ikqbT8sARYszZvoFHhhsogYnaG59PazLJ1Ht9mfNAVRLZoPrPpirGzghKvpyC2MamTdxgJkl_Kk2d3npzIrW62gXgZL93HPqwyZ1ugSVTDRuYUJDb8CxU-9MZ7Mk5OWgcyEbBNf2-ktDnqw3NEpiOoEFCPUyPquKbm0nj_kSt7JK3YnQAtaX19mAQyDYRxc6bgxdsb4mULZ40cbxpf9fQZrJrBDgTi-pzFLRNm9hzlb9Auhx_k8GsiVwmMy0kg3ct55Oc0V7ah5N6rKrVfY4rfV5yf576YdK9tSPsrgS8NsshhfLjXPTJacpQxQnJEcAu12I-7U-f0qj-ONtawvhwlftv2OHQ8jqfWUls_hRqmDJSh4zLzybFwkqIISEsWsQsIZYJ6p6GDtQFshTcYdpEUsbcxHygPcN-PWlJFxgv3HZ-mVY8GNRkkIDBKa5dApRHboIogqpa_Rvf855HlkuLMh7Z1v_nFn7QcQqzXcUYznLNuCOwjkDqNh4l8IQkVblRdoaFdS30oGYTfWAVMNrQiVT8MiIyOmN5tlAAcskvwEkJ4_z_GryxjRMN8mFa1k9EVq9SXH9boN5EVELeNOFTHd3hth6BqCnzezjXpbLyFtfKHEvpRSVOzgxdXX_bUavf8Ok2kqT18npTahgIbR_VR1l05cdx2UFzdKzlNWBhRPKHtRSv7FhGyFUy2NmjyOCKt_vbn6DGzWQ-tt7yRAZbXR1ANgibME0r8iqxAeRa6TspPLIMFid5kBQbkI_0FYl2gBvFy1
\ No newline at end of file
+xLt_RzqsalzTVuNW_Q5jyBhOjjS3pjWxhECuQN29OzXE5zkYC6Y9Ve-DHBubAQTkh__xWQI-a1GbaPAUaxJyoNuKDJFwS4WEPyZXFnW7b5TbaQJfPocu9oXFzvJS5h1awNl4Tod0smBcQKR9UvRUGSYJD6Nl4Du32igqG1ZoXprC2UKxOZpA2i1O-bkIfcdwbD0SqbDI49h-vPjl_lFal_wcbL_lKFOEGyRIhvCq_LsASYhUXYn9Qped6KVtY3zSmWpj4SIS_PQaJrAKfySfmXnk8KsFqxzl8OOKS9Z_Jv9kAS3yfLdXykpvsUNP-UdPLNhqGZndHtz5UP8IleFq37F47qRUUDFMGh1-yKaAMidsX8xnEp4gjkzd50g1JtXwd79KGiP1kLVOYv_fhmJ3z3Z5vFtdNuZwrJxth_zxajZu2UOd-pqbzv1EWxZy-oe6wSXRRncaZuD5ciC2JjLEKaouU8V3NtYICWvunTBZ5EAg74Bpb1G5k2KKSFBdGdG0Z-UeW8-1mnqguCXRKemG77o_V-YxNn1GF4Umo2yXsnFCPe0WI7W38Igt7xWbeBY6vRu074eXBYWfzwFZd_kDmq2MqXgJkclz-DCOYfUGaOfcZmeWhNcElDsATHY8577P5CsGes0I_ycVDoDGtHYl_vQa69GATrcUI4vtL1JM9Fsf_Vt_llnoh9c6MtAw1Gb5MuH1CT5oHbij7Jcs0RpMRUEi80nULMmFSNvAiEWAxMIZFxk4W17SJ3mfnFap8ESbmIH94PDV1ek7innnA3xbbf020l7A7IYOh7F9ETY3beXGY6vcaotNFcidkdj0insWdt_zxRTVQ3hJl3VGQT_S_WobGAU80uu83k-vBGczOMG3r2HRqSDdjP9Wn0b5U7i9xo25E8FYMtzc1D-kDUjr492ApXdxdG2CwdFxHgUialS8WLgu3saUj5BFinFMWEbZinnQ2UTFV6l0yWbEWNtaUlEWFWFoGlpXu8AIUvJ3V60gdABgc0_xhqmqzJNPe-p3j3CNh4VULw9za0xWRo_u0D7_zAMM0vd5zR3hMa3V23UA8SzNiqsDzWjFbT3VGGexysBGKc3LNW1rBPn13IdPtrgUMhURi2RcZuBX3Ve9Zdd3GztPwQl_FMqVlsKmJrAGvqYOJw0u9rnur3Ab2yBJWLqljEEbUspq7n8dHiBfrYq0usyLXayV_3mU8RuH2hU8uD665nKUgHS_IOL3JjcvdJXV9vg6rwxFke-P_SzgvhRN0UNgcroNW2AhX2SWlp9SNessT2E7087GIr9q_Jo71Rg7zE4rv3kIzqJBAzeOy3dCK0cApx_PVFM5Irqxxq1_oLCAfP5LQhkz07XRWQsDXNYvW5kGAPKnPN7glCOLL-Kjf6_kBW3bA4DxuIBMboDxi8K28LYB8KgCchOaUqgs59IYpLoFQJFuzX_ykuACIoMmHbhXiZUWOTLjYPZG7Qlnv0FEeOaa1tGB-GNPYmON2kvWBqq4vmCE7FeddjmZoYjGFAkLmQMbxNYlkqo5AeMFz3e5xr7VyDIsmm0PNGn8yGvh2-bYIKMPHAC4UtPDHbPv5i56_djGeFpvA6BVrrNcZdgrby7KsIUY7-p15d3u1h-6T0iDZmMd5JoGFry3kImhZ3Afyg_8rdVrcLdlZIwkpQeVqNgp9vh7bzDcmXcT8fjutA6Y4w9U3ZW8zTVet-xda9diL8jS9uRtxKcw5vCzu-mDVhil271wy0xR8Sxk8UPgNBn01U7dGDaFfn1pI6KxUIW9jAYCDBtDi8KI6vn7A9WYFaf0p9Jjt3suXLfwGc0NgS6syHkez1bHirh4vEpzuQTlBo_Fpgzlhb--FhkwFdbzuUf_v3qs4NURqm7GEDZs3RuzoUmFQn7p7iuzXnmnP5-XNw7vDU_WWwFv6vU4a650Gyn0Sl0G9Y8IeM7cp382HYUk0NiX953ogGNw2uJVFGNPkrQ_3BI30xaXK7suO2t19ys2y5BtwiWdLidYm6veA3S4maFwRTmWwVEo2LReZEtbyraQt9qAvpe3kABm2fMe01JnXnUKL7k_A6wu_bji9bwrQX_85GT_oOegX_dBktF2J_8IOKSZ_pu2rZHGTw6nxdbM-BgugC7zEy1UOhc210CXvvu7om9BJeAdEu2t7kD1o7ooTa1__jnZ-yimV7_bzfYJ8XzuqZg558aXQr4-aEw9lZV6mDqrSNZswEPLpwafVAYbkUWGffMrbo9gYoKpaE2zrm9Knhw1E_7vHe1YXt37XkmRPrZZ3AnrlIwLxZudhbrChu7XmEWIcw5FD6fs6uMRgWshqCb_BXwVH1372SpKnAxFvXCljxDwhvwhQQQRv13XaROU8hYD5ZHBeXrWExtJrvAUcuuJlVWroT4TMgqHPRofZWTCjBJBAuzpGFkXJJ_xQRxsAw_L1Q0Gyn2x8cHx_CTaEjUIV8-07d_oME_rzz0zfwIyAW7bR0SXl-RWeW1ONmEDRw6Zt1ruNxgrJY9_BWDHhn1-j-v91_-ct7PY14-zOPv8ATybt1SWWsKTu5pihdKINSbvUqYth2stP_DRNpgb3KF0K5MUlmXIAVKMmjiNgslYV4kSTf-eztiJw3wwuHFVuLPeXWesFN6PynbWN87YUOLpXBUl2UQx7iaiHwu1kbPZrwDkQ1qU4gIMGhT_L3wxsiCCWG8nVbO2L4nLSX92M4Q9fNqnAP5UsTe4LKVjfqJgLNCAfGViH-mAiQlWbEJEznqJYAR7fDf_4EVmbD_6yDJyWTp3qYtc7ZqSwvgW8J4CqmEjuFmyMVxiPyoUmlQwg6uBb1bx3tU0mloVI0vQvf6ybp9Qz0nmu1i9Kl6bv-EX-PTjfM3GZP1FUfn-zlBPsVdro-SlpvvzTdny99TrbjGtwfqZVvJapW9yHxeWcR6AeZ0ZD3ybbTgSL5TXsGwMeA_OsiTw-37zjpGef2TH3jyMGDvZ4Ji_uM9Hg6_KIe_JlzMUf0moDbp1lv7cDX3VFnzj8mlzDVjT3kyGHhh_IaPl8GQt1HpOSN37zrUGSuItnZjM5nOQb5SpV792RLu3zgvzSGzufwvxmlY5QZA4jsBlbYvwYzHhW0f8pLqijo84ozsxnhI9tTv6DZTCtauzK1W6SXbYxuTxalvCRXrR49-8RNd29xVObgEGLGLlToAoAXF8QGEcZXZc40ESGH_OE4NRBQJjjjksbjvfBPfjvmKSRWbzq22ViSr7UdfqLwt_j76bOBYh2YSIxL4mcMBWgYfBG17qdicD1R5hAjSMxaT_FTBUEL9hXNi4tx_CtGHUQ-VKHBs6IlFMKT3y8zqGfq6rt_g6WpmFEmB6A27b6xe56FrgxmB-TlCVtkOOB8dQY7Eb3Xhqo1Zp7iACqoeRVuFA4Q3Phpfje4k4dWKjvhi6VOxaYilRRBnJzQSrfIRSg4hxtb6hiX1EKtcXAXfgAzuxwYR9aVcx9uqAR3NKclUFmZRWI4frlAQPPImVySUWuuJ1wTLlHYY5KX-VGP2_GXWkzv_L2kp1lVL8wcT_5shoTuY9trwcud9L3bT19T_3Y2O-ubXyPyZKilmuLDXFsNB88IqHDizY6_qH0ofEUjntckO8xwweV7hZhcWsztNjK8xlyU_oCaeEqJ5chki_5KrUHGFNLZfxa7mqbRkg0vUFPvGqeL8lfMt4qnIrVlaJCuOVKJV_SCFYGSV3E10kQfj77JVYCYGkGnmhrXL7ENl5VFWnJYqlwi_nzaCjNjH_OjuiLUyQbV23p_HF7u9rewatAVzZ3DnQQwpOSInTUAn-yyUnTVJlMTlKwM4LBPtWbl4xChruG5orLSLXX7htGKZkl4-fQUrjdPP7IbDNKl6CwFitLIvapa6sAqsO0aC3gW4D6p50u_ePFHmwQAo3rntfUelnmuzr8Kw7LTq5Y8lOjvLWSa4l-VWM0dQsJBzUjArGBYAvi_HNF_3KDlQyT08VMog0yDrJbc1Bjp4FVESXjrvrdm9_uLSSxaEEyU3wdR6cgwjyMiTtUc3qZ-uGxZgwC_6YFBRJgsCtmMEamlFMAJr3llfvFMUhHaRAlHXUd3EVbaxtyNUspilo-CVPuJhZGEF7SbhisZegQfH_WFDYVKpz3eFv7UFI3t_DIOYwna7I1NUdXFQ2GvykquvnpqkUEljmsXsYZ9_M0w9FtPVuGXm-a9jRfskV4tub5Xb-SjJVvgN_dZABgjuYYyhU4elAlW8FdjWREgAdZILTHzOrUQMLc4S2lNcfro_4dWsP6jBFVH4sYBECZ4wmL-BvHtnlpOKwltC1AICOmsAfiMxJCx9EGpExJkWNziwHrFlR1SDUAYwLdNNPxc9uTwV6-qdt1tGUZb83ZqkPPVzsFTXJU8T-SUIMldCLDztJVIogRWlN0Rz377wURwnr3YmhLQqReJQTxEWvCyUJVq50Gy-MZLotzg66jh5pY7GqHN85aLKc3dnyTdmiVeDCvDgz1_G1uQqEmDPpxCHZ-VmkLJRVCqgDHSpGeOLr5xUbEoqpsSCsBxETVqPA-vJa3-rs_2RDbxFGUMonecW43t-FWlzfe18WF2SqHulsBMbDg3H5wfjQRIyX1sOUnAR4pu5NeX8wrT9jPOYyFjvNH8IaCFuOBd_mJVdionsnxWYkESik676yJVUgA9opcMqiTD-lxA83s8uvP1b5UsUlNjcol8BIhDIIbFeUl0mvEiXwNCRCpLxb0-jnds5zraISNclZIdzsJBpJ2C_97RncMjlPkMgxZqVbLKOebFbQGRPRTPGJRg6lB3kMWE9hEmrlRGFTsDOQ4vBrBc48Rb_4uKa1_Kwg7Y8SuvDiuabQGiRKtH1jR4vbysqE-ZQHmVQOR_TXDw9g4uptIDqmDq6TbHJu7b04f3be7cttDWC2WqA7d1I7vxwtJTB2bUsQ3n_v4opgizDlxY6bA8Thzv6gOp7nIdE8-r09GUz1KS5-v41dxQEnygM-AWQSS0VT4UZRuHJTuItvVPlyKQq7Fj1enyaxwBBbk6ifuTjyRHBN03oc0TYH4buU-4wb7PDNzj218KmitrwHhLe6hk-fSVkv2h3jWiRfSNfoUNdohnz_QkOL2TwDh7KayWA8JpYf-8vGwK5sP0veFj9rVv7JARGZ69uxyqCP8MnD4X6l1lXUvhD0ZAECDNl8RuHDSLEAErfkI3sCYV5Sf8swAG5z9ok8EL9cFawqorLlP9ChfqAI_hi21U8FDu2nYegz9JSvOBjR0cZUX0j1eRXf3pW520jQQWiMqvur0NNzCsJqN1OqkjhSNxFKbxlq7Xp4bBdgZaOzOaAv7qwUrQj23r2w9IIXFP_43oVFKS4XzOl2LoLsBelb3gpHsoz-E7brObzwvk4r4-RyZLP3bfN53M2zejk7Nb47vCIK8-h-8jbKbL8DMQJlWEWmj8nm3D_VKFB-nbnva29tbDRBl1k6jewMU6LkugZZeZJ2D0hJm6QI25KVAikR4o7gGpHsJqZ4ousNm_CVeN935I4QXQZR8LXlUOvuUU9btuSNqHf1Ayw3_bZ5mJqCBzkp6y4Q0IlYD8fJx7jUC4LuB7X6hkAE0Da0MJTJH80KGACtQqUGN215W7e0nLM90vPdprvvG987V5XkVPYuu1xW_8Q3RSzNBofB4DjF6_ncTPJe1kLG7WNgRpTLpvrnIvMG_iOPt9h09-WMywXIBiWYG1P0SG9pW39o1Na4g04eHWEc48hBr15mng_UZZWDu1GnSc7alcmx0jG0D7uhTiJ9ncaiAKH4oqOhpdCU84Lm7E14K6nG1kz3ZlYq-0nVdnn1Se4K0N81557feJ9ykMv4g4KeAiyWD14L2vN1cteZF1qy4Pf1Q28i5YmXr-7-wP09kVRsJmVFv5su4ISJfu5RN2DCJfWSLuH6f4QGSjzd6I6RXYogs2HUKQjyK79SJ16H6a8gZbL1MCL1QXAeYD0WGkxx3YDA9af3bob6d5muHMNo02bET85GnNutXLC4Q12WWNlk8ankc2945aJn_4SWH14U6vxXM-yYG3f04HWBcAaxwo9A1afn5-WKGX4P6ndttLiH706Eywwl28s4Pl2YhuYFXa_K6mkNc7vs69bj76XSiCH5WniOntrM4Pa6cGYA1OfzGrP4HQ8oTUTMXCaDqyIrN14sDsmH8XkYaVkz8a8AeVxrO14PWKoqVboeEAu3Yy0qm0pV9XOniZ2As3Ym6Rx3B6BKO1H1iK3fNw0LSH6ukE_HYjWeREoRmVxKk2IVfIUCaGofUqcI997hzFJegwFqooz__Cjj0R9tctq14Dxm7ShUOSZOVt_zqvTVoYhQ4M0XL_jh8w-9JjJfCrzYTlQKz3ZTYgAD-jCzIPAHkx8iRk9IFfXRx7vBqoDybFOIYgV7uZeuisoJWa5cYUCgd03-KtSqfSSMRr5buoWpNrjTgjPLx2rG-qOoT4N2R87wb-dqf6ghR8OUjEyUKZwL9dgETEhD0yhMzDXGoUHx1vHhMDEcPNiCdLChund2gHSjoUO9rCJvUbBep9jAgt0qhcrqiMsUhSRD7D77FhHNUpKPt7raazBkwMgyr1WzzPsorceyjNunnRYhkXhtuPvkCn_gro6ysus0wN7AzZ13e42CjwMYKUiUevMPOYIzspLcMXhgtUFwLb_xoKQMFwUaxBspI89UKcnXNLbb6yFw4HoMiUnbf98MdLMuCbFtpiRqkCxgRprmChUklmXgPiVz8jAeDCitlH8rd3GcmugsHoR_cpGrmZbtB3g5LQmQnHQ_abuA_m0M7ZScY3MIgBLHVyOhBnB64NCIWubUocefd6key8ddJYwazEbc6TsUzQSpi4oyK4v3cgzOkBKzbCT0JBbh1VcpNdek9-wCn0fchl9cmUj8-HquIBiogzTZjVLeuofaoL36w7KNfdj9jD6URksv0lS9VYvEeUIT4YgGcqURckKwZFfRoNlijgFLLv52Dxg6cNS6rnAU3Gzc4a-MkoCQofkKKE5p0yMG6q8QTe0njCKmTn0A5cR8YjhqRAbvj7jghuYRIXzAr6vr9QpPoYkEKivXQJxSlU5KJ2fjcm-EWC5iz4i8sYzXUkaikdBi0XJYsbUMxL2HpqF3MB2rvcpTluAfzJU8YBbDbBp1SAF-ktvF57LP9BlnUvBq_BJ-J8Vok0ahSk1Ht_kx2dyp_OB1ttmqmD_EOXsqBdAztzdHrgF6eBQALnUZzRVhPPjUKSTxiVkgzkuxl5R1zDi1g0uOg0uMJCK12LK_ynUAilhPP3dPc8zDvCog-hCvahMvYvj5jFfLDr0uHVkWYRAnZAAbj5b7SxwS5SxX7S8mxzIpKBxx7MgzDF15xqaIRdqF-YisVRV5NFfc6sxqfapqQ9e_MIjBiHM7G-RJcaSuVPFsHkF14YjvQSXuRBrvd7AAjbj8OVxqOTFzI3po4CwQ1kpyZyP4qCiVjka_RDzy717RB88kGQqgZVaY9ortbhqb0hMQVDSHT01Kw_Rw_4VRBBor5ilJ9heOq27rSAF3TG0fdDodkAwaegr6iB4UCwGGAmanra7yUiPAWWorX9ErdZDjbIROTWHzsc0n_m1xkdHnJPmPlzPUMdIaDuCBZXKb6HsftBAC35HBfPbi8biMyuNL9N69m3PEgPAHL6SB972E5UQZWVwwfXG3E3Zv6acdrfgjHeVhjkAgKhWdIssMrBxUoWAqUW096wL-aQJvCld9W0_8imi-drjS6m-4LJ_fmkVX8NVT-uZhiatxN-U5T7Z8LtIr4xjmqj--I9v1LrrFZcJDxKa6jyuzFhqzQSHHhktzc2NgT41-uuxDB-NNULwrxvVkL_sJz3GjI_rPf2NsuFHgpMLcjg_Ef5EjfEVFMUGAQaQ2YvvBI8qSTddIkdpMy41jUgweH0DwVbHQ-VDgGlsmD9l7PI1AeJ5oHYdcyMhiUvswCkPdqND70Uf2VLzKKgVHc3r8AQ453NiKOZDecv79Zh8-gC-Ys-5R4sp03QNad8OQggG_tuHpqB8eQQJtNUaCceqzKec_ctH7IgY89DqyljPMSbh_Fxw1V3bVv-xY8UQExSJB9V1Xl8pVCBRRnMWExI0mUVHt3ypXvR8QEpvzgr4TsrXsv8CIjKV9UX9CkqiSqnAbRL4mjVQuSTqRwyCeWcAYSexC7YgOUDqSALoxsrRScTNixDKtQgtESt33bbC6P-saRh_r3WfmyS_KiO1WheQgrImqNTDF6mUgd7sGM-8dYpPI3UtdYPenrZXM8CWt6yhQyeNqxKM7piDSR2pZ8xUnLtmdjXGNWF6t-rmS91H1B5eREQ4CNCPhmw9lkOPIztEDABXy72AX_5qxgHIt_awgERJj_JmTRUD9E6rNaBWcteyHXr8VQmMy0kg3Yt75Oc0T7ih5d6qKQNso2QNNPMeH6zefL3ltMLiQJD2UMpVT33kBbcFGxCWk7Z5T-H5Q3lVMWwGv5B_mTCfzJQP_Gc8MY_QLnlNuVKk_SJKq9HEdU_5H-hl6at3kuDdCaOqbnipF63WYwq_QOPqodzDQkaVBcvjzc8FrzkolmaRVidyW5bwuhSlIj99OoCcsPSWj8MZjdxRT9Gi86-7XLKaFkBp_YVYELCrkRBEyLxYgF8QpLVURedKPUIcry_3UN7t8V4u61kX4xR2Kzilgv0T38UkoCHnNrpjAgkskPsDkpCSzUyryxZQM788Fi1kfENq8SbG8ro75kQDL8LOFzPv3RngZX-5OkyQMlVng-BoKgEbSXhlFKStjByp_YDHS5WDdXPOE8gOxUoKpPQkVFfZNmUmTrlC7czg_7jp4jZYgONiXyhdqy704V2dyZHFJZ6_E5LIB0BRUVlzkhFo8jKTGcYvR5JWWnJ8hY-eUrBdVfeWqpiwvLDIt9Sa_8CwhiaZ-7m00
\ No newline at end of file
diff --git a/docs/logical_data_model.puml b/docs/logical_data_model.puml
index c8362a2e39..3e79647e78 100644
--- a/docs/logical_data_model.puml
+++ b/docs/logical_data_model.puml
@@ -2508,7 +2508,7 @@ Grants "1" --[#black,plain,thickness=2]-- "1" GrantNumberLinks : grant, grantNum
ActivityReportCollaborators "1" --[#black,dashed,thickness=2]--{ "n" CollaboratorRoles : collaboratorRoles, activityReportCollaborator
ActivityReportGoals "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportGoalFieldResponses : activityReportGoal, activityReportGoalFieldResponses
ActivityReportGoals "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportGoalResources : activityReportGoal, activityReportGoalResources
-ActivityReportObjectives "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportObjectiveCitations : activityReportObjective, activityReportObjectiveCitation, activityReportObjectiveCitations
+ActivityReportObjectives "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportObjectiveCitations : activityReportObjective, activityReportObjectiveCitations
ActivityReportObjectives "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportObjectiveCourses : activityReportObjective, activityReportObjectiveCourses
ActivityReportObjectives "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportObjectiveFiles : activityReportObjective, activityReportObjectiveFiles
ActivityReportObjectives "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportObjectiveResources : activityReportObjective, activityReportObjectiveResources
diff --git a/src/models/activityReportObjective.js b/src/models/activityReportObjective.js
index 812c8d8e03..ca1018444b 100644
--- a/src/models/activityReportObjective.js
+++ b/src/models/activityReportObjective.js
@@ -16,7 +16,6 @@ export default (sequelize, DataTypes) => {
ActivityReportObjective.hasMany(models.ActivityReportObjectiveTopic, { foreignKey: 'activityReportObjectiveId', as: 'activityReportObjectiveTopics' });
ActivityReportObjective.hasMany(models.ActivityReportObjectiveResource, { foreignKey: 'activityReportObjectiveId', as: 'activityReportObjectiveResources' });
ActivityReportObjective.hasMany(models.ActivityReportObjectiveCourse, { foreignKey: 'activityReportObjectiveId', as: 'activityReportObjectiveCourses' });
- ActivityReportObjective.hasMany(models.ActivityReportObjectiveCitation, { foreignKey: 'activityReportObjectiveId', as: 'activityReportObjectiveCitations' });
ActivityReportObjective.belongsToMany(models.File, {
through: models.ActivityReportObjectiveFile,
diff --git a/src/models/activityReportObjectiveCitation.js b/src/models/activityReportObjectiveCitation.js
index 850d9226e7..1a93e37e3d 100644
--- a/src/models/activityReportObjectiveCitation.js
+++ b/src/models/activityReportObjectiveCitation.js
@@ -15,8 +15,12 @@ export default (sequelize, DataTypes) => {
as: 'activityReportObjective',
});
- // Citation (standard).
- ActivityReportObjectiveCitation.belongsTo(models.ActivityReportObjective, { foreignKey: 'activityReportObjectiveId', as: 'activityReportObjectiveCitation' });
+ // ARO Citations.
+ models.ActivityReportObjective.hasMany(models.ActivityReportObjectiveCitation, {
+ foreignKey: 'activityReportObjectiveId',
+ onDelete: 'cascade',
+ as: 'activityReportObjectiveCitations',
+ });
}
}
ActivityReportObjectiveCitation.init({
From d66f7ce182eef6d1f9781de67a3e44f046772ac6 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Mon, 2 Dec 2024 16:20:52 -0500
Subject: [PATCH 042/198] update alerts to require start date
---
.../Pages/__tests__/goalsObjectives.js | 43 ++++++++++++++----
.../ActivityReport/Pages/goalsObjectives.js | 44 +++++++++++++++----
src/models/activityReportObjectiveCitation.js | 9 ++--
3 files changed, 75 insertions(+), 21 deletions(-)
diff --git a/frontend/src/pages/ActivityReport/Pages/__tests__/goalsObjectives.js b/frontend/src/pages/ActivityReport/Pages/__tests__/goalsObjectives.js
index bd30dc0d3e..93fd18f625 100644
--- a/frontend/src/pages/ActivityReport/Pages/__tests__/goalsObjectives.js
+++ b/frontend/src/pages/ActivityReport/Pages/__tests__/goalsObjectives.js
@@ -22,12 +22,12 @@ const goalUrl = join('api', 'activity-reports', 'goals');
const spy = jest.fn();
const RenderGoalsObjectives = ({
- grantIds, activityRecipientType, connectionActive = true,
+ grantIds, activityRecipientType, connectionActive = true, startDate = null,
}) => {
const activityRecipients = grantIds.map((activityRecipientId) => ({
activityRecipientId, id: activityRecipientId,
}));
- const data = { activityRecipientType, activityRecipients };
+ const data = { activityRecipientType, activityRecipients, startDate };
const hookForm = useForm({
mode: 'onChange',
defaultValues: {
@@ -100,6 +100,7 @@ const renderGoals = (
isGoalFormClosed = false,
throwFetchError = false,
toggleGoalForm = jest.fn(),
+ startDate = null,
) => {
const query = grantIds.map((id) => `grantIds=${id}`).join('&');
const fetchResponse = throwFetchError ? 500 : goals;
@@ -112,6 +113,7 @@ const renderGoals = (
grantIds={grantIds}
activityRecipientType={activityRecipientType}
connectionActive={!throwFetchError}
+ startDate={startDate}
/>
,
@@ -145,14 +147,14 @@ describe('goals objectives', () => {
const isGoalFormClosed = false;
const throwFetchError = true;
- renderGoals([1], 'recipient', goals, isGoalFormClosed, throwFetchError);
+ renderGoals([1], 'recipient', goals, isGoalFormClosed, throwFetchError, jest.fn(), '2021-01-01');
expect(await screen.findByText('Connection error. Cannot load options.')).toBeVisible();
});
});
describe('when activity recipient type is "recipient"', () => {
it('the display goals section is displayed', async () => {
- renderGoals([1], 'recipient');
+ renderGoals([1], 'recipient', [], false, false, jest.fn(), '2021-01-01');
expect(await screen.findByText('Goal summary', { selector: '.margin-bottom-0.margin-top-4' })).toBeVisible();
expect(screen.queryByText(/indicates required field/i)).toBeTruthy();
});
@@ -279,17 +281,40 @@ describe('goals objectives', () => {
const grants = [1];
const isGoalFormClosed = false;
const throwFetchError = true;
- renderGoals(grants, recipientType, goals, isGoalFormClosed, throwFetchError);
+ renderGoals(grants, recipientType, goals, isGoalFormClosed, throwFetchError, jest.fn(), '2021-01-01');
expect(await screen.findByText('Connection error. Cannot load options.')).toBeVisible();
});
});
describe('when activity recipient type is not "recipient" or "other-entity"', () => {
- it('a warning is displayed', async () => {
+ it('both warnings are displayed when the report type and start date are missing', async () => {
renderGoals([1], null);
- expect(await screen.findByText(
- /To add goals and objectives, indicate who the activity was for in/i,
- )).toBeVisible();
+ expect(await screen.findByText('To add goals and objectives, indicate in the')).toBeVisible();
+ expect(await screen.findByText('who the activity was for')).toBeVisible();
+ expect(await screen.findByText('the start date of the activity')).toBeVisible();
+ });
+
+ it('shows the report type warning when the report type is missing', async () => {
+ renderGoals([1], null, [], false, false, jest.fn(), '2021-01-01');
+ expect(await screen.findByText('To add goals and objectives, indicate in the')).toBeVisible();
+ expect(await screen.findByText('who the activity was for')).toBeVisible();
+ expect(screen.queryByText('the start date of the activity')).toBeNull();
+ });
+
+ it('shows the start date warning when the start date is missing', async () => {
+ renderGoals([1], 'recipient', [], false, false, jest.fn());
+ expect(await screen.findByText('To add goals and objectives, indicate in the')).toBeVisible();
+ expect(screen.queryByText('who the activity was for')).toBeNull();
+ expect(await screen.findByText('the start date of the activity')).toBeVisible();
+ });
+
+ it('hides the warnings when the report type and start date are present', async () => {
+ renderGoals([1], 'recipient', [], false, false, jest.fn(), '2021-01-01');
+ expect(screen.queryByText('To add goals and objectives, indicate in the')).toBeNull();
+ expect(screen.queryByText('who the activity was for')).toBeNull();
+ expect(screen.queryByText('the start date of the activity')).toBeNull();
+ // Expect to find an h3 with the text "Goal summary".
+ expect(await screen.findByText('Goal summary', { selector: '.margin-bottom-0.margin-top-4' })).toBeVisible();
});
});
diff --git a/frontend/src/pages/ActivityReport/Pages/goalsObjectives.js b/frontend/src/pages/ActivityReport/Pages/goalsObjectives.js
index 722fb8d349..48ddf6e494 100644
--- a/frontend/src/pages/ActivityReport/Pages/goalsObjectives.js
+++ b/frontend/src/pages/ActivityReport/Pages/goalsObjectives.js
@@ -102,6 +102,7 @@ const GoalsObjectives = ({
const activityRecipientType = watch('activityRecipientType');
const activityRecipients = watch('activityRecipients');
const objectivesWithoutGoals = watch('objectivesWithoutGoals');
+ const startDate = watch('startDate');
const pageState = getValues('pageState');
const {
@@ -314,6 +315,36 @@ const GoalsObjectives = ({
isOtherEntityReport && !isObjectivesFormClosed
);
+ const determineReportTypeAlert = () => {
+ const messages = [];
+
+ // Check that the report type is set.
+ if (!isOtherEntityReport && !isRecipientReport) {
+ messages.push('who the activity was for');
+ }
+ // Check the startDate is set.
+ if (!startDate) {
+ messages.push('the start date of the activity');
+ }
+
+ if (messages.length > 0) {
+ return (
+
+ To add goals and objectives, indicate in the
+ {' '}
+ Activity Summary
+ {' '}
+
+ {messages.map((message) => (
+ {message}
+ ))}
+
+
+ );
+ }
+ return null;
+ };
+
return (
<>
@@ -323,14 +354,9 @@ const GoalsObjectives = ({
) }
- {(!isOtherEntityReport && !isRecipientReport) && (
-
- To add goals and objectives, indicate who the activity was for in
- {' '}
- Activity Summary
- .
-
- )}
+ {
+ determineReportTypeAlert()
+ }
{/**
* on non-recipient reports, only objectives are shown
@@ -378,7 +404,7 @@ const GoalsObjectives = ({
* conditionally show the goal picker
*/}
- {showGoals && !isGoalFormClosed
+ {showGoals && !isGoalFormClosed && startDate
? (
<>
Goal summary
diff --git a/src/models/activityReportObjectiveCitation.js b/src/models/activityReportObjectiveCitation.js
index 850d9226e7..c948593611 100644
--- a/src/models/activityReportObjectiveCitation.js
+++ b/src/models/activityReportObjectiveCitation.js
@@ -14,9 +14,12 @@ export default (sequelize, DataTypes) => {
onDelete: 'cascade',
as: 'activityReportObjective',
});
-
- // Citation (standard).
- ActivityReportObjectiveCitation.belongsTo(models.ActivityReportObjective, { foreignKey: 'activityReportObjectiveId', as: 'activityReportObjectiveCitation' });
+ // ARO Citations.
+ models.ActivityReportObjective.hasMany(models.ActivityReportObjectiveCitation, {
+ foreignKey: 'activityReportObjectiveId',
+ onDelete: 'cascade',
+ as: 'activityReportObjectiveCitations',
+ });
}
}
ActivityReportObjectiveCitation.init({
From 911ed42f4eb88677e54a01122ab2eb00411ce19e Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Tue, 3 Dec 2024 11:16:36 -0500
Subject: [PATCH 043/198] Decompose types
---
src/services/monitoring.ts | 97 +++-----------------------------
src/services/types/monitoring.ts | 93 ++++++++++++++++++++++++++++++
2 files changed, 100 insertions(+), 90 deletions(-)
create mode 100644 src/services/types/monitoring.ts
diff --git a/src/services/monitoring.ts b/src/services/monitoring.ts
index 65354913f3..ff96012832 100644
--- a/src/services/monitoring.ts
+++ b/src/services/monitoring.ts
@@ -1,5 +1,12 @@
import moment from 'moment';
import db from '../models';
+import {
+ ITTAByReviewResponse,
+ ITTAByCitationResponse,
+ IMonitoringResponse,
+ IMonitoringReviewGrantee,
+ IMonitoringReview,
+} from './types/monitoring';
const {
Grant,
@@ -12,89 +19,6 @@ const {
MonitoringClassSummary,
} = db;
-interface IMonitoringReview {
- reportDeliveryDate: Date;
- id: number;
- reviewType: string;
- statusLink: {
- status: {
- name: string;
- }
- };
-}
-
-interface IMonitoringReviewGrantee {
- id: number;
- grantId: number;
- reviewId: number;
- monitoringReviewLink: {
- monitoringReviews: IMonitoringReview[];
- }
-}
-
-interface IMonitoringResponse {
- recipientId: number;
- regionId: number;
- reviewStatus: string;
- reviewDate: string;
- reviewType: string;
- grant: string;
-}
-
-interface ITTAByReviewObjective {
- title: string;
- activityReportIds: string[];
- endDate: string;
- topics: string[];
- status: string;
-}
-
-interface ITTAByCitationReview {
- name: string;
- reviewType: string;
- reviewReceived: string;
- outcome: string;
- findingStatus: string;
- specialists: {
- name: string;
- roles: string[];
- }[];
- objectives: ITTAByReviewObjective[];
-}
-
-interface ITTAByReviewFinding {
- citation: string;
- status: string;
- type: string;
- category: string;
- correctionDeadline: string;
- objectives: ITTAByReviewObjective[];
-}
-
-interface ITTAByReviewResponse {
- name: string;
- reviewType: string;
- reviewReceived: string;
- findings: ITTAByReviewFinding[];
- grants: string[];
- outcome: string;
- lastTTADate: string | null;
- specialists: {
- name: string;
- roles: string[];
- }[];
-}
-
-interface ITTAByCitationResponse {
- citationNumber: string;
- findingType: string;
- status: string;
- category: string;
- grantNumbers: string[];
- lastTTADate: string | null;
- reviews: ITTAByCitationReview[];
-}
-
export async function ttaByReviews(
_recipientId: number,
_regionId: number,
@@ -535,13 +459,6 @@ export async function monitoringData({
return b;
}, monitoringReviews[0]);
- // from the most recent review, get the status via the statusLink
- const { monitoringReviewStatuses } = monitoringReview.statusLink;
-
- // I am presuming there can only be one status linked to a review
- // as that was the structure before tables were refactored
- const [status] = monitoringReviewStatuses;
-
return {
recipientId: grant.recipientId,
regionId: grant.regionId,
diff --git a/src/services/types/monitoring.ts b/src/services/types/monitoring.ts
new file mode 100644
index 0000000000..88ed03ce8e
--- /dev/null
+++ b/src/services/types/monitoring.ts
@@ -0,0 +1,93 @@
+interface IMonitoringReview {
+ reportDeliveryDate: Date;
+ id: number;
+ reviewType: string;
+ statusLink: {
+ status: {
+ name: string;
+ }
+ };
+}
+
+interface IMonitoringReviewGrantee {
+ id: number;
+ grantId: number;
+ reviewId: number;
+ monitoringReviewLink: {
+ monitoringReviews: IMonitoringReview[];
+ }
+}
+
+interface IMonitoringResponse {
+ recipientId: number;
+ regionId: number;
+ reviewStatus: string;
+ reviewDate: string;
+ reviewType: string;
+ grant: string;
+}
+
+interface ITTAByReviewObjective {
+ title: string;
+ activityReportIds: string[];
+ endDate: string;
+ topics: string[];
+ status: string;
+}
+
+interface ITTAByCitationReview {
+ name: string;
+ reviewType: string;
+ reviewReceived: string;
+ outcome: string;
+ findingStatus: string;
+ specialists: {
+ name: string;
+ roles: string[];
+ }[];
+ objectives: ITTAByReviewObjective[];
+}
+
+interface ITTAByReviewFinding {
+ citation: string;
+ status: string;
+ type: string;
+ category: string;
+ correctionDeadline: string;
+ objectives: ITTAByReviewObjective[];
+}
+
+interface ITTAByReviewResponse {
+ name: string;
+ reviewType: string;
+ reviewReceived: string;
+ findings: ITTAByReviewFinding[];
+ grants: string[];
+ outcome: string;
+ lastTTADate: string | null;
+ specialists: {
+ name: string;
+ roles: string[];
+ }[];
+}
+
+interface ITTAByCitationResponse {
+ citationNumber: string;
+ findingType: string;
+ status: string;
+ category: string;
+ grantNumbers: string[];
+ lastTTADate: string | null;
+ reviews: ITTAByCitationReview[];
+}
+
+export {
+ IMonitoringReview,
+ IMonitoringReviewGrantee,
+ IMonitoringResponse,
+ ITTAByReviewObjective,
+ ITTAByCitationReview,
+ ITTAByReviewFinding,
+ ITTAByReviewResponse,
+ ITTAByCitationResponse,
+};
From 24ef526e665abfffb431a05957f917dafe532ff4 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Tue, 3 Dec 2024 16:02:15 -0500
Subject: [PATCH 044/198] hooking up ar to citations
---
...veTopics.js => GenericSelectWithDrawer.js} | 52 +++++++++-------
...veTopics.js => GenericSelectWithDrawer.js} | 13 ++--
frontend/src/components/selectOptionsReset.js | 10 ++++
frontend/src/fetchers/__tests__/citations.js | 14 +++++
frontend/src/fetchers/activityReports.js | 2 +-
frontend/src/fetchers/citations.js | 18 ++++++
.../Pages/components/GoalForm.js | 7 +++
.../Pages/components/GoalPicker.js | 46 ++++++++++++++-
.../Pages/components/Objective.js | 59 +++++++++++++++++--
.../Pages/components/Objectives.js | 10 ++++
.../components/__tests__/goalValidator.js | 13 ++++
.../Pages/components/goalValidator.js | 6 ++
.../ActivityReport/Pages/goalsObjectives.js | 2 +-
.../pages/ActivityReport/__tests__/index.js | 2 +-
src/goalServices/goals.js | 10 +++-
src/models/activityReportObjective.js | 1 -
src/routes/apiDirectory.js | 2 +
src/routes/citations/handlers.js | 7 +--
src/routes/citations/handlers.test.js | 6 +-
src/routes/citations/index.js | 10 +++-
src/services/citations.js | 16 ++++-
21 files changed, 254 insertions(+), 52 deletions(-)
rename frontend/src/components/GoalForm/{ObjectiveTopics.js => GenericSelectWithDrawer.js} (61%)
rename frontend/src/components/GoalForm/__tests__/{ObjectiveTopics.js => GenericSelectWithDrawer.js} (87%)
create mode 100644 frontend/src/fetchers/__tests__/citations.js
create mode 100644 frontend/src/fetchers/citations.js
diff --git a/frontend/src/components/GoalForm/ObjectiveTopics.js b/frontend/src/components/GoalForm/GenericSelectWithDrawer.js
similarity index 61%
rename from frontend/src/components/GoalForm/ObjectiveTopics.js
rename to frontend/src/components/GoalForm/GenericSelectWithDrawer.js
index 7bb5ba7971..b7a4aa0f37 100644
--- a/frontend/src/components/GoalForm/ObjectiveTopics.js
+++ b/frontend/src/components/GoalForm/GenericSelectWithDrawer.js
@@ -10,17 +10,21 @@ import Req from '../Req';
import ContentFromFeedByTag from '../ContentFromFeedByTag';
import DrawerTriggerButton from '../DrawerTriggerButton';
-export default function ObjectiveTopics({
+export default function GenericSelectWithDrawer({
error,
- topicOptions,
- validateObjectiveTopics,
- topics,
- onChangeTopics,
+ name,
+ options,
+ validateValues,
+ values,
+ onChangeValues,
inputName,
isLoading,
}) {
const drawerTriggerRef = useRef(null);
- topicOptions.sort((a, b) => a.name.localeCompare(b.name));
+ if (options && options.length > 0) {
+ options.sort((a, b) => (a.name || '').localeCompare(b.name || ''));
+ }
+ const nameToLower = name ? name.toLowerCase() : '';
return (
<>
@@ -28,21 +32,25 @@ export default function ObjectiveTopics({
triggerRef={drawerTriggerRef}
stickyHeader
stickyFooter
- title="Topic guidance"
+ title={`${name} guidance`}
>
-
+
<>
- Topics
+ {name}
+ s
{' '}
>
- Get help choosing topics
+ Get help choosing
+ {' '}
+ {name}
+ s
{error}
@@ -56,10 +64,10 @@ export default function ObjectiveTopics({
}}
className="usa-select"
isMulti
- options={topicOptions}
- onBlur={validateObjectiveTopics}
- value={topics}
- onChange={onChangeTopics}
+ options={options}
+ onBlur={validateValues}
+ value={values}
+ onChange={onChangeValues}
closeMenuOnSelect={false}
isDisabled={isLoading}
getOptionLabel={(option) => option.name}
@@ -71,23 +79,23 @@ export default function ObjectiveTopics({
);
}
-ObjectiveTopics.propTypes = {
+GenericSelectWithDrawer.propTypes = {
+ name: PropTypes.string.isRequired,
error: PropTypes.node.isRequired,
- topicOptions: PropTypes.arrayOf(PropTypes.shape({
+ options: PropTypes.arrayOf(PropTypes.shape({
label: PropTypes.string,
value: PropTypes.number,
})).isRequired,
- validateObjectiveTopics: PropTypes.func.isRequired,
- topics: PropTypes.arrayOf(PropTypes.shape({
+ validateValues: PropTypes.func.isRequired,
+ values: PropTypes.arrayOf(PropTypes.shape({
label: PropTypes.string,
value: PropTypes.number,
})).isRequired,
- onChangeTopics: PropTypes.func.isRequired,
- inputName: PropTypes.string,
+ onChangeValues: PropTypes.func.isRequired,
+ inputName: PropTypes.string.isRequired,
isLoading: PropTypes.bool,
};
-ObjectiveTopics.defaultProps = {
- inputName: 'topics',
+GenericSelectWithDrawer.defaultProps = {
isLoading: false,
};
diff --git a/frontend/src/components/GoalForm/__tests__/ObjectiveTopics.js b/frontend/src/components/GoalForm/__tests__/GenericSelectWithDrawer.js
similarity index 87%
rename from frontend/src/components/GoalForm/__tests__/ObjectiveTopics.js
rename to frontend/src/components/GoalForm/__tests__/GenericSelectWithDrawer.js
index 1a57df76a4..d80f999dfb 100644
--- a/frontend/src/components/GoalForm/__tests__/ObjectiveTopics.js
+++ b/frontend/src/components/GoalForm/__tests__/GenericSelectWithDrawer.js
@@ -4,7 +4,7 @@ import {
render, screen,
} from '@testing-library/react';
import fetchMock from 'fetch-mock';
-import ObjectiveTopics from '../ObjectiveTopics';
+import GenericSelectWithDrawer from '../GenericSelectWithDrawer';
describe('ObjectiveTopics', () => {
beforeEach(() => fetchMock.get('/api/feeds/item?tag=ttahub-topic', `
@@ -33,12 +33,13 @@ describe('ObjectiveTopics', () => {
goalStatus = 'In Progress',
userCanEdit = true,
) => render((
- >}
- topicOptions={[]}
- validateObjectiveTopics={jest.fn()}
- topics={topics}
- onChangeTopics={jest.fn()}
+ name="topic"
+ options={[]}
+ validateValues={jest.fn()}
+ values={topics}
+ onChangeValues={jest.fn()}
status={objectiveStatus}
isOnReport={isOnReport}
goalStatus={goalStatus}
diff --git a/frontend/src/components/selectOptionsReset.js b/frontend/src/components/selectOptionsReset.js
index 82a7461294..ba332d6e54 100644
--- a/frontend/src/components/selectOptionsReset.js
+++ b/frontend/src/components/selectOptionsReset.js
@@ -1,6 +1,7 @@
// this is an importable config object for the react select/emotion
// to use this, give the select the "usa-select" classname and pass this as
// its style object, its meant to match the USDWS styles
+import colors from '../colors';
const selectOptionsReset = {
container: (provided, state) => {
@@ -13,6 +14,15 @@ const selectOptionsReset = {
height: 'auto',
};
},
+ groupHeading: (provided) => ({
+ ...provided,
+ fontWeight: 'bold',
+ fontFamily: 'SourceSansPro',
+ textTransform: 'capitalize',
+ fontSize: '14px',
+ color: colors.smartHubTextInk,
+ lineHeight: '22px',
+ }),
control: (provided, state) => {
const selected = state.getValue();
return {
diff --git a/frontend/src/fetchers/__tests__/citations.js b/frontend/src/fetchers/__tests__/citations.js
new file mode 100644
index 0000000000..f6d5e60b61
--- /dev/null
+++ b/frontend/src/fetchers/__tests__/citations.js
@@ -0,0 +1,14 @@
+// import join from 'url-join';
+import fetchMock from 'fetch-mock';
+
+import { fetchCitationsByGrant } from '../citations';
+
+describe('Citations fetcher', () => {
+ beforeEach(() => fetchMock.reset());
+
+ it('fetches citations', async () => {
+ fetchMock.get('/api/citations/region/1?grantIds=1&2&reportStartDate=2024-12-03', []);
+ await fetchCitationsByGrant(1, [1, 2], '2024-12-03');
+ expect(fetchMock.called()).toBeTruthy();
+ });
+});
diff --git a/frontend/src/fetchers/activityReports.js b/frontend/src/fetchers/activityReports.js
index 0c02becc1a..ab8292a54b 100644
--- a/frontend/src/fetchers/activityReports.js
+++ b/frontend/src/fetchers/activityReports.js
@@ -109,7 +109,7 @@ export const getRecipientsForExistingAR = async (reportId) => {
};
export const getGoals = async (grantIds, reportStartDate = null) => {
- const reportStartDateParam = reportStartDate ? `&reportStartDate=${reportStartDate}` : '';
+ const reportStartDateParam = reportStartDate ? `&reportStartDate=${new Date(reportStartDate).toISOString().split('T')[0]}` : '';
const params = grantIds.map((grantId) => `grantIds=${grantId}${reportStartDateParam}`);
const url = join(activityReportUrl, 'goals', `?${params.join('&')}`);
const goals = await get(url);
diff --git a/frontend/src/fetchers/citations.js b/frontend/src/fetchers/citations.js
new file mode 100644
index 0000000000..9dbef2f32e
--- /dev/null
+++ b/frontend/src/fetchers/citations.js
@@ -0,0 +1,18 @@
+/* eslint-disable import/prefer-default-export */
+import join from 'url-join';
+import {
+ get,
+} from './index';
+
+export async function fetchCitationsByGrant(region, grantIds, reportStartDate) {
+ const formattedDate = new Date(reportStartDate).toISOString().split('T')[0];
+ const citations = await get(join(
+ '/',
+ 'api',
+ 'citations',
+ 'region',
+ String(region),
+ `?grantIds=${grantIds.join('&')}&reportStartDate=${formattedDate}`,
+ ));
+ return citations.json();
+}
diff --git a/frontend/src/pages/ActivityReport/Pages/components/GoalForm.js b/frontend/src/pages/ActivityReport/Pages/components/GoalForm.js
index 95fb4c782a..241e60016d 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/GoalForm.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/GoalForm.js
@@ -28,6 +28,7 @@ export default function GoalForm({
datePickerKey,
templatePrompts,
isMultiRecipientReport,
+ citationOptions,
}) {
// pull the errors out of the form context
const { errors, watch } = useFormContext();
@@ -219,6 +220,7 @@ export default function GoalForm({
noObjectiveError={errors.goalForEditing && errors.goalForEditing.objectives
? ERROR_FORMAT(errors.goalForEditing.objectives.message) : NO_ERROR}
reportId={parseInt(reportId, DECIMAL_BASE)}
+ citationOptions={citationOptions}
/>
>
);
@@ -253,6 +255,10 @@ GoalForm.propTypes = {
value: PropTypes.number,
label: PropTypes.string,
})).isRequired,
+ citationOptions: PropTypes.arrayOf(PropTypes.shape({
+ value: PropTypes.number,
+ label: PropTypes.string,
+ })),
reportId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
datePickerKey: PropTypes.string.isRequired,
templatePrompts: PropTypes.oneOfType([
@@ -269,4 +275,5 @@ GoalForm.propTypes = {
GoalForm.defaultProps = {
isMultiRecipientReport: false,
+ citationOptions: [],
};
diff --git a/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js b/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js
index 2906aae9e2..624a20e298 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js
@@ -15,6 +15,7 @@ import { validateGoals } from './goalValidator';
import './GoalPicker.css';
import GoalForm from './GoalForm';
import Modal from '../../../../components/VanillaModal';
+import { fetchCitationsByGrant } from '../../../../fetchers/citations';
export const newGoal = (grantIds) => ({
value: uuidv4(),
@@ -57,8 +58,12 @@ const GoalPicker = ({
const [useOhsStandardGoal, setOhsStandardGoal] = useState(false);
const activityRecipientType = watch('activityRecipientType');
+ const [citationOptions, setCitationOptions] = useState([]);
+
const selectedGoals = useWatch({ name: 'goals' });
const activityRecipients = watch('activityRecipients');
+ const regionId = watch('regionId');
+ const startDate = watch('startDate');
const isMultiRecipientReport = activityRecipients && activityRecipients.length > 1;
const modalRef = useRef();
@@ -105,10 +110,48 @@ const GoalPicker = ({
const topics = await getTopics();
setTopicOptions(topics);
}
-
fetchTopics();
}, []);
+ // Fetch citations for the goal if the source is CLASS or RANs.
+ useEffect(() => {
+ async function fetchCitations() {
+ // If its a monitoring goal and the source is CLASS or RANs, fetch the citations.
+ if (goalForEditing && goalForEditing.source === 'Federal monitoring issues, including CLASS and RANs') {
+ const retrievedCitationOptions = await fetchCitationsByGrant(
+ regionId,
+ goalForEditing.grantIds,
+ startDate,
+ );
+ if (retrievedCitationOptions) {
+ // Reduce the citation options to only unique values.
+ const uniqueCitationOptions = Object.values(retrievedCitationOptions.reduce(
+ (acc, current) => {
+ current.grants.forEach((currentGrant) => {
+ const { findingType } = currentGrant;
+ if (!acc[findingType]) {
+ acc[findingType] = { label: findingType, options: [] };
+ }
+
+ const findingKey = `${currentGrant.acro} - ${currentGrant.citation} - ${currentGrant.findingType}`;
+ if (!acc[findingType].options.find((option) => option.label === findingKey)) {
+ acc[findingType].options.push({
+ name: findingKey,
+ id: current.standardId,
+ });
+ }
+ });
+
+ return acc;
+ }, {},
+ ));
+ setCitationOptions(uniqueCitationOptions);
+ }
+ }
+ }
+ fetchCitations();
+ }, [goalForEditing, regionId, startDate]);
+
const uniqueAvailableGoals = uniqBy(allAvailableGoals, 'name');
// We need options with the number and also we need to add the
@@ -259,6 +302,7 @@ const GoalPicker = ({
datePickerKey={datePickerKey}
templatePrompts={templatePrompts}
isMultiRecipientReport={isMultiRecipientReport}
+ citationOptions={citationOptions}
/>
) : null}
diff --git a/frontend/src/pages/ActivityReport/Pages/components/Objective.js b/frontend/src/pages/ActivityReport/Pages/components/Objective.js
index ec1b4caa70..35102da7c3 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/Objective.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/Objective.js
@@ -7,7 +7,7 @@ import {
useController, useFormContext,
} from 'react-hook-form';
import ObjectiveTitle from './ObjectiveTitle';
-import ObjectiveTopics from '../../../../components/GoalForm/ObjectiveTopics';
+import GenericSelectWithDrawer from '../../../../components/GoalForm/GenericSelectWithDrawer';
import ResourceRepeater from '../../../../components/GoalForm/ResourceRepeater';
import ObjectiveFiles from '../../../../components/GoalForm/ObjectiveFiles';
import ObjectiveTta from './ObjectiveTta';
@@ -20,6 +20,7 @@ import {
OBJECTIVE_TITLE,
OBJECTIVE_TTA,
OBJECTIVE_TOPICS,
+ OBJECTIVE_CITATIONS,
} from './goalValidator';
import { validateListOfResources, noDisallowedUrls } from '../../../../components/GoalForm/constants';
import AppLoadingContext from '../../../../AppLoadingContext';
@@ -40,6 +41,7 @@ export default function Objective({
parentGoal,
initialObjectiveStatus,
reportId,
+ citationOptions,
}) {
const modalRef = useRef();
@@ -100,6 +102,23 @@ export default function Objective({
defaultValue: objective.topics,
});
+ const {
+ field: {
+ onChange: onChangeCitations,
+ onBlur: onBlurCitations,
+ value: objectiveCitations,
+ name: objectiveCitationsInputName,
+ },
+ } = useController({
+ name: `${fieldArrayName}[${index}].citations`,
+ rules: {
+ validate: {
+ notEmpty: (value) => (value && value.length) || OBJECTIVE_CITATIONS,
+ },
+ },
+ defaultValue: objective.citations,
+ });
+
const {
field: {
onChange: onChangeResources,
@@ -358,14 +377,31 @@ export default function Objective({
initialObjectiveStatus={statusForCalculations}
/>
- 0 && (
+
+ )
+ }
+
+
@@ -483,6 +519,9 @@ Objective.propTypes = {
topics: PropTypes.shape({
message: PropTypes.string,
}),
+ citations: PropTypes.shape({
+ message: PropTypes.string,
+ }),
closeSuspendReason: PropTypes.shape({
message: PropTypes.string,
}),
@@ -491,6 +530,10 @@ Objective.propTypes = {
value: PropTypes.number,
label: PropTypes.string,
})).isRequired,
+ citationOptions: PropTypes.arrayOf(PropTypes.shape({
+ value: PropTypes.number,
+ label: PropTypes.string,
+ })),
options: PropTypes.arrayOf(
OBJECTIVE_PROP,
).isRequired,
@@ -504,3 +547,7 @@ Objective.propTypes = {
initialObjectiveStatus: PropTypes.string.isRequired,
reportId: PropTypes.number.isRequired,
};
+
+Objective.defaultProps = {
+ citationOptions: [],
+};
diff --git a/frontend/src/pages/ActivityReport/Pages/components/Objectives.js b/frontend/src/pages/ActivityReport/Pages/components/Objectives.js
index a1aff737f9..c2b49d2dda 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/Objectives.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/Objectives.js
@@ -11,6 +11,7 @@ export default function Objectives({
topicOptions,
noObjectiveError,
reportId,
+ citationOptions,
}) {
const { errors, getValues, setValue } = useFormContext();
@@ -137,6 +138,7 @@ export default function Objectives({
parentGoal={getValues('goalForEditing')}
initialObjectiveStatus={objective.status}
reportId={reportId}
+ citationOptions={citationOptions}
/>
);
})}
@@ -150,9 +152,17 @@ Objectives.propTypes = {
value: PropTypes.number,
label: PropTypes.string,
})).isRequired,
+ citationOptions: PropTypes.arrayOf(PropTypes.shape({
+ value: PropTypes.number,
+ label: PropTypes.string,
+ })),
objectiveOptions: PropTypes.arrayOf(
OBJECTIVE_PROP,
).isRequired,
noObjectiveError: PropTypes.node.isRequired,
reportId: PropTypes.number.isRequired,
};
+
+Objectives.defaultProps = {
+ citationOptions: [],
+};
diff --git a/frontend/src/pages/ActivityReport/Pages/components/__tests__/goalValidator.js b/frontend/src/pages/ActivityReport/Pages/components/__tests__/goalValidator.js
index fde5eb245f..77ccfb03a9 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/__tests__/goalValidator.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/__tests__/goalValidator.js
@@ -7,6 +7,7 @@ import {
UNFINISHED_OBJECTIVES,
GOAL_MISSING_OBJECTIVE,
OBJECTIVE_TOPICS,
+ OBJECTIVE_CITATIONS,
OBJECTIVE_TITLE,
OBJECTIVE_TTA,
OBJECTIVE_RESOURCES,
@@ -124,6 +125,18 @@ describe('validateGoals', () => {
expect(setError).toHaveBeenCalledWith(`goalForEditing.objectives[${1}].topics`, { message: OBJECTIVE_TOPICS });
});
+ it('if one objective has no "citations"', () => {
+ const objectives = [
+ { ...validObjective },
+ { ...validObjective, citations: [] },
+ ];
+
+ const setError = jest.fn();
+ const result = unfinishedObjectives(objectives, setError);
+ expect(result).toEqual(UNFINISHED_OBJECTIVES);
+ expect(setError).toHaveBeenCalledWith(`goalForEditing.objectives[${1}].citations`, { message: OBJECTIVE_CITATIONS });
+ });
+
it('if one objective has no "supportType"', () => {
const objectives = [
{ ...validObjective },
diff --git a/frontend/src/pages/ActivityReport/Pages/components/goalValidator.js b/frontend/src/pages/ActivityReport/Pages/components/goalValidator.js
index 88c7dc095b..6fde9d0da4 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/goalValidator.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/goalValidator.js
@@ -12,6 +12,7 @@ export const OBJECTIVE_ROLE = 'Select a specialist role';
export const OBJECTIVE_RESOURCES = 'Each resource should be a valid link. Invalid resources will not be saved.';
export const OBJECTIVE_TTA = 'Describe the TTA provided';
export const OBJECTIVE_TOPICS = 'Select at least one topic';
+export const OBJECTIVE_CITATIONS = 'Select at least one citation';
/**
* Function to validate a single value based on a user's flags
@@ -59,6 +60,11 @@ export const unfinishedObjectives = (
incomplete = true;
}
+ if (objective.citations && (!objective.citations || !objective.citations.length)) {
+ setError(`${fieldArrayName}[${index}].citations`, { message: OBJECTIVE_CITATIONS });
+ incomplete = true;
+ }
+
if (!objective.resources || !validateListOfResources(objective.resources)) {
setError(`${fieldArrayName}[${index}].resources`, { message: OBJECTIVE_RESOURCES });
incomplete = true;
diff --git a/frontend/src/pages/ActivityReport/Pages/goalsObjectives.js b/frontend/src/pages/ActivityReport/Pages/goalsObjectives.js
index 48ddf6e494..6d74f56e9e 100644
--- a/frontend/src/pages/ActivityReport/Pages/goalsObjectives.js
+++ b/frontend/src/pages/ActivityReport/Pages/goalsObjectives.js
@@ -165,7 +165,7 @@ const GoalsObjectives = ({
const fetch = async () => {
try {
if (isRecipientReport && hasGrant) {
- const fetchedGoals = await getGoals(grantIds);
+ const fetchedGoals = await getGoals(grantIds, startDate);
const formattedGoals = fetchedGoals.map((g) => {
// if the goal is on an "old" grant, we should
// treat it like a new goal for now
diff --git a/frontend/src/pages/ActivityReport/__tests__/index.js b/frontend/src/pages/ActivityReport/__tests__/index.js
index a362305a38..31565ca3bd 100644
--- a/frontend/src/pages/ActivityReport/__tests__/index.js
+++ b/frontend/src/pages/ActivityReport/__tests__/index.js
@@ -752,7 +752,7 @@ describe('ActivityReport', () => {
});
fetchMock.get('/api/topic', [{ id: 64, name: 'Communication' }]);
- fetchMock.get('/api/activity-reports/goals?grantIds=10431', [{
+ fetchMock.get('/api/activity-reports/goals?grantIds=10431&reportStartDate=2012-03-20', [{
endDate: null,
grantIds: [10431],
goalIds: [37502],
diff --git a/src/goalServices/goals.js b/src/goalServices/goals.js
index e960f2050c..3f105970ab 100644
--- a/src/goalServices/goals.js
+++ b/src/goalServices/goals.js
@@ -842,15 +842,21 @@ export async function goalsForGrants(grantIds, reportStartDate, user) {
*/
let goalsToReturn = regularGoals;
const hasGoalMonitoringOverride = !!(user && new Users(user).canSeeBehindFeatureFlag('monitoring_integration'));
-
if (hasGoalMonitoringOverride && reportStartDate) {
const monitoringGoals = await getMonitoringGoals(ids, reportStartDate);
-
// Combine goalsToReturn with monitoringGoals.
const allGoals = await Promise.all([regularGoals, monitoringGoals]);
// Flatten the array of arrays.
goalsToReturn = allGoals.flat();
+
+ // Sort goals by created date desc.
+ goalsToReturn.sort((a, b) => {
+ if (a.created < b.created) {
+ return 1;
+ }
+ return -1;
+ });
}
return goalsToReturn;
}
diff --git a/src/models/activityReportObjective.js b/src/models/activityReportObjective.js
index 812c8d8e03..ca1018444b 100644
--- a/src/models/activityReportObjective.js
+++ b/src/models/activityReportObjective.js
@@ -16,7 +16,6 @@ export default (sequelize, DataTypes) => {
ActivityReportObjective.hasMany(models.ActivityReportObjectiveTopic, { foreignKey: 'activityReportObjectiveId', as: 'activityReportObjectiveTopics' });
ActivityReportObjective.hasMany(models.ActivityReportObjectiveResource, { foreignKey: 'activityReportObjectiveId', as: 'activityReportObjectiveResources' });
ActivityReportObjective.hasMany(models.ActivityReportObjectiveCourse, { foreignKey: 'activityReportObjectiveId', as: 'activityReportObjectiveCourses' });
- ActivityReportObjective.hasMany(models.ActivityReportObjectiveCitation, { foreignKey: 'activityReportObjectiveId', as: 'activityReportObjectiveCitations' });
ActivityReportObjective.belongsToMany(models.File, {
through: models.ActivityReportObjectiveFile,
diff --git a/src/routes/apiDirectory.js b/src/routes/apiDirectory.js
index c4483241de..e30c9138a6 100644
--- a/src/routes/apiDirectory.js
+++ b/src/routes/apiDirectory.js
@@ -34,6 +34,7 @@ import coursesRouter from './courses';
import { currentUserId } from '../services/currentUser';
import objectiveRouter from './objectives';
import ssdiRouter from './ssdi';
+import citationsRouter from './citations';
export const loginPath = '/login';
@@ -81,6 +82,7 @@ router.use('/national-center', nationalCenterRouter);
router.use('/communication-logs', communicationLogRouter);
router.use('/monitoring', monitoringRouter);
router.use('/courses', coursesRouter);
+router.use('/citations', citationsRouter);
router.use('/ssdi', ssdiRouter);
const getUser = async (req, res) => {
diff --git a/src/routes/citations/handlers.js b/src/routes/citations/handlers.js
index b9ccccc21d..57238447c3 100644
--- a/src/routes/citations/handlers.js
+++ b/src/routes/citations/handlers.js
@@ -16,9 +16,8 @@ const logContext = { namespace };
export const getCitationsByGrants = async (req, res) => {
try {
// Get the grant we need citations for.
- const { regionId, reportStartDate } = req.params;
-
- const { grantIds } = req.query;
+ const { regionId } = req.params;
+ const { grantIds, reportStartDate } = req.query;
const userId = await currentUserId(req, res);
const user = await userById(userId);
@@ -33,7 +32,7 @@ export const getCitationsByGrants = async (req, res) => {
const formattedStartDate = new Date(reportStartDate).toISOString().split('T')[0];
// Get the citations for the grant.
- const citations = await getCitationsByGrantIds(grantIds, formattedStartDate);
+ const citations = await getCitationsByGrantIds([grantIds].flat(), formattedStartDate);
// Return the citations.
res.status(httpCodes.OK).send(citations);
diff --git a/src/routes/citations/handlers.test.js b/src/routes/citations/handlers.test.js
index d23dbcfcf1..9ecbf718b6 100644
--- a/src/routes/citations/handlers.test.js
+++ b/src/routes/citations/handlers.test.js
@@ -27,10 +27,10 @@ describe('Citation handlers', () => {
const req = {
query: {
grantIds: [1],
+ reportStartDate: '2024-10-01',
},
params: {
regionId: 1,
- reportStartDate: '2024-10-01',
},
};
@@ -75,10 +75,10 @@ describe('Citation handlers', () => {
const req = {
query: {
grantIds: [1],
+ reportStartDate: '2024-10-01',
},
params: {
regionId: 1,
- reportStartDate: '2024-10-01',
},
};
@@ -120,10 +120,10 @@ describe('Citation handlers', () => {
const req = {
query: {
grantIds: [1],
+ reportStartDate: '2024-10-01',
},
params: {
regionId: 1,
- reportStartDate: '2024-10-01',
},
};
diff --git a/src/routes/citations/index.js b/src/routes/citations/index.js
index 97eb727ef8..64ca6dec72 100644
--- a/src/routes/citations/index.js
+++ b/src/routes/citations/index.js
@@ -3,10 +3,16 @@ import transactionWrapper from '../transactionWrapper';
import {
getCitationsByGrants,
} from './handlers';
+import {
+ checkRegionIdParam,
+} from '../../middleware/checkIdParamMiddleware';
const router = express.Router();
-// Citations by Region ID and Grant Ids.
-router.put('/:regionId', transactionWrapper(getCitationsByGrants));
+router.get(
+ '/region/:regionId',
+ checkRegionIdParam,
+ transactionWrapper(getCitationsByGrants),
+);
export default router;
diff --git a/src/services/citations.js b/src/services/citations.js
index 7ea148201d..a83a1a5afa 100644
--- a/src/services/citations.js
+++ b/src/services/citations.js
@@ -2,7 +2,7 @@
/* eslint-disable import/prefer-default-export */
import { sequelize } from '../models';
-const cutOffStartDate = '2024-10-01'; // TODO: Set this before we deploy to prod.
+const cutOffStartDate = '2024-01-01'; // TODO: Set this before we deploy to prod.
/*
The purpose of this function is to get citations by grant id.
We then need to format the response for how it needs to be
@@ -30,7 +30,19 @@ export async function getCitationsByGrantIds(grantIds, reportStartDate) {
'reportDeliveryDate', mfh."reportDeliveryDate",
'findingType', mf."findingType",
'findingSource', mf."source",
- 'monitoringFindingStatusName', mfs."name"
+ 'monitoringFindingStatusName', mfs."name",
+ 'reportDeliveryDate', mfh."reportDeliveryDate",
+ 'citation', ms.citation,
+ 'severity', CASE
+ WHEN mf."findingType" = 'Deficiency' THEN 1
+ WHEN mf."findingType" = 'Noncompliance' THEN 2
+ ELSE 3
+ END,
+ 'acro', CASE
+ WHEN mf."findingType" = 'Deficiency' THEN 'DEF'
+ WHEN mf."findingType" = 'Noncompliance' THEN 'ANC'
+ ELSE 'AOC'
+ END
)
) grants
FROM "Grants" gr
From d9ba411901965a6d3f3330586cba74e502e8ba54 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Tue, 3 Dec 2024 16:17:28 -0500
Subject: [PATCH 045/198] fixes per Matt
---
src/goalServices/goals.js | 2 +-
src/routes/citations/handlers.test.js | 1 -
2 files changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/goalServices/goals.js b/src/goalServices/goals.js
index e960f2050c..e36eabe2aa 100644
--- a/src/goalServices/goals.js
+++ b/src/goalServices/goals.js
@@ -1315,7 +1315,7 @@ export async function saveGoalsForReport(goals, report) {
raw: true,
where: {
grantId: goal.grantIds,
- standard: 'Monitoring',
+ createdVia: 'monitoring',
status: { [Op.not]: GOAL_STATUS.CLOSED },
},
});
diff --git a/src/routes/citations/handlers.test.js b/src/routes/citations/handlers.test.js
index d23dbcfcf1..0a2f5fa496 100644
--- a/src/routes/citations/handlers.test.js
+++ b/src/routes/citations/handlers.test.js
@@ -5,7 +5,6 @@ import { getCitationsByGrantIds } from '../../services/citations';
import User from '../../policies/user';
import handleErrors from '../../lib/apiErrorHandler';
-// Mock these files.
jest.mock('../../models');
jest.mock('../../services/currentUser');
jest.mock('../../services/users');
From 86c3b87e104ae69652d32f126ff5e755e6e11002 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Tue, 3 Dec 2024 16:56:15 -0500
Subject: [PATCH 046/198] remove date conversion
---
src/routes/citations/handlers.js | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/src/routes/citations/handlers.js b/src/routes/citations/handlers.js
index b9ccccc21d..861909a070 100644
--- a/src/routes/citations/handlers.js
+++ b/src/routes/citations/handlers.js
@@ -29,11 +29,8 @@ export const getCitationsByGrants = async (req, res) => {
return;
}
- // Convert reportStartDate to the format 'YYYY-MM-DD'.
- const formattedStartDate = new Date(reportStartDate).toISOString().split('T')[0];
-
// Get the citations for the grant.
- const citations = await getCitationsByGrantIds(grantIds, formattedStartDate);
+ const citations = await getCitationsByGrantIds(grantIds, reportStartDate);
// Return the citations.
res.status(httpCodes.OK).send(citations);
From 45b629fb60eeeb77e0bf5c7204a8458d398feb0a Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Wed, 4 Dec 2024 10:34:02 -0500
Subject: [PATCH 047/198] More work on byReview query, need to track down a few
additional fields
---
.../Monitoring/components/DescriptionList.css | 3 -
.../components/FindingWithinReview.js | 4 +-
.../pages/Monitoring/components/ReviewCard.js | 2 +-
.../Monitoring/components/ReviewCards.js | 2 +-
src/services/monitoring.ts | 411 +++++++++++-------
src/services/types/monitoring.ts | 57 ++-
6 files changed, 313 insertions(+), 166 deletions(-)
diff --git a/frontend/src/pages/RecipientRecord/pages/Monitoring/components/DescriptionList.css b/frontend/src/pages/RecipientRecord/pages/Monitoring/components/DescriptionList.css
index a1b4d1cf6d..a353944cec 100644
--- a/frontend/src/pages/RecipientRecord/pages/Monitoring/components/DescriptionList.css
+++ b/frontend/src/pages/RecipientRecord/pages/Monitoring/components/DescriptionList.css
@@ -1,9 +1,6 @@
.ttahub-data-card-description-list {
flex-wrap: wrap;
}
-.ttahub-data-card-description-list > div {
- flex: 1;
-}
@media (min-width: 1560px) {
.ttahub-data-card-description-list {
diff --git a/frontend/src/pages/RecipientRecord/pages/Monitoring/components/FindingWithinReview.js b/frontend/src/pages/RecipientRecord/pages/Monitoring/components/FindingWithinReview.js
index fdffb79eaa..0074b71686 100644
--- a/frontend/src/pages/RecipientRecord/pages/Monitoring/components/FindingWithinReview.js
+++ b/frontend/src/pages/RecipientRecord/pages/Monitoring/components/FindingWithinReview.js
@@ -20,7 +20,7 @@ export default function FindingWithinReview({ finding, regionId }) {
{finding.status}
- {finding.type}
+ {finding.findingType}
{finding.category}
@@ -46,7 +46,7 @@ FindingWithinReview.propTypes = {
finding: PropTypes.shape({
citation: PropTypes.string.isRequired,
status: PropTypes.string.isRequired,
- type: PropTypes.string.isRequired,
+ findingType: PropTypes.string.isRequired,
category: PropTypes.string.isRequired,
correctionDeadline: PropTypes.string.isRequired,
objectives: PropTypes.arrayOf(PropTypes.shape({
diff --git a/frontend/src/pages/RecipientRecord/pages/Monitoring/components/ReviewCard.js b/frontend/src/pages/RecipientRecord/pages/Monitoring/components/ReviewCard.js
index 6d609d50b4..c8f09bdb59 100644
--- a/frontend/src/pages/RecipientRecord/pages/Monitoring/components/ReviewCard.js
+++ b/frontend/src/pages/RecipientRecord/pages/Monitoring/components/ReviewCard.js
@@ -86,7 +86,7 @@ ReviewCard.propTypes = {
findings: PropTypes.arrayOf(PropTypes.shape({
citation: PropTypes.string.isRequired,
status: PropTypes.string.isRequired,
- type: PropTypes.string.isRequired,
+ findingType: PropTypes.string.isRequired,
category: PropTypes.string.isRequired,
correctionDeadline: PropTypes.string.isRequired,
objectives: PropTypes.arrayOf(PropTypes.shape({
diff --git a/frontend/src/pages/RecipientRecord/pages/Monitoring/components/ReviewCards.js b/frontend/src/pages/RecipientRecord/pages/Monitoring/components/ReviewCards.js
index 49769a55cb..43edb8899d 100644
--- a/frontend/src/pages/RecipientRecord/pages/Monitoring/components/ReviewCards.js
+++ b/frontend/src/pages/RecipientRecord/pages/Monitoring/components/ReviewCards.js
@@ -25,7 +25,7 @@ ReviewCards.propTypes = {
findings: PropTypes.arrayOf(PropTypes.shape({
citation: PropTypes.string.isRequired,
status: PropTypes.string.isRequired,
- type: PropTypes.string.isRequired,
+ findingType: PropTypes.string.isRequired,
category: PropTypes.string.isRequired,
correctionDeadline: PropTypes.string.isRequired,
objectives: PropTypes.arrayOf(PropTypes.shape({
diff --git a/src/services/monitoring.ts b/src/services/monitoring.ts
index ff96012832..20fcb14ec2 100644
--- a/src/services/monitoring.ts
+++ b/src/services/monitoring.ts
@@ -1,11 +1,13 @@
+/* eslint-disable max-len */
import moment from 'moment';
import db from '../models';
import {
ITTAByReviewResponse,
- ITTAByCitationResponse,
- IMonitoringResponse,
- IMonitoringReviewGrantee,
IMonitoringReview,
+ IMonitoringReviewGrantee,
+ IMonitoringResponse,
+ ITTAByReviewsSequelizeQueryResponse,
+ ITTAByCitationResponse,
} from './types/monitoring';
const {
@@ -17,158 +19,262 @@ const {
MonitoringReviewLink,
MonitoringReviewStatusLink,
MonitoringClassSummary,
+ MonitoringFindingLink,
+ MonitoringFindingHistory,
+ MonitoringFinding,
+ MonitoringFindingStatusLink,
+ MonitoringFindingStatus,
} = db;
+export async function grantNumbersByRecipientAndRegion(recipientId: number, regionId: number) {
+ const grants = await Grant.findAll({
+ attributes: ['number'],
+ where: {
+ recipientId,
+ regionId,
+ },
+ }) as { number: string }[];
+
+ return grants.map((grant) => grant.number);
+}
+
export async function ttaByReviews(
- _recipientId: number,
- _regionId: number,
+ recipientId: number,
+ regionId: number,
): Promise {
- return [
- {
- name: '241234FU',
- reviewType: 'Follow-up',
- reviewReceived: '01/01/2021',
- outcome: 'Compliant',
+ const grantNumbers = await grantNumbersByRecipientAndRegion(recipientId, regionId) as string[];
+ const reviews = await MonitoringReview.findAll({
+ include: [
+ {
+ model: MonitoringReviewLink,
+ as: 'monitoringReviewLink',
+ required: true,
+ include: [
+ {
+ model: MonitoringReviewGrantee,
+ as: 'monitoringReviewGrantees',
+ required: true,
+ where: {
+ grantNumber: grantNumbers,
+ },
+ },
+ {
+ model: MonitoringFindingHistory,
+ as: 'monitoringFindingHistories',
+ include: [
+ {
+ model: MonitoringFindingLink,
+ as: 'monitoringFindingLink',
+ include: [
+ {
+ model: MonitoringFinding,
+ as: 'monitoringFindings',
+ include: [
+ {
+ model: MonitoringFindingStatusLink,
+ as: 'statusLink',
+ include: [
+ {
+ model: MonitoringFindingStatus,
+ as: 'monitoringFindingStatuses',
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ }) as ITTAByReviewsSequelizeQueryResponse[];
+
+ const response = reviews.map((review) => {
+ const { monitoringReviewGrantees, monitoringFindingHistories } = review.monitoringReviewLink;
+
+ const findings = [];
+
+ monitoringFindingHistories.forEach((history) => {
+ if (!history.monitoringFindingLink) {
+ return;
+ }
+
+ history.monitoringFindingLink.monitoringFindings.forEach((finding) => {
+ const status = finding.statusLink.monitoringFindingStatuses[0].name;
+ findings.push({
+ citation: '',
+ status,
+ findingType: finding.findingType,
+ correctionDeadline: moment(finding.correctionDeadLine).format('MM/DD/YYYY'),
+ category: finding.source,
+ objectives: [],
+ });
+ });
+ });
+
+ return {
+ name: review.name,
+ id: review.id,
lastTTADate: null,
+ outcome: review.outcome,
+ reviewType: review.reviewType,
+ reviewReceived: moment(review.reportDeliveryDate).format('MM/DD/YYYY'),
+ grants: monitoringReviewGrantees.map((grantee) => grantee.grantNumber),
specialists: [],
- grants: [
- '14CH123456',
- '14HP141234',
- ],
- findings: [
- {
- citation: '1392.47(b)(5)(i)',
- status: 'Corrected',
- type: 'Noncompliance',
- category: 'Monitoring and Implementing Quality Health Services',
- correctionDeadline: '09/18/2024',
- objectives: [],
- },
- {
- citation: '1302.91(a)',
- status: 'Corrected',
- type: 'Noncompliance',
- category: 'Program Management and Quality Improvement',
- correctionDeadline: '09/18/2024',
- objectives: [],
- },
- {
- citation: '1302.12(m)',
- status: 'Corrected',
- type: 'Noncompliance',
- category: 'Program Management and Quality Improvement',
- correctionDeadline: '09/18/2024',
- objectives: [],
- },
- ],
- },
- {
- name: '241234RAN',
- reviewType: 'RAN',
- reviewReceived: '06/21/2024',
- outcome: 'Deficiency',
- lastTTADate: '07/12/2024',
- grants: [
- '14CH123456',
- '14HP141234',
- ],
- specialists: [
- {
- name: 'Specialist 1',
- roles: ['GS'],
- },
- ],
- findings: [
- {
- citation: '1302.47(b)(5)(iv)',
- status: 'Active',
- type: 'Deficiency',
- category: 'Inappropriate Release',
- correctionDeadline: '07/25/2024',
- objectives: [
- {
- title: 'The TTA Specialist will assist the recipient with developing a QIP and/or corrective action plan to address the finding with correction strategies, timeframes, and evidence of the correction.',
- activityReportIds: ['14AR29888'],
- endDate: '07/12/2024',
- topics: ['Communication', 'Quality Improvement Plan/QIP', 'Safety Practices', 'Transportation'],
- status: 'In Progress',
- },
- ],
- },
- ],
- },
- {
- name: '241234F2',
- reviewType: 'FA-2',
- reviewReceived: '05/20/2024',
- outcome: 'Noncompliant',
- lastTTADate: '03/27/2024',
- grants: [
- '14CH123456',
- '14HP141234',
- ],
- specialists: [
- {
- name: 'Specialist 1',
- roles: ['GS'],
- },
- {
- name: 'Specialist 1',
- roles: ['ECS'],
- },
- ],
- findings: [
- {
- citation: '1302.47(b)(5)(v)',
- status: 'Active',
- type: 'Noncompliance',
- category: 'Monitoring and Implementing Quality Health Services',
- correctionDeadline: '09/18/2024',
- objectives: [
- {
- title: 'The TTA Specialist will support the recipient by developing a plan to ensure all staff implement health and safety practices to always keep children safe',
- activityReportIds: ['14AR12345'],
- endDate: '06/24/2024',
- topics: ['Human Resources', 'Ongoing Monitoring Management System', 'Safety Practices', 'Training and Profressional Development'],
- status: 'Complete',
- },
- ],
- },
- {
- citation: '1302.91(a)',
- status: 'Active',
- type: 'Noncompliance',
- category: 'Program Management and Quality Improvement',
- correctionDeadline: '09/18/2024',
- objectives: [
- {
- title: 'The TTA Specialist will support the recipient by developing a plan to ensure all staff implement health and safety practices to always keep children safe',
- activityReportIds: ['14AR12345'],
- endDate: '06/24/2024',
- topics: ['Human Resources', 'Ongoing Monitoring Management System', 'Safety Practices', 'Training and Profressional Development'],
- status: 'Complete',
- },
- ],
- },
- {
- citation: '1302.12(m)',
- status: 'Active',
- type: 'Noncompliance',
- category: 'Program Management and Quality Improvement',
- correctionDeadline: '09/18/2024',
- objectives: [
- {
- title: 'The TTA Specialist will support the recipient by developing a plan to ensure all staff implement health and safety practices to always keep children safe',
- activityReportIds: ['14AR12345'],
- endDate: '06/24/2024',
- topics: ['Human Resources', 'Ongoing Monitoring Management System', 'Safety Practices', 'Training and Profressional Development'],
- status: 'Complete',
- },
- ],
- },
- ],
- },
- ];
+ findings,
+ };
+ });
+
+ return response;
+
+ // return [
+ // {
+ // name: '241234FU',
+ // reviewType: 'Follow-up',
+ // reviewReceived: '01/01/2021',
+ // outcome: 'Compliant',
+ // lastTTADate: null,
+ // specialists: [],
+ // grants: [
+ // '14CH123456',
+ // '14HP141234',
+ // ],
+ // findings: [
+ // {
+ // citation: '1392.47(b)(5)(i)',
+ // status: 'Corrected',
+ // type: 'Noncompliance',
+ // category: 'Monitoring and Implementing Quality Health Services',
+ // correctionDeadline: '09/18/2024',
+ // objectives: [],
+ // },
+ // {
+ // citation: '1302.91(a)',
+ // status: 'Corrected',
+ // type: 'Noncompliance',
+ // category: 'Program Management and Quality Improvement',
+ // correctionDeadline: '09/18/2024',
+ // objectives: [],
+ // },
+ // {
+ // citation: '1302.12(m)',
+ // status: 'Corrected',
+ // type: 'Noncompliance',
+ // category: 'Program Management and Quality Improvement',
+ // correctionDeadline: '09/18/2024',
+ // objectives: [],
+ // },
+ // ],
+ // },
+ // {
+ // name: '241234RAN',
+ // reviewType: 'RAN',
+ // reviewReceived: '06/21/2024',
+ // outcome: 'Deficiency',
+ // lastTTADate: '07/12/2024',
+ // grants: [
+ // '14CH123456',
+ // '14HP141234',
+ // ],
+ // specialists: [
+ // {
+ // name: 'Specialist 1',
+ // roles: ['GS'],
+ // },
+ // ],
+ // findings: [
+ // {
+ // citation: '1302.47(b)(5)(iv)',
+ // status: 'Active',
+ // type: 'Deficiency',
+ // category: 'Inappropriate Release',
+ // correctionDeadline: '07/25/2024',
+ // objectives: [
+ // {
+ // title: 'The TTA Specialist will assist the recipient with developing a QIP and/or corrective action plan to address the finding with correction strategies, timeframes, and evidence of the correction.',
+ // activityReportIds: ['14AR29888'],
+ // endDate: '07/12/2024',
+ // topics: ['Communication', 'Quality Improvement Plan/QIP', 'Safety Practices', 'Transportation'],
+ // status: 'In Progress',
+ // },
+ // ],
+ // },
+ // ],
+ // },
+ // {
+ // name: '241234F2',
+ // reviewType: 'FA-2',
+ // reviewReceived: '05/20/2024',
+ // outcome: 'Noncompliant',
+ // lastTTADate: '03/27/2024',
+ // grants: [
+ // '14CH123456',
+ // '14HP141234',
+ // ],
+ // specialists: [
+ // {
+ // name: 'Specialist 1',
+ // roles: ['GS'],
+ // },
+ // {
+ // name: 'Specialist 1',
+ // roles: ['ECS'],
+ // },
+ // ],
+ // findings: [
+ // {
+ // citation: '1302.47(b)(5)(v)',
+ // status: 'Active',
+ // type: 'Noncompliance',
+ // category: 'Monitoring and Implementing Quality Health Services',
+ // correctionDeadline: '09/18/2024',
+ // objectives: [
+ // {
+ // title: 'The TTA Specialist will support the recipient by developing a plan to ensure all staff implement health and safety practices to always keep children safe',
+ // activityReportIds: ['14AR12345'],
+ // endDate: '06/24/2024',
+ // topics: ['Human Resources', 'Ongoing Monitoring Management System', 'Safety Practices', 'Training and Profressional Development'],
+ // status: 'Complete',
+ // },
+ // ],
+ // },
+ // {
+ // citation: '1302.91(a)',
+ // status: 'Active',
+ // type: 'Noncompliance',
+ // category: 'Program Management and Quality Improvement',
+ // correctionDeadline: '09/18/2024',
+ // objectives: [
+ // {
+ // title: 'The TTA Specialist will support the recipient by developing a plan to ensure all staff implement health and safety practices to always keep children safe',
+ // activityReportIds: ['14AR12345'],
+ // endDate: '06/24/2024',
+ // topics: ['Human Resources', 'Ongoing Monitoring Management System', 'Safety Practices', 'Training and Profressional Development'],
+ // status: 'Complete',
+ // },
+ // ],
+ // },
+ // {
+ // citation: '1302.12(m)',
+ // status: 'Active',
+ // type: 'Noncompliance',
+ // category: 'Program Management and Quality Improvement',
+ // correctionDeadline: '09/18/2024',
+ // objectives: [
+ // {
+ // title: 'The TTA Specialist will support the recipient by developing a plan to ensure all staff implement health and safety practices to always keep children safe',
+ // activityReportIds: ['14AR12345'],
+ // endDate: '06/24/2024',
+ // topics: ['Human Resources', 'Ongoing Monitoring Management System', 'Safety Practices', 'Training and Profressional Development'],
+ // status: 'Complete',
+ // },
+ // ],
+ // },
+ // ],
+ // },
+ // ];
}
export async function ttaByCitations(
@@ -459,6 +565,13 @@ export async function monitoringData({
return b;
}, monitoringReviews[0]);
+ // from the most recent review, get the status via the statusLink
+ const { monitoringReviewStatuses } = monitoringReview.statusLink;
+
+ // I am presuming there can only be one status linked to a review
+ // as that was the structure before tables were refactored
+ const [status] = monitoringReviewStatuses;
+
return {
recipientId: grant.recipientId,
regionId: grant.regionId,
diff --git a/src/services/types/monitoring.ts b/src/services/types/monitoring.ts
index 88ed03ce8e..0148b134d2 100644
--- a/src/services/types/monitoring.ts
+++ b/src/services/types/monitoring.ts
@@ -49,22 +49,23 @@ interface ITTAByCitationReview {
}
interface ITTAByReviewFinding {
- citation: string;
- status: string;
- type: string;
- category: string;
- correctionDeadline: string;
+ citation: string; // ??
+ status: string; // check
+ findingType: string; // check
+ category: string; // check
+ correctionDeadline: string; // check
objectives: ITTAByReviewObjective[];
}
interface ITTAByReviewResponse {
- name: string;
- reviewType: string;
+ id: number; // check
+ name: string; // check
+ reviewType: string; // check
reviewReceived: string;
findings: ITTAByReviewFinding[];
- grants: string[];
- outcome: string;
- lastTTADate: string | null;
+ grants: string[]; // check
+ outcome: string; // check
+ lastTTADate: string | null; // need ars
specialists: {
name: string;
roles: string[];
@@ -81,6 +82,41 @@ interface ITTAByCitationResponse {
reviews: ITTAByCitationReview[];
}
+interface ITTAByReviewsSequelizeQueryResponse {
+ id: number;
+ reviewId: string;
+ reportDeliveryDate: string;
+ contentId: string;
+ name: string;
+ outcome: string;
+ reviewType: string;
+ monitoringReviewLink: {
+ monitoringFindingHistories: {
+ narrative: string;
+ ordinal: number;
+ determination: string;
+ monitoringFindingLink?: {
+ monitoringFindings: {
+ citationNumber: string;
+ findingType: string;
+ source: string;
+ correctionDeadLine: string;
+ statusLink: {
+ monitoringFindingStatuses: {
+ name: string;
+ }[];
+ };
+ }[];
+ };
+ }[];
+ monitoringReviewGrantees: {
+ grantNumber: string;
+ }[];
+ };
+
+ toJSON: () => Omit;
+}
+
export {
IMonitoringReview,
IMonitoringReviewGrantee,
@@ -89,5 +125,6 @@ export {
ITTAByCitationReview,
ITTAByReviewFinding,
ITTAByReviewResponse,
+ ITTAByReviewsSequelizeQueryResponse,
ITTAByCitationResponse,
};
From fa5a39e91bd399021113782dc34a868e4b75b23f Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Wed, 4 Dec 2024 15:46:03 -0500
Subject: [PATCH 048/198] fixes and upates to get citations on reports
---
docs/logical_data_model.encoded | 2 +-
docs/logical_data_model.puml | 2 +-
.../Pages/components/GoalForm.js | 15 ++++
.../Pages/components/GoalPicker.js | 3 +
.../Pages/components/Objective.js | 36 ++++++++-
.../Pages/components/Objectives.js | 15 ++++
.../pages/ActivityReport/__tests__/index.js | 2 +-
.../pages/ActivityReport/formDataHelpers.js | 4 +
src/goalServices/goals.js | 17 +++--
src/goalServices/reduceGoals.ts | 13 +++-
src/goalServices/types.ts | 6 +-
src/services/reportCache.js | 75 ++++++-------------
src/services/reportCache.test.js | 20 +----
13 files changed, 125 insertions(+), 85 deletions(-)
diff --git a/docs/logical_data_model.encoded b/docs/logical_data_model.encoded
index c26b9430d4..799d0f05b4 100644
--- a/docs/logical_data_model.encoded
+++ b/docs/logical_data_model.encoded
@@ -1 +1 @@
-xLrVS-Kcbd_Nfr1v7f9qtKnDairMhMjsQznkTwThkzqksvsfIIhbmXA-bxG4Tm2vusJptRS0_G64991qtTq9J_P5m3dyEC3X73ZyWt50ULKMIPf-be9xWl9qJybj0KkMlaTybmAyBM2UPfIyP-aTX3n9K_OEuJsWe4mBX25_sCEKKBuZngUi08obloLfcgPF2SqXFIK5eUbVlldcFq_-wszQ-lKEPUymP2pzEadJtoKhfw8qP4bQrtEISNIF-CCbpD0UGSpPRqdw94LvUfmYpE4MqlGu_ViMOaW1X_d_9EcM0inVcXakpvwVNPwTdvvTfKSln7Tsz5UK9odXEqZFC4VyOQJ4cpONWdN19obeETiJEaRlnCZObiye50oVyFGuvQY4Z8Dohx0NFzDV2ORfSOh9-y-_4FchVUwV__SaiV4Jn4_sUqhk8Pq6SNdxAWRfIAct3DB7mIBDOG6dSYSf9boKuV0NdgIC0rwnz3Y5k2g7q3mbHG5k2GMS_BaG7S3ZEIhWO-1mXmeuKago4OA3R_k7sls2WE8vWaL-2TcUO3O31565Dn0YSzl0BG75DIpt1U1GCd51IRaVdV_S9Gu3MKfhJEgXzUDFOoXUGaOgcZqgWBIcELAwPUim4AdAPLSqKuw2IVmdVzwCG7LZ5FyjIJ4e5UuoFP6KxgX8hAlqh-tl__VbbsFTDDoJqpr8ADeY38g9dZFwQkdCiGsWjMuVPmLXyAfYUuZpKuB5K6WN6_tR9GICu6RXII7Edm6vBWaYIOgOR1ai7ivonA5eoYqX1GIYo1qecCnpoJdOWxOI8H3ToYPxhltMBdJtW6OxG3__-jjllz1afYatC6ZVtFuC9KEdY0EE20uVkMqLUiV83gYhR5aFNzLAWX4d5E7j9Ro35E8CYhR-p0Y-NMlLwo0W5Pqpzpi169NdjaGdh9BsI0KrSnzIFMYvdgSdh07JnwOvj1pEN_ZMWEKJN0BxoFNMGNq5v8Numy459VSeX_d0KZXLgsCzx1UPwEfhiaVPX-bdBaYFb5UiVP01uAyl-03HR_fIpO7CedhOVIsWRuGPnU3dgzcgHjl2KyNqTv32JhqOr1HOjHT0NGjZK5EL76yjJMtOJMWJysV1y0xz1EUyuQdkxFJL_vwsdzyoc2Sfo7Car2TGd1CkB6gPaWLXwS2k0krywPuR_QV4YPbmkdSBmFXRXU7J1p-BHuXjXC8j8dYquqM6Hnh5Jr9Xa5DwRiVEruacuRNfinvZvlYpkhaTTK2P-YRt9Q38Ac49oAyCarSZBTtC8K0Wz98KtJZFOO7k8Nru3VcEv3tMyees2dmECrI2ulDlDbzzuHBNpZhGt_AKGcXabbflBm3ULc1hOo4A5t0BCYKo9YzEcTUeuggyXVJDtGMW76LeBqp4-dPa3xPG80HhDaIAaTFcP4_9be9Iz9dhCsqct_w7VrUGSIa4ramhJFO6DAowBH4JicDrdlp0EJIHv81k8RyWUz7XGi0TvAMAuFp0WKF-n9TxH7a5YcUrCBYaj9tlTPEf4ANmcLwlmNlMDoorxJH0HZS3aht3wa8wMr9G9j4emPvTav5TdbLXm_yzA53-V1Inx-iiSsUzsifWokmJCGxsO0iuV8DVGxg5XiM2qmeUo9wlWLoM5KQP5FbNPEkx-yoiSgERQpFhHxJVxB7cqNBQB3YZIsJZZpkKTqBaom47eVOQlUr-1fd9GRN8T69OtvwaUx7OCyxUuBVxWW2d1U-m0-Bi7cAkviKBL11-3fJ-S0enXrJsauUGGAkMaQwt2RPGOeFoI0qcYu_Y43DvEtTFRg6Qdc3OLTLWt_XDLFgAg6ajOd7sRlZfsylBi_Fhc-kNhy-khazUNxZ67tcFJSJTwdG0D0woVOLlJpAxOIrYtjEvvvGZfkmhz2iqM_CEFZYwlp5921u69CHq90TFOS8Z4UfcpYmbO7JX6j0NGXmbdvgZlq3utLEGlMlboq0xs94R0jM7SzOIVD9i22zrguxyOgKiTko6kd9383oatSODahukcs25hjXUDf-bpjqfTwxJWIiIhr292a0HVtX1IRNty-A6oxz5PgAebVj0fZZuIrPKESnVtfqJVfAN23iw-VSHiAQ1kWlrTS_JmjV5HKtktm7s4iiT91XmEVSiM1POSX4ytG6yzPWFGjQJjHtwyzSUsrw6uVihjyMS5Fd0bzOefAeOqXRb1scUw5jZ3DnT2uVdY6wAUYvDuLCjpSM0CBMilMHHsooPZWJlinQWCiuDt8dc6mwA7S8T6R9lZ66DCR2MzJfLclkSk7O-lGQ60wDBR88-tgZPVHIkgZQaGXV_kNXy4auS9p1J2xk-cayKRbVrNZvNrqntoI7S8sq-H73hBMYsH3l0TlgWhpazDmqdUl9hagCxj5edIdzJxG-Ow6cNLHxdWFPTctxwqtpfLrQh2q0XvY5wHSZn-8z9TQuv-Hu1tV_aiT_hdw5xJabvLGBAsGv2Vit1HG6mlWSQFq97kTlnllHedKJ-N3gYNY7yRjsR3lvTkUtO25vwmooHahzBk2z01ilQmRdONEkiT2NZxI7DiZRVdjrljNHA6eQ0eIezVn4aKgeKmjiNgqlYV4sSTatKnxq9T1_Ty8NlS3iqGmIRaRZC-0mmha3nlC8vmjlN1FDThyeiHwu9kXPZrwTkQXsU4gIKGhTzL7wxsiCCWG8nVbO2L4nbSbQ4iFGII_rYaYCzitK8glRQJx7Ko-PaIdVOZ-WLubJ1AScTvHqBY2R7fDf_4EFmbDt6iDJyWSp3qYpcdZqS-vgWAJ4CqmFjuFmiMVxaPwoUGlUwg6uBb1bd3tU0mloVI4vQvfMybz9Rz0nmuEi9Klwbv-Ek-PTjfc3G3PTFUfn-zlBPsVdro-SlpvvzTdny9BTr5jStgioHFqhoP06-8vsJpDX5KPWHcf-IIXrEghEmR0TBq1TihR4UTap_8mqAwKcKmuy5aFTO-CvFkDWKwWUrqkFqRxKbAOEC3MVmxwIv7SJtpqVxo49_pN6Nmpk4aMw_ajvRYC5j0GTE7Dpn_HNa7E6jKSOQmaA2ycg6JKw8pMl0thK5zu1tuhedZ5-eBaDuAyvkwQotIBq6g83KFCLoAromt7rgJ5lOxMDaSztaxT83XM8Wjo7sURWdwS_ard44yOaubYTiSejrEGfPLV3s92AhCe6SDc7gY64ECS0Pz86DKxJTGjeUksrpwOFMghCTl833BQ4F6kGpZlb8IuzkglLVDguK37TLuKIYlGecGmnSLMK9A8BUAxcn8NOjvTh6_UYlHrgP8xLNk4Vm_SlSNS5hxPwQoDVKqfGw2bf_f1dYD2ZtMrztaETbHy0O9QHyGxS0er_jKy3VRlx3Mns39L47SQxKHODUDSQwXp3ZjCh6Ir0v0pJdLQSLT8dGiw3DVDUXpv7SSNchZNSg_xIar1KtejAMEsf55iBfMbweZ8ROYd97tI8vidxtk56EhQQsflnHs04SMLFjhsbM6Sj6_378E65mUlUR4KgXbCTd4EIl48RBxMUr07jGRosI-jtVXPBy7MBYjvUgk5oKmqLGyVSm8ecFE1VV6Jor93yE4VOJjXnoI4k4pRlOndZ4W8gR7lUTf3dSkoiAdzwvAwhDVLaxbE5xl5rUIiaX1iRCVVsd8ichIE3QGcTFaeycSbUru_Bnb9B6LUebj0rxcg3MXJynPVWHPVCFDoqUT3wC4-6YlddKUIDkG-8snB5ZNN6MitVCXv_ZrFQYRewt7MhnfluJisQfVDUeX1zyfN_w42mRJRv9_XzZujPwOyM1Oyd2OusRFuwkexTdRLMdXrNKT89xnk_AzE89STLU5PSHwjC78R7pFgMkjOzrMXOfJLn9nJEYdzzKiP0v1zckD64AT0se3ZHqn029wqVqSCYWiGzUTwRhRSOFFzU561rMDHSWBdpVLe391QFauzi8sD4o_KRHjaAvyEJEab_zmbFRsVF22NnieG32JqzPZ2qznZpGpaCUlUgQ0d_XLnpkGwxnuBhCMDVLMJwjwpizCFfNTmXtNTsPA2YlxVJos5tm6A7ml6LAZv2lVbxFEQgHaN8lfXVdogU5qttyawtpeZp-SKwuphZG-B7S5hksheeQwH_WlbWVKx_BO7xNkFJBNtC2egvnaNJ1socXe-3GjykqQzppWgVElfnsXoZZf_M4w9Ft9RuGbm_auTRftEU4NubDXXyVTTkyvD-JPv7LcsMHsPjZaTcNuS4RUqDNzBIvf1l8kYIlj1Fp2A0tB_JwXJYp8KD3-falmYPnbf7H1VPAVF-8twsvKFTtBY1b16DOB5NMxVgCgDFGZAuJsiBUFPBwVBiWs0l5fQApBZjT3E_kr7m_QVw0pijHQk9n2RCStwuFUmhlq2-EsD9thkHcUBglHTLzuRhWTuZBp_ELjSw0POMgTIFKXfCTlMVc-F8l26ZetRAHvzO-rDGsTW-HZeP8hY2oqiI5Zy-EZyLla8bOk_U0FY2yrG7OUiwTU2oUtwMAvdj6gR696RgqiExcjdI7PIPxsCPbjkFFI6wValoXFOxVXFbo9ZglBLOKZU3XRr7mlmrK0eJdkEPeqPQIseGAKwJQr4fx2Jamuo0sANu6l1ALq2cMRYr3nFNnVY8Y98Jnn_3uWs_8Pr_gY7LNSCrLTi6CuwswLqNXNCriPQ5hlzAB3g0xvf1b5EsTetfbpV8AIhLI2rBgU_4ovEeWwr4PKpTxcGyints2zLqJSNkjzIlzsJ7n3YCy9tRmcc5jPyUgxHgFogkCK2ZZjO9ij-ei9yn2NrbwB075rtOOtbe7kh6rDIOuwpt24Do_YCEJ0lhTL3s4EyOdsCIJj8ICgRSXMj-SYkRRNFHjF8FjDT-kOJSYQ-ECzmZTC3T0dHKK-1vG1AGwQ5vjzrO30fr2XrmKXzU-jqtGmfNrcW_l-HCawhlJR-uXfIY7Qu-HgYCnKQIvn7seWA3teAXWttEWa_QHM5ceBue1Dzn1TmNwTZX4TtXFVjy6VrJxWGyqst7o3hfqEUxQIlXsNnl4DG2FQG1sf8IN1tvJQSVaaprqO0eJsxTNPAksWUjRQjnshW9iEwTnUfoUN5wU_El7dvfrHO9t8tCTYRW0eWad5TyHAYt8ZiW1JIDTxnSvZK8xWk6vCpwKuSBME17-Qe3lPJmBn3ZAM7E6_4Re53UbUAFbXjJJc9ZFHTYecnBGTpA3M2DbvED4UrprHdQvCYeawRyhK23-S0COgwWuMtAJ0xQx9O3cZRiG58gQze5JWB2eInt1Ojgtng6kDqDwpsL28wlD-pLBRRbBty51JAchRc_K8mQK--7aMRqhz02bsq8IshD9_73o71KSKZylV5LYvxPOdi0QVVtYXvDN9_QZPnvMCw7EtsWvP5fPt03MIxfzw2LrG4uiEIBlBv9CLIdHa4dw2OWEGqi9n_3z3INlRyoT1oboHsgzpRjXQEleYLVc8OxQBqeZJACm3HmcYL3reRAiEH6YFaHZxuH4lDnuEIp_4IetL16YKOYw4uBraUU8boTUzdju4QqHjE8ww8zLdDp3yB8jkn6i4R0YJgCunR7d1LE4nu9dv2dk290DaFKoHGH80JJwidO4oGLI1AW7KDoLE61vzkoL1oHvGCDogs7YWNk0yniEjZrRlSWiGMoRDjXcTPNe1kLGNWNg7pTLtvr-InMG_iRft9h09-WLyoXIBiWYG1P0SG5pW3Bo17a4g04eUWEc48hRr15mnf_UZZWDu1GnSc7aVcmx0jG0DFugTiJ9ncaiA4H4oqOhhdCU84Lm7E14K6nG1jz3ZlYq-0m_dnn1Se4K0N81557feJByk6v4g4Ke2iyWD14L2vN1bteZF1qy4Pf1Q28i5YmXp-7-wP09UVRsJmVFv5ku4ISJfu5NN2DCJfWSJuH6f4QGShzd6I6RXYoAs2HUKQfyK78yJ16H6a8gpbL1MCL9QXAeYD0WGkxt3YDA9af3Zob6d5muHM7o02akT85GnN4tXLC4Q12WWNVk8ankc2945aJn_aSWH14U6vxXL-yYG3f04HWBcAatwo9A1ahn5EWKGX4P6ndtsriH706Eywsl28s4Pl2XhuYFXa-K6mkN67vs69bjd6XSiCHDWniOntjM4Pa6cGYA1OhzGbP4HQ8ozUPMXCaDqyIpN14sDsmH8XkYaVUz8a8AeVxpO14PWKpKVboeE9u3Yy0qm0o_9XOniZ2As3Ym6Nx3B6BKO1H1iK3fFw0LSH6ukD_HYjWeREoNmVxKk2IVfIUCaGofTqcI997xzFJegwFqooz__Cjj0R9tctq14Dxm7ShUOSZOVt_zqvTVoYpQ5M31L_jh8v-9JjJjCz_AxEWfwN6w5KaRzQVxaYGZRsLPrCIZV32toFsMfaVuAEq554-FnNDmPjec1OF44iTJE07qfzveIeyjsYBBnf5c5bkTgzPKxCrGMo8PUY9XDaFzntHwNBLPDaCVchAJ6GxQ01L7UWxxgkG1UMk6T2Xdijm1-HeMnMdU7aCerTPv1dEgeCNPF8OQHs2F2vruasaLYiQxZYwopRVLg9bJ_bX3UxbNuy4zHZOfxRwkHajDubFjbBbMewcDYod2NTRLg0-FT9luqBWEeTsU54oFKxQ76G85OhnD5PrQZnWjpMGZwJs_CLCLKMMFwsT_xIyRcVwSah9NPX47lQZPGlsoqZQ6UYGeB2FP2rabLJuhSMMYxgEFwN4Tzj_wu2HklOyHrCsE1qYarcccRtefQZffL8OLTOzC_ZUfQeHJxaXq0gj8DO8jVYMz6Fy193oUL12RA95he_wCPrx4ZCCf9IOJFPNLN3WtMU4BpvtSQUdBpR9wUzgVJi1IyK8f3Kc-8k6Mzb0Q0qxdhXVaptflkhnofwjM47EbDpjYHqpmm4dQ5NgzdgkjJnjN8ak6GaglktFToqeRwUasfojGdw3deXHQlPD31NBpQLEJN8TG_wnvZznsDFigNEXArp3nJk5QGhEXmSpAa-NsYgRcJKgeCCg1OiXE8HKxm1ZQOfWxAWfcPaYBslHqgNbrzyHULMuAVMYbTgyhPirUNd6MSavBy-7kJQTYKMdxV70632QZlrBGjeJfgRFgbNCNe13NnhHiXufy7Xh6XgrLP-ln5LnR_mI9MDj4oXiCD-g_w_T4rIwJv31-JvAU8TUVw52WDw553dpgcuTNeM_6Pp3ecvy6wDjXheFsHQxodHvjVMeKQYjvTJPQWxoUjkabTRGNcwzgyx_3QXLSVpi1rHu5DS22Qt50GjNF_8MgMVrimg1ipCycScPM_TiSpjhWniqcMlsgEoYyHlicYjAnZAIbTREkplbyLZY7TmZ3l1FFGjdkTwYrqi0NlIz9kFOzQA_OzCqMS-it-t2ZTsMYkzlygxN265kEEcH-hMc4rs_MlT5eiCIARXeo7ribZrDEqNOBASoVFcow7ub7eC9Pou0T8D9O27e1nZRzZst9ns6n3JFeOjJAqgWlSj5oX-5Rmb0hoNTTXGw0QjbUd_veduNtrc9-saWO0pAaFcvqk0x0XPFw5DMLHBIL23OsOeO4OYKcnWQ8tyXOYP6Xdd2IreEghT85swv2dpvOJ3z0dovTL1DdIB2rjnPTrPl13SUAd8pEL6vUHYQgfTnCTf7DYrbWTKvSBB2Dawja91RQ0WwSSuNvQM2yhid500vEkaPKghLMgz7fkisuAXVknL9R9RNMMs62LdSWOIFLpvCqdoS_7l01QNREvtEBQcE1iF8ddViytEMkMnzndRPf_-kia0uB-OgcTc8dJjhR5_4pQAhhyP6iUJp9eDQPnwUF9uqOohMp7xDOdNR8Rzo--QNqEikhrdvfVjMRfCTnMMhF8crb3qwhrbnYPl_gH3hHIdhsd4Mce6OckEYPXDJCOQSdhSbd3GlKfcUAIZIWvtkbbJslhgZlbcRpg0nuAJH2JY_5F5gDVg_NhfJvMVMvlG1redukAgdZRCoUf1JJMyUfN3QZRjCpsTMiJ-gpwktmhIjim0sjoRaCDRMI_deJJa4BiwHLlUzIPzJPw2EL-Bj9TwI4gfAaMyVNEcjEw__7JvWlf-iyNJn4t76SU3a7FeINqHl6ThiOpQ4J12QD_kc1ERHSMcD7fwyrgdiTo-w4KABEA5bVWklk8ikqbT8sARYszZvoFHhhsogYnaG59PazLJ1Ht9mfNAVRLZoPrPpirGzghKvpyC2MamTdxgJkl_Kk2d3npzIrW62gXgZL93HPqwyZ1ugSVTDRuYUJDb8CxU-9MZ7Mk5OWgcyEbBNf2-ktDnqw3NEpiOoEFCPUyPquKbm0nj_kSt7JK3YnQAtaX19mAQyDYRxc6bgxdsb4mULZ40cbxpf9fQZrJrBDgTi-pzFLRNm9hzlb9Auhx_k8GsiVwmMy0kg3ct55Oc0V7ah5N6rKrVfY4rfV5yf576YdK9tSPsrgS8NsshhfLjXPTJacpQxQnJEcAu12I-7U-f0qj-ONtawvhwlftv2OHQ8jqfWUls_hRqmDJSh4zLzybFwkqIISEsWsQsIZYJ6p6GDtQFshTcYdpEUsbcxHygPcN-PWlJFxgv3HZ-mVY8GNRkkIDBKa5dApRHboIogqpa_Rvf855HlkuLMh7Z1v_nFn7QcQqzXcUYznLNuCOwjkDqNh4l8IQkVblRdoaFdS30oGYTfWAVMNrQiVT8MiIyOmN5tlAAcskvwEkJ4_z_GryxjRMN8mFa1k9EVq9SXH9boN5EVELeNOFTHd3hth6BqCnzezjXpbLyFtfKHEvpRSVOzgxdXX_bUavf8Ok2kqT18npTahgIbR_VR1l05cdx2UFzdKzlNWBhRPKHtRSv7FhGyFUy2NmjyOCKt_vbn6DGzWQ-tt7yRAZbXR1ANgibME0r8iqxAeRa6TspPLIMFid5kBQbkI_0FYl2gBvFy1
\ No newline at end of file
+xLt_RzqsalzTVuNW_Q5jyBhOjjS3pjWxhECuQN29OzXE5zkYC6Y9Ve-DHBubAQTkh__xWQI-a1GbaPAUaxJyoNuKDJFwS4WEPyZXFnW7b5TbaQJfPocu9oXFzvJS5h1awNl4Tod0smBcQKR9UvRUGSYJD6Nl4Du32igqG1ZoXprC2UKxOZpA2i1O-bkIfcdwbD0SqbDI49h-vPjl_lFal_wcbL_lKFOEGyRIhvCq_LsASYhUXYn9Qped6KVtY3zSmWpj4SIS_PQaJrAKfySfmXnk8KsFqxzl8OOKS9Z_Jv9kAS3yfLdXykpvsUNP-UdPLNhqGZndHtz5UP8IleFq37F47qRUUDFMGh1-yKaAMidsX8xnEp4gjkzd50g1JtXwd79KGiP1kLVOYv_fhmJ3z3Z5vFtdNuZwrJxth_zxajZu2UOd-pqbzv1EWxZy-oe6wSXRRncaZuD5ciC2JjLEKaouU8V3NtYICWvunTBZ5EAg74Bpb1G5k2KKSFBdGdG0Z-UeW8-1mnqguCXRKemG77o_V-YxNn1GF4Umo2yXsnFCPe0WI7W38Igt7xWbeBY6vRu074eXBYWfzwFZd_kDmq2MqXgJkclz-DCOYfUGaOfcZmeWhNcElDsATHY8577P5CsGes0I_ycVDoDGtHYl_vQa69GATrcUI4vtL1JM9Fsf_Vt_llnoh9c6MtAw1Gb5MuH1CT5oHbij7Jcs0RpMRUEi80nULMmFSNvAiEWAxMIZFxk4W17SJ3mfnFap8ESbmIH94PDV1ek7innnA3xbbf020l7A7IYOh7F9ETY3beXGY6vcaotNFcidkdj0insWdt_zxRTVQ3hJl3VGQT_S_WobGAU80uu83k-vBGczOMG3r2HRqSDdjP9Wn0b5U7i9xo25E8FYMtzc1D-kDUjr492ApXdxdG2CwdFxHgUialS8WLgu3saUj5BFinFMWEbZinnQ2UTFV6l0yWbEWNtaUlEWFWFoGlpXu8AIUvJ3V60gdABgc0_xhqmqzJNPe-p3j3CNh4VULw9za0xWRo_u0D7_zAMM0vd5zR3hMa3V23UA8SzNiqsDzWjFbT3VGGexysBGKc3LNW1rBPn13IdPtrgUMhURi2RcZuBX3Ve9Zdd3GztPwQl_FMqVlsKmJrAGvqYOJw0u9rnur3Ab2yBJWLqljEEbUspq7n8dHiBfrYq0usyLXayV_3mU8RuH2hU8uD665nKUgHS_IOL3JjcvdJXV9vg6rwxFke-P_SzgvhRN0UNgcroNW2AhX2SWlp9SNessT2E7087GIr9q_Jo71Rg7zE4rv3kIzqJBAzeOy3dCK0cApx_PVFM5Irqxxq1_oLCAfP5LQhkz07XRWQsDXNYvW5kGAPKnPN7glCOLL-Kjf6_kBW3bA4DxuIBMboDxi8K28LYB8KgCchOaUqgs59IYpLoFQJFuzX_ykuACIoMmHbhXiZUWOTLjYPZG7Qlnv0FEeOaa1tGB-GNPYmON2kvWBqq4vmCE7FeddjmZoYjGFAkLmQMbxNYlkqo5AeMFz3e5xr7VyDIsmm0PNGn8yGvh2-bYIKMPHAC4UtPDHbPv5i56_djGeFpvA6BVrrNcZdgrby7KsIUY7-p15d3u1h-6T0iDZmMd5JoGFry3kImhZ3Afyg_8rdVrcLdlZIwkpQeVqNgp9vh7bzDcmXcT8fjutA6Y4w9U3ZW8zTVet-xda9diL8jS9uRtxKcw5vCzu-mDVhil271wy0xR8Sxk8UPgNBn01U7dGDaFfn1pI6KxUIW9jAYCDBtDi8KI6vn7A9WYFaf0p9Jjt3suXLfwGc0NgS6syHkez1bHirh4vEpzuQTlBo_Fpgzlhb--FhkwFdbzuUf_v3qs4NURqm7GEDZs3RuzoUmFQn7p7iuzXnmnP5-XNw7vDU_WWwFv6vU4a650Gyn0Sl0G9Y8IeM7cp382HYUk0NiX953ogGNw2uJVFGNPkrQ_3BI30xaXK7suO2t19ys2y5BtwiWdLidYm6veA3S4maFwRTmWwVEo2LReZEtbyraQt9qAvpe3kABm2fMe01JnXnUKL7k_A6wu_bji9bwrQX_85GT_oOegX_dBktF2J_8IOKSZ_pu2rZHGTw6nxdbM-BgugC7zEy1UOhc210CXvvu7om9BJeAdEu2t7kD1o7ooTa1__jnZ-yimV7_bzfYJ8XzuqZg558aXQr4-aEw9lZV6mDqrSNZswEPLpwafVAYbkUWGffMrbo9gYoKpaE2zrm9Knhw1E_7vHe1YXt37XkmRPrZZ3AnrlIwLxZudhbrChu7XmEWIcw5FD6fs6uMRgWshqCb_BXwVH1372SpKnAxFvXCljxDwhvwhQQQRv13XaROU8hYD5ZHBeXrWExtJrvAUcuuJlVWroT4TMgqHPRofZWTCjBJBAuzpGFkXJJ_xQRxsAw_L1Q0Gyn2x8cHx_CTaEjUIV8-07d_oME_rzz0zfwIyAW7bR0SXl-RWeW1ONmEDRw6Zt1ruNxgrJY9_BWDHhn1-j-v91_-ct7PY14-zOPv8ATybt1SWWsKTu5pihdKINSbvUqYth2stP_DRNpgb3KF0K5MUlmXIAVKMmjiNgslYV4kSTf-eztiJw3wwuHFVuLPeXWesFN6PynbWN87YUOLpXBUl2UQx7iaiHwu1kbPZrwDkQ1qU4gIMGhT_L3wxsiCCWG8nVbO2L4nLSX92M4Q9fNqnAP5UsTe4LKVjfqJgLNCAfGViH-mAiQlWbEJEznqJYAR7fDf_4EVmbD_6yDJyWTp3qYtc7ZqSwvgW8J4CqmEjuFmyMVxiPyoUmlQwg6uBb1bx3tU0mloVI0vQvf6ybp9Qz0nmu1i9Kl6bv-EX-PTjfM3GZP1FUfn-zlBPsVdro-SlpvvzTdny99TrbjGtwfqZVvJapW9yHxeWcR6AeZ0ZD3ybbTgSL5TXsGwMeA_OsiTw-37zjpGef2TH3jyMGDvZ4Ji_uM9Hg6_KIe_JlzMUf0moDbp1lv7cDX3VFnzj8mlzDVjT3kyGHhh_IaPl8GQt1HpOSN37zrUGSuItnZjM5nOQb5SpV792RLu3zgvzSGzufwvxmlY5QZA4jsBlbYvwYzHhW0f8pLqijo84ozsxnhI9tTv6DZTCtauzK1W6SXbYxuTxalvCRXrR49-8RNd29xVObgEGLGLlToAoAXF8QGEcZXZc40ESGH_OE4NRBQJjjjksbjvfBPfjvmKSRWbzq22ViSr7UdfqLwt_j76bOBYh2YSIxL4mcMBWgYfBG17qdicD1R5hAjSMxaT_FTBUEL9hXNi4tx_CtGHUQ-VKHBs6IlFMKT3y8zqGfq6rt_g6WpmFEmB6A27b6xe56FrgxmB-TlCVtkOOB8dQY7Eb3Xhqo1Zp7iACqoeRVuFA4Q3Phpfje4k4dWKjvhi6VOxaYilRRBnJzQSrfIRSg4hxtb6hiX1EKtcXAXfgAzuxwYR9aVcx9uqAR3NKclUFmZRWI4frlAQPPImVySUWuuJ1wTLlHYY5KX-VGP2_GXWkzv_L2kp1lVL8wcT_5shoTuY9trwcud9L3bT19T_3Y2O-ubXyPyZKilmuLDXFsNB88IqHDizY6_qH0ofEUjntckO8xwweV7hZhcWsztNjK8xlyU_oCaeEqJ5chki_5KrUHGFNLZfxa7mqbRkg0vUFPvGqeL8lfMt4qnIrVlaJCuOVKJV_SCFYGSV3E10kQfj77JVYCYGkGnmhrXL7ENl5VFWnJYqlwi_nzaCjNjH_OjuiLUyQbV23p_HF7u9rewatAVzZ3DnQQwpOSInTUAn-yyUnTVJlMTlKwM4LBPtWbl4xChruG5orLSLXX7htGKZkl4-fQUrjdPP7IbDNKl6CwFitLIvapa6sAqsO0aC3gW4D6p50u_ePFHmwQAo3rntfUelnmuzr8Kw7LTq5Y8lOjvLWSa4l-VWM0dQsJBzUjArGBYAvi_HNF_3KDlQyT08VMog0yDrJbc1Bjp4FVESXjrvrdm9_uLSSxaEEyU3wdR6cgwjyMiTtUc3qZ-uGxZgwC_6YFBRJgsCtmMEamlFMAJr3llfvFMUhHaRAlHXUd3EVbaxtyNUspilo-CVPuJhZGEF7SbhisZegQfH_WFDYVKpz3eFv7UFI3t_DIOYwna7I1NUdXFQ2GvykquvnpqkUEljmsXsYZ9_M0w9FtPVuGXm-a9jRfskV4tub5Xb-SjJVvgN_dZABgjuYYyhU4elAlW8FdjWREgAdZILTHzOrUQMLc4S2lNcfro_4dWsP6jBFVH4sYBECZ4wmL-BvHtnlpOKwltC1AICOmsAfiMxJCx9EGpExJkWNziwHrFlR1SDUAYwLdNNPxc9uTwV6-qdt1tGUZb83ZqkPPVzsFTXJU8T-SUIMldCLDztJVIogRWlN0Rz377wURwnr3YmhLQqReJQTxEWvCyUJVq50Gy-MZLotzg66jh5pY7GqHN85aLKc3dnyTdmiVeDCvDgz1_G1uQqEmDPpxCHZ-VmkLJRVCqgDHSpGeOLr5xUbEoqpsSCsBxETVqPA-vJa3-rs_2RDbxFGUMonecW43t-FWlzfe18WF2SqHulsBMbDg3H5wfjQRIyX1sOUnAR4pu5NeX8wrT9jPOYyFjvNH8IaCFuOBd_mJVdionsnxWYkESik676yJVUgA9opcMqiTD-lxA83s8uvP1b5UsUlNjcol8BIhDIIbFeUl0mvEiXwNCRCpLxb0-jnds5zraISNclZIdzsJBpJ2C_97RncMjlPkMgxZqVbLKOebFbQGRPRTPGJRg6lB3kMWE9hEmrlRGFTsDOQ4vBrBc48Rb_4uKa1_Kwg7Y8SuvDiuabQGiRKtH1jR4vbysqE-ZQHmVQOR_TXDw9g4uptIDqmDq6TbHJu7b04f3be7cttDWC2WqA7d1I7vxwtJTB2bUsQ3n_v4opgizDlxY6bA8Thzv6gOp7nIdE8-r09GUz1KS5-v41dxQEnygM-AWQSS0VT4UZRuHJTuItvVPlyKQq7Fj1enyaxwBBbk6ifuTjyRHBN03oc0TYH4buU-4wb7PDNzj218KmitrwHhLe6hk-fSVkv2h3jWiRfSNfoUNdohnz_QkOL2TwDh7KayWA8JpYf-8vGwK5sP0veFj9rVv7JARGZ69uxyqCP8MnD4X6l1lXUvhD0ZAECDNl8RuHDSLEAErfkI3sCYV5Sf8swAG5z9ok8EL9cFawqorLlP9ChfqAI_hi21U8FDu2nYegz9JSvOBjR0cZUX0j1eRXf3pW520jQQWiMqvur0NNzCsJqN1OqkjhSNxFKbxlq7Xp4bBdgZaOzOaAv7qwUrQj23r2w9IIXFP_43oVFKS4XzOl2LoLsBelb3gpHsoz-E7brObzwvk4r4-RyZLP3bfN53M2zejk7Nb47vCIK8-h-8jbKbL8DMQJlWEWmj8nm3D_VKFB-nbnva29tbDRBl1k6jewMU6LkugZZeZJ2D0hJm6QI25KVAikR4o7gGpHsJqZ4ousNm_CVeN935I4QXQZR8LXlUOvuUU9btuSNqHf1Ayw3_bZ5mJqCBzkp6y4Q0IlYD8fJx7jUC4LuB7X6hkAE0Da0MJTJH80KGACtQqUGN215W7e0nLM90vPdprvvG987V5XkVPYuu1xW_8Q3RSzNBofB4DjF6_ncTPJe1kLG7WNgRpTLpvrnIvMG_iOPt9h09-WMywXIBiWYG1P0SG9pW39o1Na4g04eHWEc48hBr15mng_UZZWDu1GnSc7alcmx0jG0D7uhTiJ9ncaiAKH4oqOhpdCU84Lm7E14K6nG1kz3ZlYq-0nVdnn1Se4K0N81557feJ9ykMv4g4KeAiyWD14L2vN1cteZF1qy4Pf1Q28i5YmXr-7-wP09kVRsJmVFv5su4ISJfu5RN2DCJfWSLuH6f4QGSjzd6I6RXYogs2HUKQjyK79SJ16H6a8gZbL1MCL1QXAeYD0WGkxx3YDA9af3bob6d5muHMNo02bET85GnNutXLC4Q12WWNlk8ankc2945aJn_4SWH14U6vxXM-yYG3f04HWBcAaxwo9A1afn5-WKGX4P6ndttLiH706Eywwl28s4Pl2YhuYFXa_K6mkNc7vs69bj76XSiCH5WniOntrM4Pa6cGYA1OfzGrP4HQ8oTUTMXCaDqyIrN14sDsmH8XkYaVkz8a8AeVxrO14PWKoqVboeEAu3Yy0qm0pV9XOniZ2As3Ym6Rx3B6BKO1H1iK3fNw0LSH6ukE_HYjWeREoRmVxKk2IVfIUCaGofUqcI997hzFJegwFqooz__Cjj0R9tctq14Dxm7ShUOSZOVt_zqvTVoYhQ4M0XL_jh8w-9JjJfCrzYTlQKz3ZTYgAD-jCzIPAHkx8iRk9IFfXRx7vBqoDybFOIYgV7uZeuisoJWa5cYUCgd03-KtSqfSSMRr5buoWpNrjTgjPLx2rG-qOoT4N2R87wb-dqf6ghR8OUjEyUKZwL9dgETEhD0yhMzDXGoUHx1vHhMDEcPNiCdLChund2gHSjoUO9rCJvUbBep9jAgt0qhcrqiMsUhSRD7D77FhHNUpKPt7raazBkwMgyr1WzzPsorceyjNunnRYhkXhtuPvkCn_gro6ysus0wN7AzZ13e42CjwMYKUiUevMPOYIzspLcMXhgtUFwLb_xoKQMFwUaxBspI89UKcnXNLbb6yFw4HoMiUnbf98MdLMuCbFtpiRqkCxgRprmChUklmXgPiVz8jAeDCitlH8rd3GcmugsHoR_cpGrmZbtB3g5LQmQnHQ_abuA_m0M7ZScY3MIgBLHVyOhBnB64NCIWubUocefd6key8ddJYwazEbc6TsUzQSpi4oyK4v3cgzOkBKzbCT0JBbh1VcpNdek9-wCn0fchl9cmUj8-HquIBiogzTZjVLeuofaoL36w7KNfdj9jD6URksv0lS9VYvEeUIT4YgGcqURckKwZFfRoNlijgFLLv52Dxg6cNS6rnAU3Gzc4a-MkoCQofkKKE5p0yMG6q8QTe0njCKmTn0A5cR8YjhqRAbvj7jghuYRIXzAr6vr9QpPoYkEKivXQJxSlU5KJ2fjcm-EWC5iz4i8sYzXUkaikdBi0XJYsbUMxL2HpqF3MB2rvcpTluAfzJU8YBbDbBp1SAF-ktvF57LP9BlnUvBq_BJ-J8Vok0ahSk1Ht_kx2dyp_OB1ttmqmD_EOXsqBdAztzdHrgF6eBQALnUZzRVhPPjUKSTxiVkgzkuxl5R1zDi1g0uOg0uMJCK12LK_ynUAilhPP3dPc8zDvCog-hCvahMvYvj5jFfLDr0uHVkWYRAnZAAbj5b7SxwS5SxX7S8mxzIpKBxx7MgzDF15xqaIRdqF-YisVRV5NFfc6sxqfapqQ9e_MIjBiHM7G-RJcaSuVPFsHkF14YjvQSXuRBrvd7AAjbj8OVxqOTFzI3po4CwQ1kpyZyP4qCiVjka_RDzy717RB88kGQqgZVaY9ortbhqb0hMQVDSHT01Kw_Rw_4VRBBor5ilJ9heOq27rSAF3TG0fdDodkAwaegr6iB4UCwGGAmanra7yUiPAWWorX9ErdZDjbIROTWHzsc0n_m1xkdHnJPmPlzPUMdIaDuCBZXKb6HsftBAC35HBfPbi8biMyuNL9N69m3PEgPAHL6SB972E5UQZWVwwfXG3E3Zv6acdrfgjHeVhjkAgKhWdIssMrBxUoWAqUW096wL-aQJvCld9W0_8imi-drjS6m-4LJ_fmkVX8NVT-uZhiatxN-U5T7Z8LtIr4xjmqj--I9v1LrrFZcJDxKa6jyuzFhqzQSHHhktzc2NgT41-uuxDB-NNULwrxvVkL_sJz3GjI_rPf2NsuFHgpMLcjg_Ef5EjfEVFMUGAQaQ2YvvBI8qSTddIkdpMy41jUgweH0DwVbHQ-VDgGlsmD9l7PI1AeJ5oHYdcyMhiUvswCkPdqND70Uf2VLzKKgVHc3r8AQ453NiKOZDecv79Zh8-gC-Ys-5R4sp03QNad8OQggG_tuHpqB8eQQJtNUaCceqzKec_ctH7IgY89DqyljPMSbh_Fxw1V3bVv-xY8UQExSJB9V1Xl8pVCBRRnMWExI0mUVHt3ypXvR8QEpvzgr4TsrXsv8CIjKV9UX9CkqiSqnAbRL4mjVQuSTqRwyCeWcAYSexC7YgOUDqSALoxsrRScTNixDKtQgtESt33bbC6P-saRh_r3WfmyS_KiO1WheQgrImqNTDF6mUgd7sGM-8dYpPI3UtdYPenrZXM8CWt6yhQyeNqxKM7piDSR2pZ8xUnLtmdjXGNWF6t-rmS91H1B5eREQ4CNCPhmw9lkOPIztEDABXy72AX_5qxgHIt_awgERJj_JmTRUD9E6rNaBWcteyHXr8VQmMy0kg3Yt75Oc0T7ih5d6qKQNso2QNNPMeH6zefL3ltMLiQJD2UMpVT33kBbcFGxCWk7Z5T-H5Q3lVMWwGv5B_mTCfzJQP_Gc8MY_QLnlNuVKk_SJKq9HEdU_5H-hl6at3kuDdCaOqbnipF63WYwq_QOPqodzDQkaVBcvjzc8FrzkolmaRVidyW5bwuhSlIj99OoCcsPSWj8MZjdxRT9Gi86-7XLKaFkBp_YVYELCrkRBEyLxYgF8QpLVURedKPUIcry_3UN7t8V4u61kX4xR2Kzilgv0T38UkoCHnNrpjAgkskPsDkpCSzUyryxZQM788Fi1kfENq8SbG8ro75kQDL8LOFzPv3RngZX-5OkyQMlVng-BoKgEbSXhlFKStjByp_YDHS5WDdXPOE8gOxUoKpPQkVFfZNmUmTrlC7czg_7jp4jZYgONiXyhdqy704V2dyZHFJZ6_E5LIB0BRUVlzkhFo8jKTGcYvR5JWWnJ8hY-eUrBdVfeWqpiwvLDIt9Sa_8CwhiaZ-7m00
\ No newline at end of file
diff --git a/docs/logical_data_model.puml b/docs/logical_data_model.puml
index c8362a2e39..3e79647e78 100644
--- a/docs/logical_data_model.puml
+++ b/docs/logical_data_model.puml
@@ -2508,7 +2508,7 @@ Grants "1" --[#black,plain,thickness=2]-- "1" GrantNumberLinks : grant, grantNum
ActivityReportCollaborators "1" --[#black,dashed,thickness=2]--{ "n" CollaboratorRoles : collaboratorRoles, activityReportCollaborator
ActivityReportGoals "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportGoalFieldResponses : activityReportGoal, activityReportGoalFieldResponses
ActivityReportGoals "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportGoalResources : activityReportGoal, activityReportGoalResources
-ActivityReportObjectives "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportObjectiveCitations : activityReportObjective, activityReportObjectiveCitation, activityReportObjectiveCitations
+ActivityReportObjectives "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportObjectiveCitations : activityReportObjective, activityReportObjectiveCitations
ActivityReportObjectives "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportObjectiveCourses : activityReportObjective, activityReportObjectiveCourses
ActivityReportObjectives "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportObjectiveFiles : activityReportObjective, activityReportObjectiveFiles
ActivityReportObjectives "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportObjectiveResources : activityReportObjective, activityReportObjectiveResources
diff --git a/frontend/src/pages/ActivityReport/Pages/components/GoalForm.js b/frontend/src/pages/ActivityReport/Pages/components/GoalForm.js
index 241e60016d..1961112c7c 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/GoalForm.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/GoalForm.js
@@ -29,6 +29,7 @@ export default function GoalForm({
templatePrompts,
isMultiRecipientReport,
citationOptions,
+ rawCitations,
}) {
// pull the errors out of the form context
const { errors, watch } = useFormContext();
@@ -221,6 +222,7 @@ export default function GoalForm({
? ERROR_FORMAT(errors.goalForEditing.objectives.message) : NO_ERROR}
reportId={parseInt(reportId, DECIMAL_BASE)}
citationOptions={citationOptions}
+ rawCitations={rawCitations}
/>
>
);
@@ -259,6 +261,18 @@ GoalForm.propTypes = {
value: PropTypes.number,
label: PropTypes.string,
})),
+ rawCitations: PropTypes.arrayOf(PropTypes.shape({
+ standardId: PropTypes.number,
+ citation: PropTypes.string,
+ // Create array of jsonb objects
+ grants: PropTypes.arrayOf(PropTypes.shape({
+ grantId: PropTypes.number,
+ findingId: PropTypes.string,
+ reviewName: PropTypes.string,
+ grantNumber: PropTypes.string,
+ reportDeliveryDate: PropTypes.string,
+ })),
+ })),
reportId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
datePickerKey: PropTypes.string.isRequired,
templatePrompts: PropTypes.oneOfType([
@@ -276,4 +290,5 @@ GoalForm.propTypes = {
GoalForm.defaultProps = {
isMultiRecipientReport: false,
citationOptions: [],
+ rawCitations: [],
};
diff --git a/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js b/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js
index 624a20e298..e54d781087 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js
@@ -59,6 +59,7 @@ const GoalPicker = ({
const activityRecipientType = watch('activityRecipientType');
const [citationOptions, setCitationOptions] = useState([]);
+ const [rawCitations, setRawCitations] = useState([]);
const selectedGoals = useWatch({ name: 'goals' });
const activityRecipients = watch('activityRecipients');
@@ -146,6 +147,7 @@ const GoalPicker = ({
}, {},
));
setCitationOptions(uniqueCitationOptions);
+ setRawCitations(retrievedCitationOptions);
}
}
}
@@ -303,6 +305,7 @@ const GoalPicker = ({
templatePrompts={templatePrompts}
isMultiRecipientReport={isMultiRecipientReport}
citationOptions={citationOptions}
+ rawCitations={rawCitations}
/>
) : null}
diff --git a/frontend/src/pages/ActivityReport/Pages/components/Objective.js b/frontend/src/pages/ActivityReport/Pages/components/Objective.js
index 35102da7c3..b3c891d74b 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/Objective.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/Objective.js
@@ -42,6 +42,7 @@ export default function Objective({
initialObjectiveStatus,
reportId,
citationOptions,
+ rawCitations,
}) {
const modalRef = useRef();
@@ -66,7 +67,6 @@ export default function Objective({
* but we want to keep the logic in one place for the AR/RTR
* if at all possible
*/
-
const {
field: {
onChange: onChangeTitle,
@@ -349,6 +349,25 @@ export default function Objective({
}
};
+ // Store the complete citation in ActivityReportObjectiveCitations in the DB row.
+ const selectedCitationsChanged = (newCitations) => {
+ const newCitationStandardIds = newCitations.map((newCitation) => newCitation.id);
+
+ // From rawCitations get all the raw citations with the same standardId as the newCitations.
+ const newCitationsObjects = rawCitations.filter(
+ (rawCitation) => newCitationStandardIds.includes(rawCitation.standardId),
+ ).map((rawCitation) => (
+ {
+ ...rawCitation,
+ id: rawCitation.standardId,
+ name: newCitations.find(
+ (newCitation) => newCitation.id === rawCitation.standardId,
+ ).name,
+ monitoringReferences: rawCitation.grants,
+ }));
+ onChangeCitations([...newCitationsObjects]);
+ };
+
return (
<>
)
@@ -530,6 +549,18 @@ Objective.propTypes = {
value: PropTypes.number,
label: PropTypes.string,
})).isRequired,
+ rawCitations: PropTypes.arrayOf(PropTypes.shape({
+ standardId: PropTypes.number,
+ citation: PropTypes.string,
+ // Create array of jsonb objects
+ grants: PropTypes.arrayOf(PropTypes.shape({
+ grantId: PropTypes.number,
+ findingId: PropTypes.string,
+ reviewName: PropTypes.string,
+ grantNumber: PropTypes.string,
+ reportDeliveryDate: PropTypes.string,
+ })),
+ })),
citationOptions: PropTypes.arrayOf(PropTypes.shape({
value: PropTypes.number,
label: PropTypes.string,
@@ -550,4 +581,5 @@ Objective.propTypes = {
Objective.defaultProps = {
citationOptions: [],
+ rawCitations: [],
};
diff --git a/frontend/src/pages/ActivityReport/Pages/components/Objectives.js b/frontend/src/pages/ActivityReport/Pages/components/Objectives.js
index c2b49d2dda..9d64ff6d3c 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/Objectives.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/Objectives.js
@@ -12,6 +12,7 @@ export default function Objectives({
noObjectiveError,
reportId,
citationOptions,
+ rawCitations,
}) {
const { errors, getValues, setValue } = useFormContext();
@@ -139,6 +140,7 @@ export default function Objectives({
initialObjectiveStatus={objective.status}
reportId={reportId}
citationOptions={citationOptions}
+ rawCitations={rawCitations}
/>
);
})}
@@ -156,6 +158,18 @@ Objectives.propTypes = {
value: PropTypes.number,
label: PropTypes.string,
})),
+ rawCitations: PropTypes.arrayOf(PropTypes.shape({
+ standardId: PropTypes.number,
+ citation: PropTypes.string,
+ // Create array of jsonb objects
+ grants: PropTypes.arrayOf(PropTypes.shape({
+ grantId: PropTypes.number,
+ findingId: PropTypes.string,
+ reviewName: PropTypes.string,
+ grantNumber: PropTypes.string,
+ reportDeliveryDate: PropTypes.string,
+ })),
+ })),
objectiveOptions: PropTypes.arrayOf(
OBJECTIVE_PROP,
).isRequired,
@@ -165,4 +179,5 @@ Objectives.propTypes = {
Objectives.defaultProps = {
citationOptions: [],
+ rawCitations: [],
};
diff --git a/frontend/src/pages/ActivityReport/__tests__/index.js b/frontend/src/pages/ActivityReport/__tests__/index.js
index 31565ca3bd..74a6234f7f 100644
--- a/frontend/src/pages/ActivityReport/__tests__/index.js
+++ b/frontend/src/pages/ActivityReport/__tests__/index.js
@@ -752,7 +752,7 @@ describe('ActivityReport', () => {
});
fetchMock.get('/api/topic', [{ id: 64, name: 'Communication' }]);
- fetchMock.get('/api/activity-reports/goals?grantIds=10431&reportStartDate=2012-03-20', [{
+ fetchMock.get('/api/activity-reports/goals?grantIds=10431&reportStartDate=2012-04-20', [{
endDate: null,
grantIds: [10431],
goalIds: [37502],
diff --git a/frontend/src/pages/ActivityReport/formDataHelpers.js b/frontend/src/pages/ActivityReport/formDataHelpers.js
index be5d9fc84d..126b08f084 100644
--- a/frontend/src/pages/ActivityReport/formDataHelpers.js
+++ b/frontend/src/pages/ActivityReport/formDataHelpers.js
@@ -114,6 +114,7 @@ export const packageGoals = (goals, goal, grantIds, prompts) => {
name: g.name,
grantIds,
id: g.id,
+ createdVia: g.createdVia,
goalTemplateId: g.goalTemplateId,
isActivelyBeingEditing: false,
prompts: grantIds.length < 2 ? g.prompts : [],
@@ -125,6 +126,7 @@ export const packageGoals = (goals, goal, grantIds, prompts) => {
status: objective.status,
resources: objective.resources,
topics: objective.topics,
+ citations: objective.citations,
files: objective.files,
supportType: objective.supportType,
courses: objective.courses,
@@ -143,6 +145,7 @@ export const packageGoals = (goals, goal, grantIds, prompts) => {
onApprovedAR: goal.onApprovedAR,
source: goal.source,
name: goal.name,
+ createdVia: goal.createdVia,
isActivelyBeingEditing: goal.isActivelyBeingEditing,
goalTemplateId: goal.goalTemplateId,
objectives: goal.objectives.map((objective) => ({
@@ -153,6 +156,7 @@ export const packageGoals = (goals, goal, grantIds, prompts) => {
status: objective.status,
resources: objective.resources,
topics: objective.topics,
+ citations: objective.citations,
files: objective.files,
supportType: objective.supportType,
courses: objective.courses,
diff --git a/src/goalServices/goals.js b/src/goalServices/goals.js
index 3f105970ab..38ad17b309 100644
--- a/src/goalServices/goals.js
+++ b/src/goalServices/goals.js
@@ -1222,6 +1222,7 @@ export async function createObjectivesForGoal(goal, objectives) {
status,
resources,
topics,
+ citations, // Not saved for objective only ARO (pass through).
files,
supportType,
courses,
@@ -1282,6 +1283,7 @@ export async function createObjectivesForGoal(goal, objectives) {
...savedObjective.toJSON(),
status,
topics,
+ citations, // Not saved for objective only ARO (pass through).
resources,
files,
courses,
@@ -1314,21 +1316,26 @@ export async function saveGoalsForReport(goals, report) {
// Loop and Create or Update goals.
const currentGoals = await Promise.all(goals.map(async (goal, index) => {
// We need to skip creation of monitoring goals for non monitoring grants.
- if (goal.standard === 'Monitoring') {
+ if (goal.createdVia === 'monitoring') {
// Find the corresponding monitoring goals.
const monitoringGoals = await Goal.findAll({
- attribute: ['grantId'],
+ attributes: ['grantId'],
raw: true,
where: {
grantId: goal.grantIds,
- standard: 'Monitoring',
+ createdVia: 'monitoring',
status: { [Op.not]: GOAL_STATUS.CLOSED },
},
});
- if (monitoringGoals.length > 0) {
+
+ const distinctMonitoringGoalGrantIds = [...new Set(
+ monitoringGoals.map((monitoringGoal) => monitoringGoal.grantId),
+ )];
+
+ if (distinctMonitoringGoalGrantIds.length > 0) {
// Replace the goal granIds only with the grants that should have monitoring goals created.
// eslint-disable-next-line no-param-reassign
- goals[index].grantIds = monitoringGoals;
+ goals[index].grantIds = distinctMonitoringGoalGrantIds;
} else {
// Do not create monitoring goals for any of these recipients.
// eslint-disable-next-line no-param-reassign
diff --git a/src/goalServices/reduceGoals.ts b/src/goalServices/reduceGoals.ts
index 2638dd8ed0..c69efb3931 100644
--- a/src/goalServices/reduceGoals.ts
+++ b/src/goalServices/reduceGoals.ts
@@ -182,7 +182,11 @@ export function reduceObjectivesForActivityReport(
exists.citations = uniq(objective.activityReportObjectives
&& objective.activityReportObjectives.length > 0
? objective.activityReportObjectives[0].activityReportObjectiveCitations.map(
- (c) => c.citation,
+ (c) => ({
+ ...c.dataValues,
+ id: c.monitoringReferences[0].standardId,
+ name: `${c.monitoringReferences[0].acro} - ${c.citation} - ${c.monitoringReferences[0].findingType123}`,
+ }),
)
: []);
@@ -265,7 +269,12 @@ export function reduceObjectivesForActivityReport(
objective.activityReportObjectives
&& objective.activityReportObjectives.length > 0
? objective.activityReportObjectives[0].activityReportObjectiveCitations.map(
- (c) => c.citation,
+ (c) => (
+ {
+ ...c.dataValues,
+ id: c.monitoringReferences[0].standardId,
+ name: `${c.monitoringReferences[0].acro} - ${c.citation} - ${c.monitoringReferences[0].findingType}456`,
+ }),
)
: [],
),
diff --git a/src/goalServices/types.ts b/src/goalServices/types.ts
index 120bd058f7..552c489fd2 100644
--- a/src/goalServices/types.ts
+++ b/src/goalServices/types.ts
@@ -69,7 +69,6 @@ interface ICourse {
interface ICitation {
citation: string;
- monitoringReferences: JSON;
}
interface ICourseModelInstance extends ICourse {
@@ -113,7 +112,10 @@ interface IActivityReportObjective {
course: ICourse;
}[];
activityReportObjectiveCitations: {
- citation: ICitation;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ dataValues: any;
+ citation: string;
+ monitoringReferences: JSON;
}[];
}
diff --git a/src/services/reportCache.js b/src/services/reportCache.js
index bafae2ad63..eb5f3a3078 100644
--- a/src/services/reportCache.js
+++ b/src/services/reportCache.js
@@ -141,70 +141,37 @@ const cacheTopics = async (objectiveId, activityReportObjectiveId, topics = [])
/*
- ActivityReportObjectiveCitation -
- Each row in this table is per grant.
+ Each row in this table is per grant (from ARO).
Each row has a json column called 'monitoringReferences', this is an array of objects.
Each object is unique by a combination of grantId, findingId, and reviewName (for the same grant).
To avoid complex lookups, we will simply UPDATE (by id) existing and CREATE new citations.
Citations to remove will be determined by id.
*/
export const cacheCitations = async (objectiveId, activityReportObjectiveId, citations = []) => {
- // Get current report citation ids for the activity report objective.
- const currentReportCitationIds = citations.map((citation) => citation.id);
-
- // Get all existing citations for the activity report objective from DB.
- const existingDBCitations = await ActivityReportObjectiveCitation.findAll({
+ // Delete all existing citations for this activity report objective.
+ await ActivityReportObjectiveCitation.destroy({
where: { activityReportObjectiveId },
- raw: true,
+ individualHooks: true,
+ hookMetadata: { objectiveId },
});
- // Get all existing citation ids from DB.
- const existingDBCitationIds = existingDBCitations.map((citation) => citation.id) || [];
-
- // Get citations to remove.
- const removedCitationIds = existingDBCitationIds.filter(
- (citationId) => !currentReportCitationIds.includes(citationId),
- );
-
- // Get all citations that have an DB id.
- const citationsToUpdate = citations.filter((citation) => citation.id);
-
- // Get all citations that do not have a DB id defined.
- const newCitations = citations.filter((citation) => !citation.id);
+ // Create citations to save.
+ let newCitations = [];
+ if (citations.length > 0) {
+ newCitations = citations.map((citation) => (
+ {
+ ...citation,
+ activityReportObjectiveId,
+ citation: citation.citation,
+ }));
+
+ // If we have citations to save, create them.
+ if (newCitations.length > 0) {
+ return ActivityReportObjectiveCitation.bulkCreate(newCitations, { individualHooks: true });
+ }
+ }
- // Do all sql operations in a promise.
- return Promise.all([
- // Update existing citations.
- citationsToUpdate.length > 0
- ? Promise.all(
- citationsToUpdate.map(async (citation) => ActivityReportObjectiveCitation.update({
- citation: citation.citation,
- monitoringReferences: citation.monitoringReferences,
- }, {
- where: { id: citation.id },
- })),
- )
- : Promise.resolve(),
- // Create new citations.
- newCitations.length > 0
- ? Promise.all(
- newCitations.map(async (citation) => ActivityReportObjectiveCitation.create({
- activityReportObjectiveId,
- citation: citation.citation,
- monitoringReferences: citation.monitoringReferences,
- })),
- )
- : Promise.resolve(),
- removedCitationIds.length > 0
- ? ActivityReportObjectiveCitation.destroy({
- where: {
- activityReportObjectiveId,
- id: { [Op.in]: removedCitationIds },
- },
- individualHooks: true,
- hookMetadata: { objectiveId },
- })
- : Promise.resolve(),
- ]);
+ return newCitations;
};
const cacheObjectiveMetadata = async (objective, reportId, metadata) => {
diff --git a/src/services/reportCache.test.js b/src/services/reportCache.test.js
index e8762e730a..2f9b4cbbf2 100644
--- a/src/services/reportCache.test.js
+++ b/src/services/reportCache.test.js
@@ -185,11 +185,9 @@ describe('activityReportObjectiveCitation', () => {
// Save the ActivityReportObjectiveCitation.
let result = await cacheCitations(objective.id, aro.id, citationsToCreate);
- // Assert updated.
- expect(result[0]).toBeUndefined();
-
// Assert created.
- expect(result[1]).toBeDefined();
+ expect(result[0]).toBeDefined();
+
const createdAroCitations = await ActivityReportObjectiveCitation.findAll({
where: {
activityReportObjectiveId: aro.id,
@@ -239,13 +237,7 @@ describe('activityReportObjectiveCitation', () => {
result = await cacheCitations(objective.id, aro.id, citationsToUpdate);
// Assert updated.
- expect(result[0]).toHaveLength(1);
-
- // Assert created.
- expect(result[1]).toHaveLength(1);
-
- // Assert deleted.
- expect(result[2]).toBeUndefined();
+ expect(result[0]).toBeDefined();
const updatedAroCitations = await ActivityReportObjectiveCitation.findAll({
where: {
@@ -292,12 +284,6 @@ describe('activityReportObjectiveCitation', () => {
// Assert updated.
expect(result[0]).toBeDefined();
- // Assert created.
- expect(result[1]).toBeUndefined();
-
- // Assert deleted.
- expect(result[2]).toBeDefined();
-
// Retrieve deleted citation 1.
const deletedCitation = await ActivityReportObjectiveCitation.findOne({
where: {
From 7ced6d014f4d85f7ff3e28111eae4a831cf8d2b3 Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Wed, 4 Dec 2024 20:31:04 -0500
Subject: [PATCH 049/198] Expand types, byreview query, start bycitation
---
.../pages/Monitoring/__tests__/index.js | 33 ++-
.../RecipientRecord/pages/Monitoring/index.js | 12 +-
.../pages/Monitoring/testHelpers/mockData.js | 8 +-
src/models/monitoringFindingStandard.js | 2 +-
src/models/monitoringStandard.js | 4 +-
src/services/monitoring.ts | 273 ++++++++----------
src/services/types/monitoring.ts | 211 +++++++++++---
7 files changed, 328 insertions(+), 215 deletions(-)
diff --git a/frontend/src/pages/RecipientRecord/pages/Monitoring/__tests__/index.js b/frontend/src/pages/RecipientRecord/pages/Monitoring/__tests__/index.js
index dd721fcb8a..e99a925580 100644
--- a/frontend/src/pages/RecipientRecord/pages/Monitoring/__tests__/index.js
+++ b/frontend/src/pages/RecipientRecord/pages/Monitoring/__tests__/index.js
@@ -11,6 +11,7 @@ import userEvent from '@testing-library/user-event';
import Monitoring from '../index';
import { citationData, reviewData } from '../testHelpers/mockData';
import UserContext from '../../../../../UserContext';
+import AppLoadingContext from '../../../../../AppLoadingContext';
describe('Monitoring', () => {
const recipientId = 1;
@@ -37,21 +38,23 @@ describe('Monitoring', () => {
const history = createMemoryHistory();
const renderTest = (currentPage = '') => {
render(
-
-
-
-
- ,
+
+
+
+
+
+
+ ,
);
};
diff --git a/frontend/src/pages/RecipientRecord/pages/Monitoring/index.js b/frontend/src/pages/RecipientRecord/pages/Monitoring/index.js
index 68a2dba532..92b346cb10 100644
--- a/frontend/src/pages/RecipientRecord/pages/Monitoring/index.js
+++ b/frontend/src/pages/RecipientRecord/pages/Monitoring/index.js
@@ -1,4 +1,6 @@
-import React, { useEffect, useState, useMemo } from 'react';
+import React, {
+ useEffect, useState, useMemo, useContext,
+} from 'react';
import ReactRouterPropTypes from 'react-router-prop-types';
import { useHistory } from 'react-router';
import Container from '../../../../components/Container';
@@ -7,6 +9,7 @@ import { getTtaByCitation, getTtaByReview } from '../../../../fetchers/monitorin
import ReviewCards from './components/ReviewCards';
import CitationCards from './components/CitationCards';
import { ROUTES } from '../../../../Constants';
+import AppLoadingContext from '../../../../AppLoadingContext';
const MONITORING_PAGES = {
REVIEW: 'review',
@@ -29,7 +32,7 @@ export default function Monitoring({
match,
}) {
const { params: { currentPage, recipientId, regionId } } = match;
-
+ const { setIsAppLoading } = useContext(AppLoadingContext);
const history = useHistory();
const [byReview, setByReview] = useState([]);
const [byCitation, setByCitation] = useState([]);
@@ -60,15 +63,18 @@ export default function Monitoring({
}
if (currentPage && ALLOWED_PAGE_SLUGS.includes(currentPage)) {
+ setIsAppLoading(true);
try {
fetchMonitoringData(currentPage);
} catch (error) {
// eslint-disable-next-line no-console
console.error('Error fetching monitoring data:', error);
history.push(`${ROUTES.SOMETHING_WENT_WRONG}/${error.status}`);
+ } finally {
+ setIsAppLoading(false);
}
}
- }, [currentPage, history, lookup, recipientId, regionId]);
+ }, [currentPage, history, lookup, recipientId, regionId, setIsAppLoading]);
return (
diff --git a/frontend/src/pages/RecipientRecord/pages/Monitoring/testHelpers/mockData.js b/frontend/src/pages/RecipientRecord/pages/Monitoring/testHelpers/mockData.js
index 9b5913d773..e9eee63a4e 100644
--- a/frontend/src/pages/RecipientRecord/pages/Monitoring/testHelpers/mockData.js
+++ b/frontend/src/pages/RecipientRecord/pages/Monitoring/testHelpers/mockData.js
@@ -57,7 +57,7 @@ const reviewData = [
{
citation: '1302.47(b)(5)(iv)',
status: 'Active',
- type: 'Deficiency',
+ findingType: 'Deficiency',
category: 'Inappropriate Release',
correctionDeadline: '07/25/2024',
objectives: [
@@ -96,7 +96,7 @@ const reviewData = [
{
citation: '1302.47(b)(5)(v)',
status: 'Active',
- type: 'Noncompliance',
+ findingType: 'Noncompliance',
category: 'Monitoring and Implementing Quality Health Services',
correctionDeadline: '09/18/2024',
objectives: [
@@ -112,7 +112,7 @@ const reviewData = [
{
citation: '1302.91(a)',
status: 'Active',
- type: 'Noncompliance',
+ findingType: 'Noncompliance',
category: 'Program Management and Quality Improvement',
correctionDeadline: '09/18/2024',
objectives: [
@@ -128,7 +128,7 @@ const reviewData = [
{
citation: '1302.12(m)',
status: 'Active',
- type: 'Noncompliance',
+ findingType: 'Noncompliance',
category: 'Program Management and Quality Improvement',
correctionDeadline: '09/18/2024',
objectives: [
diff --git a/src/models/monitoringFindingStandard.js b/src/models/monitoringFindingStandard.js
index da891ae82b..b0ae874a76 100644
--- a/src/models/monitoringFindingStandard.js
+++ b/src/models/monitoringFindingStandard.js
@@ -27,7 +27,7 @@ export default (sequelize, DataTypes) => {
models.MonitoringStandardLink,
{
foreignKey: 'standardId',
- as: 'statusLink',
+ as: 'standardLink',
},
);
diff --git a/src/models/monitoringStandard.js b/src/models/monitoringStandard.js
index 1552682382..1bdb17f633 100644
--- a/src/models/monitoringStandard.js
+++ b/src/models/monitoringStandard.js
@@ -17,7 +17,7 @@ export default (sequelize, DataTypes) => {
models.MonitoringStandard,
{
foreignKey: 'standardId',
- as: 'monitoringStandardes',
+ as: 'monitoringStandards',
},
);
@@ -25,7 +25,7 @@ export default (sequelize, DataTypes) => {
models.MonitoringStandardLink,
{
foreignKey: 'standardId',
- as: 'statusLink',
+ as: 'standardLink',
},
);
}
diff --git a/src/services/monitoring.ts b/src/services/monitoring.ts
index 20fcb14ec2..334fe2b7ac 100644
--- a/src/services/monitoring.ts
+++ b/src/services/monitoring.ts
@@ -6,8 +6,9 @@ import {
IMonitoringReview,
IMonitoringReviewGrantee,
IMonitoringResponse,
- ITTAByReviewsSequelizeQueryResponse,
ITTAByCitationResponse,
+ MonitoringStandard as MonitoringStandardType,
+ MonitoringReview as MonitoringReviewType,
} from './types/monitoring';
const {
@@ -24,6 +25,9 @@ const {
MonitoringFinding,
MonitoringFindingStatusLink,
MonitoringFindingStatus,
+ MonitoringFindingStandard,
+ MonitoringStandardLink,
+ MonitoringStandard,
} = db;
export async function grantNumbersByRecipientAndRegion(recipientId: number, regionId: number) {
@@ -66,6 +70,22 @@ export async function ttaByReviews(
model: MonitoringFindingLink,
as: 'monitoringFindingLink',
include: [
+ {
+ model: MonitoringFindingStandard,
+ as: 'monitoringFindingStandards',
+ include: [
+ {
+ model: MonitoringStandardLink,
+ as: 'standardLink',
+ include: [
+ {
+ model: MonitoringStandard,
+ as: 'monitoringStandards',
+ },
+ ],
+ },
+ ],
+ },
{
model: MonitoringFinding,
as: 'monitoringFindings',
@@ -89,7 +109,7 @@ export async function ttaByReviews(
],
},
],
- }) as ITTAByReviewsSequelizeQueryResponse[];
+ }) as MonitoringReviewType[];
const response = reviews.map((review) => {
const { monitoringReviewGrantees, monitoringFindingHistories } = review.monitoringReviewLink;
@@ -101,10 +121,17 @@ export async function ttaByReviews(
return;
}
+ let citation = '';
+ const [findingStandards] = history.monitoringFindingLink.monitoringFindingStandards;
+ if (findingStandards) {
+ const [standard] = findingStandards.standardLink.monitoringStandards;
+ citation = standard.citation;
+ }
+
history.monitoringFindingLink.monitoringFindings.forEach((finding) => {
const status = finding.statusLink.monitoringFindingStatuses[0].name;
findings.push({
- citation: '',
+ citation,
status,
findingType: finding.findingType,
correctionDeadline: moment(finding.correctionDeadLine).format('MM/DD/YYYY'),
@@ -128,159 +155,101 @@ export async function ttaByReviews(
});
return response;
-
- // return [
- // {
- // name: '241234FU',
- // reviewType: 'Follow-up',
- // reviewReceived: '01/01/2021',
- // outcome: 'Compliant',
- // lastTTADate: null,
- // specialists: [],
- // grants: [
- // '14CH123456',
- // '14HP141234',
- // ],
- // findings: [
- // {
- // citation: '1392.47(b)(5)(i)',
- // status: 'Corrected',
- // type: 'Noncompliance',
- // category: 'Monitoring and Implementing Quality Health Services',
- // correctionDeadline: '09/18/2024',
- // objectives: [],
- // },
- // {
- // citation: '1302.91(a)',
- // status: 'Corrected',
- // type: 'Noncompliance',
- // category: 'Program Management and Quality Improvement',
- // correctionDeadline: '09/18/2024',
- // objectives: [],
- // },
- // {
- // citation: '1302.12(m)',
- // status: 'Corrected',
- // type: 'Noncompliance',
- // category: 'Program Management and Quality Improvement',
- // correctionDeadline: '09/18/2024',
- // objectives: [],
- // },
- // ],
- // },
- // {
- // name: '241234RAN',
- // reviewType: 'RAN',
- // reviewReceived: '06/21/2024',
- // outcome: 'Deficiency',
- // lastTTADate: '07/12/2024',
- // grants: [
- // '14CH123456',
- // '14HP141234',
- // ],
- // specialists: [
- // {
- // name: 'Specialist 1',
- // roles: ['GS'],
- // },
- // ],
- // findings: [
- // {
- // citation: '1302.47(b)(5)(iv)',
- // status: 'Active',
- // type: 'Deficiency',
- // category: 'Inappropriate Release',
- // correctionDeadline: '07/25/2024',
- // objectives: [
- // {
- // title: 'The TTA Specialist will assist the recipient with developing a QIP and/or corrective action plan to address the finding with correction strategies, timeframes, and evidence of the correction.',
- // activityReportIds: ['14AR29888'],
- // endDate: '07/12/2024',
- // topics: ['Communication', 'Quality Improvement Plan/QIP', 'Safety Practices', 'Transportation'],
- // status: 'In Progress',
- // },
- // ],
- // },
- // ],
- // },
- // {
- // name: '241234F2',
- // reviewType: 'FA-2',
- // reviewReceived: '05/20/2024',
- // outcome: 'Noncompliant',
- // lastTTADate: '03/27/2024',
- // grants: [
- // '14CH123456',
- // '14HP141234',
- // ],
- // specialists: [
- // {
- // name: 'Specialist 1',
- // roles: ['GS'],
- // },
- // {
- // name: 'Specialist 1',
- // roles: ['ECS'],
- // },
- // ],
- // findings: [
- // {
- // citation: '1302.47(b)(5)(v)',
- // status: 'Active',
- // type: 'Noncompliance',
- // category: 'Monitoring and Implementing Quality Health Services',
- // correctionDeadline: '09/18/2024',
- // objectives: [
- // {
- // title: 'The TTA Specialist will support the recipient by developing a plan to ensure all staff implement health and safety practices to always keep children safe',
- // activityReportIds: ['14AR12345'],
- // endDate: '06/24/2024',
- // topics: ['Human Resources', 'Ongoing Monitoring Management System', 'Safety Practices', 'Training and Profressional Development'],
- // status: 'Complete',
- // },
- // ],
- // },
- // {
- // citation: '1302.91(a)',
- // status: 'Active',
- // type: 'Noncompliance',
- // category: 'Program Management and Quality Improvement',
- // correctionDeadline: '09/18/2024',
- // objectives: [
- // {
- // title: 'The TTA Specialist will support the recipient by developing a plan to ensure all staff implement health and safety practices to always keep children safe',
- // activityReportIds: ['14AR12345'],
- // endDate: '06/24/2024',
- // topics: ['Human Resources', 'Ongoing Monitoring Management System', 'Safety Practices', 'Training and Profressional Development'],
- // status: 'Complete',
- // },
- // ],
- // },
- // {
- // citation: '1302.12(m)',
- // status: 'Active',
- // type: 'Noncompliance',
- // category: 'Program Management and Quality Improvement',
- // correctionDeadline: '09/18/2024',
- // objectives: [
- // {
- // title: 'The TTA Specialist will support the recipient by developing a plan to ensure all staff implement health and safety practices to always keep children safe',
- // activityReportIds: ['14AR12345'],
- // endDate: '06/24/2024',
- // topics: ['Human Resources', 'Ongoing Monitoring Management System', 'Safety Practices', 'Training and Profressional Development'],
- // status: 'Complete',
- // },
- // ],
- // },
- // ],
- // },
- // ];
}
export async function ttaByCitations(
- _recipientId: number,
- _regionId: number,
+ recipientId: number,
+ regionId: number,
): Promise {
+ const grantNumbers = await grantNumbersByRecipientAndRegion(recipientId, regionId) as string[];
+
+ const citations = await MonitoringStandard.findAll({
+ include: [
+ {
+ model: MonitoringStandardLink,
+ as: 'standardLink',
+ required: true,
+ include: [
+ {
+ model: MonitoringFindingStandard,
+ as: 'monitoringFindingStandards',
+ required: true,
+ include: [
+ {
+ model: MonitoringFindingLink,
+ as: 'findingLink',
+ required: true,
+ include: [
+ {
+ model: MonitoringFindingHistory,
+ as: 'monitoringFindingHistories',
+ required: true,
+ include: [
+ {
+ model: MonitoringReviewLink,
+ as: 'monitoringReviewLink',
+ required: true,
+ include: [
+ {
+ model: MonitoringReview,
+ as: 'monitoringReviews',
+ required: true,
+ },
+ {
+ model: MonitoringReviewGrantee,
+ as: 'monitoringReviewGrantees',
+ required: true,
+ where: {
+ grantNumber: grantNumbers,
+ },
+ },
+ ],
+ },
+ ],
+ },
+ {
+ model: MonitoringFinding,
+ as: 'monitoringFindings',
+ required: true,
+ include: [
+ {
+ model: MonitoringFindingStatusLink,
+ as: 'statusLink',
+ required: true,
+ include: [
+ {
+ model: MonitoringFindingStatus,
+ as: 'monitoringFindingStatuses',
+ required: true,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ }) as MonitoringStandardType[];
+
+ return citations.map((citation) => {
+ // this is a comment
+ console.log(citation);
+
+ return {
+ citationNumber: citation.citation,
+ status: '',
+ findingType: '',
+ category: '',
+ grantNumbers,
+ lastTTADate: null,
+ reviews: [],
+ };
+ });
+
return [
{
citationNumber: '1302.12(m)',
diff --git a/src/services/types/monitoring.ts b/src/services/types/monitoring.ts
index 0148b134d2..81f96b70f7 100644
--- a/src/services/types/monitoring.ts
+++ b/src/services/types/monitoring.ts
@@ -49,22 +49,22 @@ interface ITTAByCitationReview {
}
interface ITTAByReviewFinding {
- citation: string; // ??
- status: string; // check
- findingType: string; // check
- category: string; // check
- correctionDeadline: string; // check
+ citation: string;
+ status: string;
+ findingType: string;
+ category: string;
+ correctionDeadline: string;
objectives: ITTAByReviewObjective[];
}
interface ITTAByReviewResponse {
- id: number; // check
- name: string; // check
- reviewType: string; // check
+ id: number;
+ name: string;
+ reviewType: string;
reviewReceived: string;
findings: ITTAByReviewFinding[];
- grants: string[]; // check
- outcome: string; // check
+ grants: string[];
+ outcome: string;
lastTTADate: string | null; // need ars
specialists: {
name: string;
@@ -82,39 +82,175 @@ interface ITTAByCitationResponse {
reviews: ITTAByCitationReview[];
}
-interface ITTAByReviewsSequelizeQueryResponse {
+export interface MonitoringStandard {
+ id: number;
+ standardId: number;
+ contentId: string;
+ citation: string;
+ text: string;
+ guidance: string;
+ citable: number;
+ hash: string;
+ sourceCreatedAt: Date;
+ sourceUpdatedAt: Date;
+ sourceDeletedAt: null;
+ createdAt: Date;
+ updatedAt: Date;
+ deletedAt: null;
+ standardLink: StandardLink;
+}
+
+export interface StandardLink {
+ id: number;
+ standardId: number;
+ createdAt: Date;
+ updatedAt: Date;
+ deletedAt: null;
+ monitoringFindingStandards: MonitoringFindingStandard[];
+ monitoringStandards: MonitoringStandard[];
+}
+
+export interface MonitoringFindingStandard {
+ id: number;
+ findingId: string;
+ standardId: number;
+ sourceCreatedAt: Date;
+ sourceUpdatedAt: Date;
+ sourceDeletedAt: null;
+ createdAt: Date;
+ updatedAt: Date;
+ deletedAt: null;
+ findingLink: FindingLink;
+ standardLink: StandardLink;
+}
+
+export interface FindingLink {
+ id: number;
+ findingId: string;
+ createdAt: Date;
+ updatedAt: Date;
+ deletedAt: null;
+ monitoringFindingHistories: MonitoringFindingHistory[];
+ monitoringFindings: MonitoringFinding[];
+}
+
+export interface MonitoringFindingHistory {
+ id: number;
+ reviewId: string;
+ findingHistoryId: string;
+ findingId: string;
+ statusId: number;
+ narrative: string;
+ ordinal: number;
+ determination: null | string;
+ hash: string;
+ sourceCreatedAt: Date;
+ sourceUpdatedAt: Date;
+ sourceDeletedAt: null;
+ createdAt: Date;
+ updatedAt: Date;
+ deletedAt: null;
+ monitoringReviewLink: MonitoringReviewLink;
+ monitoringFindingLink: MonitoringFindingLink;
+}
+
+export interface MonitoringReviewLink {
+ id: number;
+ reviewId: string;
+ createdAt: Date;
+ updatedAt: Date;
+ deletedAt: null;
+ monitoringReviews: MonitoringReview[];
+ monitoringReviewGrantees: MonitoringReviewGrantee[];
+ monitoringFindingHistories?: MonitoringFindingHistory[];
+}
+
+export interface MonitoringReviewGrantee {
+ id: number;
+ reviewId: string;
+ granteeId: string;
+ createTime: Date;
+ updateTime: Date;
+ updateBy: string;
+ grantNumber: string;
+ sourceCreatedAt: Date;
+ sourceUpdatedAt: Date;
+ sourceDeletedAt: Date;
+ createdAt: Date;
+ updatedAt: Date;
+ deletedAt: null;
+}
+
+export interface MonitoringFinding {
+ id: number;
+ findingId: string;
+ statusId: number;
+ findingType: string;
+ source: string;
+ correctionDeadLine: Date;
+ reportedDate: Date;
+ closedDate: Date;
+ hash: string;
+ sourceCreatedAt: Date;
+ sourceUpdatedAt: Date;
+ sourceDeletedAt: null;
+ createdAt: Date;
+ updatedAt: Date;
+ deletedAt: null;
+ statusLink: StatusLink;
+}
+
+export interface StatusLink {
+ id: number;
+ statusId: number;
+ createdAt: Date;
+ updatedAt: Date;
+ deletedAt: null;
+ monitoringFindingStatuses: MonitoringFindingStatus[];
+}
+
+export interface MonitoringFindingStatus {
+ id: number;
+ statusId: number;
+ name: string;
+ sourceCreatedAt: Date;
+ sourceUpdatedAt: Date;
+ sourceDeletedAt: null;
+ createdAt: Date;
+ updatedAt: Date;
+ deletedAt: null;
+}
+
+export interface MonitoringReview {
id: number;
reviewId: string;
- reportDeliveryDate: string;
contentId: string;
+ statusId: number;
name: string;
- outcome: string;
+ startDate: Date;
+ endDate: Date;
reviewType: string;
- monitoringReviewLink: {
- monitoringFindingHistories: {
- narrative: string;
- ordinal: number;
- determination: string;
- monitoringFindingLink?: {
- monitoringFindings: {
- citationNumber: string;
- findingType: string;
- source: string;
- correctionDeadLine: string;
- statusLink: {
- monitoringFindingStatuses: {
- name: string;
- }[];
- };
- }[];
- };
- }[];
- monitoringReviewGrantees: {
- grantNumber: string;
- }[];
- };
+ reportDeliveryDate: Date;
+ reportAttachmentId: string;
+ outcome: string;
+ hash: string;
+ sourceCreatedAt: Date;
+ sourceUpdatedAt: Date;
+ sourceDeletedAt: null;
+ createdAt: Date;
+ updatedAt: Date;
+ deletedAt: null;
+ monitoringReviewLink: MonitoringReviewLink;
+}
- toJSON: () => Omit;
+export interface MonitoringFindingLink {
+ id: number;
+ findingId: string;
+ createdAt: Date;
+ updatedAt: Date;
+ deletedAt: null;
+ monitoringFindingStandards: MonitoringFindingStandard[];
+ monitoringFindings: MonitoringFinding[];
}
export {
@@ -125,6 +261,5 @@ export {
ITTAByCitationReview,
ITTAByReviewFinding,
ITTAByReviewResponse,
- ITTAByReviewsSequelizeQueryResponse,
ITTAByCitationResponse,
};
From 8bd9b52bf68b3e4c6bc72c133580d9148a5d2484 Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Thu, 5 Dec 2024 10:31:20 -0500
Subject: [PATCH 050/198] Extract source
---
src/services/monitoring.ts | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/src/services/monitoring.ts b/src/services/monitoring.ts
index 334fe2b7ac..a3b0acf5a0 100644
--- a/src/services/monitoring.ts
+++ b/src/services/monitoring.ts
@@ -236,14 +236,16 @@ export async function ttaByCitations(
}) as MonitoringStandardType[];
return citations.map((citation) => {
- // this is a comment
- console.log(citation);
+ const [findingStandard] = citation.standardLink.monitoringFindingStandards;
+ const { findingLink } = findingStandard;
+ const { monitoringFindings } = findingLink;
+ const [finding] = monitoringFindings;
return {
citationNumber: citation.citation,
status: '',
findingType: '',
- category: '',
+ category: finding.source,
grantNumbers,
lastTTADate: null,
reviews: [],
From 20cf11f30d9499743deaecae18b8f1cfd69db14c Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Thu, 5 Dec 2024 11:05:04 -0500
Subject: [PATCH 051/198] see if this fixes e2e
---
src/services/reportCache.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/services/reportCache.js b/src/services/reportCache.js
index eb5f3a3078..0034492cac 100644
--- a/src/services/reportCache.js
+++ b/src/services/reportCache.js
@@ -157,7 +157,7 @@ export const cacheCitations = async (objectiveId, activityReportObjectiveId, cit
// Create citations to save.
let newCitations = [];
- if (citations.length > 0) {
+ if (citations && citations.length > 0) {
newCitations = citations.map((citation) => (
{
...citation,
From 50d89b16e39ebf2f4b39f5940e7913fb3df59b11 Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Thu, 5 Dec 2024 11:05:09 -0500
Subject: [PATCH 052/198] Finish citation query
---
src/services/monitoring.ts | 249 +++++++++----------------------------
1 file changed, 62 insertions(+), 187 deletions(-)
diff --git a/src/services/monitoring.ts b/src/services/monitoring.ts
index a3b0acf5a0..d11970a250 100644
--- a/src/services/monitoring.ts
+++ b/src/services/monitoring.ts
@@ -1,5 +1,6 @@
/* eslint-disable max-len */
import moment from 'moment';
+import { uniq } from 'lodash';
import db from '../models';
import {
ITTAByReviewResponse,
@@ -185,6 +186,32 @@ export async function ttaByCitations(
as: 'monitoringFindingHistories',
required: true,
include: [
+ {
+ model: MonitoringFindingLink,
+ as: 'monitoringFindingLink',
+ required: true,
+ include: [
+ {
+ model: MonitoringFinding,
+ as: 'monitoringFindings',
+ required: true,
+ include: [
+ {
+ model: MonitoringFindingStatusLink,
+ as: 'statusLink',
+ required: true,
+ include: [
+ {
+ model: MonitoringFindingStatus,
+ as: 'monitoringFindingStatuses',
+ required: true,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
{
model: MonitoringReviewLink,
as: 'monitoringReviewLink',
@@ -239,194 +266,49 @@ export async function ttaByCitations(
const [findingStandard] = citation.standardLink.monitoringFindingStandards;
const { findingLink } = findingStandard;
const { monitoringFindings } = findingLink;
+
+ const grants = [];
+ const reviews = [];
+
const [finding] = monitoringFindings;
+ const [status] = finding.statusLink.monitoringFindingStatuses;
+
+ findingLink.monitoringFindingHistories.forEach((history) => {
+ const { monitoringReviewLink, monitoringFindingLink } = history;
+ const { monitoringReviews } = monitoringReviewLink;
+
+ const { monitoringFindings: historicalFindings } = monitoringFindingLink;
+ const [reviewFinding] = historicalFindings;
+ const [findingStatus] = reviewFinding.statusLink.monitoringFindingStatuses;
+
+ monitoringReviews.forEach((review) => {
+ const { monitoringReviewGrantees } = monitoringReviewLink;
+ const gr = monitoringReviewGrantees.map((grantee) => grantee.grantNumber);
+
+ grants.push(gr);
+
+ reviews.push({
+ name: review.name,
+ reviewType: review.reviewType,
+ reviewReceived: moment(review.reportDeliveryDate).format('MM/DD/YYYY'),
+ outcome: review.outcome,
+ findingStatus: findingStatus.name,
+ specialists: [],
+ objectives: [],
+ });
+ });
+ });
return {
citationNumber: citation.citation,
- status: '',
- findingType: '',
+ status: status.name,
+ findingType: finding.findingType,
category: finding.source,
- grantNumbers,
+ grantNumbers: uniq(grants.flat()),
lastTTADate: null,
- reviews: [],
+ reviews,
};
});
-
- return [
- {
- citationNumber: '1302.12(m)',
- status: 'Corrected',
- findingType: 'Noncompliance',
- category: 'Program Management and Quality Improvement',
- grantNumbers: [
- '14CH123456',
- '14HP141234',
- ],
- lastTTADate: '06/24/2024',
- reviews: [
- {
- name: '241234FU',
- reviewType: 'Follow-up',
- reviewReceived: '10/17/2024',
- outcome: 'Compliant',
- findingStatus: 'Corrected',
- specialists: [],
- objectives: [],
- },
- {
- name: '241234F2',
- reviewType: 'FA-2',
- reviewReceived: '05/20/2024',
- outcome: 'Noncompliant',
- findingStatus: 'Active',
- specialists: [
- {
- name: 'Specialist 1',
- roles: ['GS'],
- },
- {
- name: 'Specialist 1',
- roles: ['ECS'],
- },
- ],
- objectives: [
- {
- title: 'The TTA Specialist will support the recipient by developing a plan to ensure all staff implement health and safety practices to always keep children safe',
- activityReportIds: ['14AR12345'],
- endDate: '06/24/2024',
- topics: ['Human Resources', 'Ongoing Monitoring Management System', 'Safety Practices', 'Training and Profressional Development'],
- status: 'Complete',
- },
- ],
- },
- ],
- },
- {
- citationNumber: '1302.47(b)(5)(i)',
- findingType: 'Noncompliance',
- status: 'Corrected',
- category: 'Monitoring and Implementing Quality Health Services',
- grantNumbers: [
- '14CH123456',
- '14HP141234',
- ],
- lastTTADate: '05/22/2022',
- reviews: [
- {
- name: '241234FU',
- reviewType: 'Follow-up',
- reviewReceived: '10/17/2024',
- outcome: 'Noncompliant',
- findingStatus: 'Corrected',
- specialists: [],
- objectives: [],
- },
- {
- name: '241234F2',
- reviewType: 'FA-2',
- reviewReceived: '05/22/2024',
- outcome: 'Noncompliant',
- findingStatus: 'Active',
- specialists: [
- {
- name: 'Specialist 1',
- roles: ['GS'],
- },
- {
- name: 'Specialist 1',
- roles: ['ECS'],
- },
- ],
- objectives: [
- {
- title: 'The TTA Specialist will support the recipient by developing a plan to ensure all staff implement health and safety practices to always keep children safe',
- activityReportIds: ['14AR12345'],
- endDate: '06/24/2024',
- topics: ['Human Resources', 'Ongoing Monitoring Management System', 'Safety Practices', 'Training and Profressional Development'],
- status: 'Complete',
- },
- ],
- },
- ],
- },
- {
- citationNumber: '1302.47(b)(5)(iv)',
- status: 'Active',
- findingType: 'Deficiency',
- category: 'Inappropriate Release',
- grantNumbers: ['14CH123456'],
- lastTTADate: '07/25/2024',
- reviews: [
- {
- name: '241234RAN',
- reviewType: 'RAN',
- reviewReceived: '06/21/2024',
- outcome: 'Deficient',
- findingStatus: 'Active',
- specialists: [
- {
- name: 'Specialist 1',
- roles: ['GS'],
- },
- ],
- objectives: [
- {
- title: 'The TTA Specialist will assist the recipient with developing a QIP and/or corrective action plan to address the finding with correction strategies, timeframes, and evidence of the correction.',
- activityReportIds: ['14AR29888'],
- endDate: '07/12/2024',
- topics: ['Communication', 'Quality Improvement Plan/QIP', 'Safety Practices', 'Transportation'],
- status: 'In Progress',
- },
- ],
- },
- ],
- },
- {
- citationNumber: '1302.91(a)',
- status: 'Corrected',
- findingType: 'Noncompliance',
- category: 'Program Management and Quality Improvement',
- grantNumbers: ['14CH123456', '14HP141234'],
- lastTTADate: '06/24/2024',
- reviews: [
- {
- name: '241234FU',
- reviewType: 'Follow-up',
- reviewReceived: '10/17/2024',
- outcome: 'Compliant',
- findingStatus: 'Corrected',
- specialists: [],
- objectives: [],
- },
- {
- name: '241234F2',
- reviewType: 'FA-2',
- reviewReceived: '05/22/2024',
- outcome: 'Noncompliant',
- findingStatus: 'Active',
- specialists: [
- {
- name: 'Specialist 1',
- roles: ['GS'],
- },
- {
- name: 'Specialist 1',
- roles: ['ECS'],
- },
- ],
- objectives: [
- {
- title: 'The TTA Specialist will support the recipient by developing a plan to ensure all staff implement health and safety practices to always keep children safe',
- activityReportIds: ['14AR12345'],
- endDate: '06/24/2024',
- topics: ['Human Resources', 'Ongoing Monitoring Management System', 'Safety Practices', 'Training and Profressional Development'],
- status: 'Complete',
- },
- ],
- },
- ],
- },
- ];
}
export async function monitoringData({
@@ -536,13 +418,6 @@ export async function monitoringData({
return b;
}, monitoringReviews[0]);
- // from the most recent review, get the status via the statusLink
- const { monitoringReviewStatuses } = monitoringReview.statusLink;
-
- // I am presuming there can only be one status linked to a review
- // as that was the structure before tables were refactored
- const [status] = monitoringReviewStatuses;
-
return {
recipientId: grant.recipientId,
regionId: grant.regionId,
From 1b41dcd0acdb0f3ae5fb2784a658a4dd01bf2562 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Thu, 5 Dec 2024 11:27:26 -0500
Subject: [PATCH 053/198] fix e2e for non montoring ar
---
.../src/pages/ActivityReport/Pages/components/Objective.js | 3 ++-
.../src/pages/ActivityReport/Pages/components/constants.js | 1 +
.../src/pages/ActivityReport/Pages/components/goalValidator.js | 1 +
3 files changed, 4 insertions(+), 1 deletion(-)
diff --git a/frontend/src/pages/ActivityReport/Pages/components/Objective.js b/frontend/src/pages/ActivityReport/Pages/components/Objective.js
index b3c891d74b..a79f849a27 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/Objective.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/Objective.js
@@ -116,7 +116,8 @@ export default function Objective({
notEmpty: (value) => (value && value.length) || OBJECTIVE_CITATIONS,
},
},
- defaultValue: objective.citations,
+ // If citations are not available, set citations to null
+ defaultValue: citationOptions && citationOptions.length ? objective.citations : null,
});
const {
diff --git a/frontend/src/pages/ActivityReport/Pages/components/constants.js b/frontend/src/pages/ActivityReport/Pages/components/constants.js
index 40a04056c5..ec17691362 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/constants.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/constants.js
@@ -17,6 +17,7 @@ export const NEW_OBJECTIVE = () => ({
roles: [],
status: 'Not Started',
isNew: true,
+ citations: null, // Only required for monitoring goals.
closeSuspendReason: null,
closeSuspendContext: null,
objectiveCreatedHere: true,
diff --git a/frontend/src/pages/ActivityReport/Pages/components/goalValidator.js b/frontend/src/pages/ActivityReport/Pages/components/goalValidator.js
index 6fde9d0da4..f9f06650b7 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/goalValidator.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/goalValidator.js
@@ -60,6 +60,7 @@ export const unfinishedObjectives = (
incomplete = true;
}
+ // We only validate citations if they exist (they are not always required).
if (objective.citations && (!objective.citations || !objective.citations.length)) {
setError(`${fieldArrayName}[${index}].citations`, { message: OBJECTIVE_CITATIONS });
incomplete = true;
From aa21b80ad50d707e6f90c079f64e1ced921395df Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Thu, 5 Dec 2024 12:02:43 -0500
Subject: [PATCH 054/198] Improve test cleanup
---
src/services/monitoring.test.js | 112 ++++++++++++++++++++++++++++++--
1 file changed, 105 insertions(+), 7 deletions(-)
diff --git a/src/services/monitoring.test.js b/src/services/monitoring.test.js
index 27f6dd18b0..94a972dcf5 100644
--- a/src/services/monitoring.test.js
+++ b/src/services/monitoring.test.js
@@ -1,5 +1,10 @@
import sequelize from 'sequelize';
-import { classScore, monitoringData } from './monitoring';
+import {
+ classScore,
+ monitoringData,
+ ttaByCitations,
+ ttaByReviews,
+} from './monitoring';
import db from '../models';
const {
@@ -91,12 +96,67 @@ async function createMonitoringData(grantNumber) {
}
async function destroyMonitoringData(grantNumber) {
- await MonitoringReviewGrantee.destroy({ where: { grantNumber }, force: true });
- await MonitoringClassSummary.destroy({ where: { grantNumber }, force: true });
- await MonitoringReview.destroy({ where: { reviewId: 'C48EAA67-90B9-4125-9DB5-0011D6D7C808' }, force: true });
- await MonitoringReviewLink.destroy({ where: { reviewId: 'C48EAA67-90B9-4125-9DB5-0011D6D7C808' }, force: true });
- await MonitoringReviewStatus.destroy({ where: { statusId: 6006 }, force: true });
- await MonitoringReviewStatusLink.destroy({ where: { statusId: 6006 }, force: true });
+ const grantees = await MonitoringReviewGrantee.findAll({
+ attribtes: ['id'],
+ where: { grantNumber },
+ include: [{
+ model: MonitoringReview,
+ attributes: ['id'],
+ include: [{
+ model: MonitoringReviewLink,
+ attributes: ['id'],
+ }],
+ }],
+ });
+
+ const granteeIds = grantees.map((grantee) => grantee.id);
+ const reviewIds = grantees.map((grantee) => grantee.monitoringReview.id);
+ const reviewLinkIds = grantees.map((grantee) => grantee.monitoringReview.monitoringReviewLink.id);
+
+ await MonitoringReviewGrantee.destroy({
+ where: { id: granteeIds },
+ force: true,
+ individualHooks: true,
+ });
+
+ await MonitoringReview.destroy({
+ where: { id: reviewIds },
+ force: true,
+ individualHooks: true,
+ });
+
+ await MonitoringReviewLink.destroy({
+ where: { id: reviewLinkIds },
+ force: true,
+ individualHooks: true,
+ });
+
+ await MonitoringReviewGrantee.destroy(
+ { where: { grantNumber }, force: true, individualHooks: true },
+ );
+ await MonitoringClassSummary.destroy({
+ where: { grantNumber }, force: true, individualHooks: true,
+ });
+ await MonitoringReview.destroy({
+ where: { reviewId: 'C48EAA67-90B9-4125-9DB5-0011D6D7C808' },
+ force: true,
+ individualHooks: true,
+ });
+ await MonitoringReviewLink.destroy({
+ where: { reviewId: 'C48EAA67-90B9-4125-9DB5-0011D6D7C808' },
+ force: true,
+ individualHooks: true,
+ });
+ await MonitoringReviewStatus.destroy({
+ where: { statusId: 6006 },
+ force: true,
+ individualHooks: true,
+ });
+ await MonitoringReviewStatusLink.destroy({
+ where: { statusId: 6006 },
+ force: true,
+ individualHooks: true,
+ });
}
describe('monitoring services', () => {
@@ -121,6 +181,44 @@ describe('monitoring services', () => {
await sequelize.close();
});
+ describe('ttaByCitations', () => {
+ beforeAll(async () => {
+ await createMonitoringData(GRANT_NUMBER);
+ });
+ afterAll(async () => {
+ await destroyMonitoringData(GRANT_NUMBER);
+ await GrantNumberLink.destroy({ where: { grantNumber: GRANT_NUMBER }, force: true });
+ });
+
+ it('fetches TTA, ordered by Citations', async () => {
+ const data = await ttaByCitations(
+ RECIPIENT_ID,
+ REGION_ID,
+ );
+
+ expect(data).toBe([]);
+ });
+ });
+
+ describe('ttaByReview', () => {
+ beforeAll(async () => {
+ await createMonitoringData(GRANT_NUMBER);
+ });
+ afterAll(async () => {
+ await destroyMonitoringData(GRANT_NUMBER);
+ await GrantNumberLink.destroy({ where: { grantNumber: GRANT_NUMBER }, force: true });
+ });
+
+ it('fetches TTA, ordered by review', async () => {
+ const data = await ttaByReviews(
+ RECIPIENT_ID,
+ REGION_ID,
+ );
+
+ expect(data).toBe([]);
+ });
+ });
+
describe('classScore', () => {
beforeAll(async () => {
await createMonitoringData(GRANT_NUMBER);
From 09187b2faf52e14f8ee3f8ecd6091489b17d9b0b Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Thu, 5 Dec 2024 12:46:05 -0500
Subject: [PATCH 055/198] fixes for both regular and citation path
---
.../Pages/components/Objective.js | 2 +-
.../Pages/components/Objectives.js | 6 ++--
.../Pages/components/constants.js | 4 +--
.../Pages/components/goalValidator.js | 2 +-
src/goalServices/reduceGoals.ts | 29 +++++++++++--------
src/services/citations.js | 2 +-
6 files changed, 25 insertions(+), 20 deletions(-)
diff --git a/frontend/src/pages/ActivityReport/Pages/components/Objective.js b/frontend/src/pages/ActivityReport/Pages/components/Objective.js
index a79f849a27..f59d318929 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/Objective.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/Objective.js
@@ -117,7 +117,7 @@ export default function Objective({
},
},
// If citations are not available, set citations to null
- defaultValue: citationOptions && citationOptions.length ? objective.citations : null,
+ defaultValue: objective.citations,
});
const {
diff --git a/frontend/src/pages/ActivityReport/Pages/components/Objectives.js b/frontend/src/pages/ActivityReport/Pages/components/Objectives.js
index 9d64ff6d3c..7bafa5434f 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/Objectives.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/Objectives.js
@@ -15,7 +15,7 @@ export default function Objectives({
rawCitations,
}) {
const { errors, getValues, setValue } = useFormContext();
-
+ const isMonitoring = citationOptions && citationOptions.length > 0;
const fieldArrayName = 'goalForEditing.objectives';
const objectivesForGoal = getValues(fieldArrayName);
const defaultValues = objectivesForGoal || [];
@@ -41,7 +41,7 @@ export default function Objectives({
);
const onAddNew = () => {
- append({ ...NEW_OBJECTIVE() });
+ append({ ...NEW_OBJECTIVE(isMonitoring) });
};
const setUpdatedUsedObjectiveIds = () => {
@@ -81,7 +81,7 @@ export default function Objectives({
};
const options = [
- NEW_OBJECTIVE(),
+ NEW_OBJECTIVE(isMonitoring),
// filter out used objectives and return them in them in a format that react-select understands
...objectiveOptions.filter((objective) => !usedObjectiveIds.includes(objective.value)).map(
(objective) => ({
diff --git a/frontend/src/pages/ActivityReport/Pages/components/constants.js b/frontend/src/pages/ActivityReport/Pages/components/constants.js
index ec17691362..9124b15be3 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/constants.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/constants.js
@@ -5,7 +5,7 @@ import { v4 as uuidv4 } from 'uuid';
export const NO_ERROR = <>>;
export const ERROR_FORMAT = (message) => {message} ;
-export const NEW_OBJECTIVE = () => ({
+export const NEW_OBJECTIVE = (isMonitoring = false) => ({
value: uuidv4(),
label: 'Create a new objective',
title: '',
@@ -17,7 +17,7 @@ export const NEW_OBJECTIVE = () => ({
roles: [],
status: 'Not Started',
isNew: true,
- citations: null, // Only required for monitoring goals.
+ citations: isMonitoring ? [] : null, // Only required for monitoring goals.
closeSuspendReason: null,
closeSuspendContext: null,
objectiveCreatedHere: true,
diff --git a/frontend/src/pages/ActivityReport/Pages/components/goalValidator.js b/frontend/src/pages/ActivityReport/Pages/components/goalValidator.js
index f9f06650b7..843ce2995a 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/goalValidator.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/goalValidator.js
@@ -61,7 +61,7 @@ export const unfinishedObjectives = (
}
// We only validate citations if they exist (they are not always required).
- if (objective.citations && (!objective.citations || !objective.citations.length)) {
+ if (objective.citations && !objective.citations.length) {
setError(`${fieldArrayName}[${index}].citations`, { message: OBJECTIVE_CITATIONS });
incomplete = true;
}
diff --git a/src/goalServices/reduceGoals.ts b/src/goalServices/reduceGoals.ts
index c69efb3931..ec613998ce 100644
--- a/src/goalServices/reduceGoals.ts
+++ b/src/goalServices/reduceGoals.ts
@@ -178,17 +178,19 @@ export function reduceObjectivesForActivityReport(
'course',
exists,
);
-
- exists.citations = uniq(objective.activityReportObjectives
+ // Citations should return null if they are not applicable.
+ exists.citations = objective.activityReportObjectives
&& objective.activityReportObjectives.length > 0
- ? objective.activityReportObjectives[0].activityReportObjectiveCitations.map(
+ && objective.activityReportObjectives[0].activityReportObjectiveCitations
+ && objective.activityReportObjectives[0].activityReportObjectiveCitations.length > 0
+ ? uniq(objective.activityReportObjectives[0].activityReportObjectiveCitations.map(
(c) => ({
...c.dataValues,
id: c.monitoringReferences[0].standardId,
name: `${c.monitoringReferences[0].acro} - ${c.citation} - ${c.monitoringReferences[0].findingType123}`,
}),
- )
- : []);
+ ))
+ : null;
exists.files = uniqBy([
...exists.files,
@@ -265,19 +267,22 @@ export function reduceObjectivesForActivityReport(
'activityReportObjectiveCourses',
'course',
),
- citations: uniq(
- objective.activityReportObjectives
- && objective.activityReportObjectives.length > 0
- ? objective.activityReportObjectives[0].activityReportObjectiveCitations.map(
+ // Citations should return null if they are not applicable.
+ citations: objective.activityReportObjectives
+ && objective.activityReportObjectives.length > 0
+ && objective.activityReportObjectives[0].activityReportObjectiveCitations
+ && objective.activityReportObjectives[0].activityReportObjectiveCitations.length > 0
+ ? uniq(
+ objective.activityReportObjectives[0].activityReportObjectiveCitations.map(
(c) => (
{
...c.dataValues,
id: c.monitoringReferences[0].standardId,
name: `${c.monitoringReferences[0].acro} - ${c.citation} - ${c.monitoringReferences[0].findingType}456`,
}),
- )
- : [],
- ),
+ ),
+ )
+ : null,
}];
}, currentObjectives);
diff --git a/src/services/citations.js b/src/services/citations.js
index 6b88fe45fc..f0ee0a845c 100644
--- a/src/services/citations.js
+++ b/src/services/citations.js
@@ -3,7 +3,7 @@
import { sequelize } from '../models';
// TODO: Update this to the day we deploy to PROD.
-const cutOffStartDate = new Date().toISOString().split('T')[0];
+const cutOffStartDate = '2021-10-01';
/*
The purpose of this function is to get citations by grant id.
We then need to format the response for how it needs to be
From 7777e048a39566ff18fdfadf1ba1820219a700b2 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Thu, 5 Dec 2024 14:15:13 -0500
Subject: [PATCH 056/198] fix fe
---
frontend/src/pages/ActivityReport/__tests__/index.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/frontend/src/pages/ActivityReport/__tests__/index.js b/frontend/src/pages/ActivityReport/__tests__/index.js
index 74a6234f7f..370d57a1e6 100644
--- a/frontend/src/pages/ActivityReport/__tests__/index.js
+++ b/frontend/src/pages/ActivityReport/__tests__/index.js
@@ -752,7 +752,7 @@ describe('ActivityReport', () => {
});
fetchMock.get('/api/topic', [{ id: 64, name: 'Communication' }]);
- fetchMock.get('/api/activity-reports/goals?grantIds=10431&reportStartDate=2012-04-20', [{
+ fetchMock.get('/api/activity-reports/goals?grantIds=10431&reportStartDate=2012-05-20', [{
endDate: null,
grantIds: [10431],
goalIds: [37502],
@@ -955,7 +955,7 @@ describe('ActivityReport', () => {
it('you can add a goal and objective and add a file after saving', async () => {
const data = formData();
fetchMock.get('/api/topic', [{ id: 64, name: 'Communication' }]);
- fetchMock.get('/api/activity-reports/goals?grantIds=12539', []);
+ fetchMock.get('/api/activity-reports/goals?grantIds=12539&reportStartDate=2012-05-20', []);
fetchMock.get('/api/goal-templates?grantIds=12539', []);
fetchMock.put('/api/activity-reports/1/goals/edit?goalIds=37504', {});
fetchMock.get('/api/activity-reports/1', {
From b49f0ea1518c6fef18f1932e2bd8d48e2c3a1f12 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Thu, 5 Dec 2024 14:41:04 -0500
Subject: [PATCH 057/198] put back date oops
---
.../pages/ActivityReport/Pages/components/Objective.js | 7 +++++++
.../Pages/components/__tests__/Objective.js | 8 ++++++++
src/services/citations.js | 2 +-
3 files changed, 16 insertions(+), 1 deletion(-)
diff --git a/frontend/src/pages/ActivityReport/Pages/components/Objective.js b/frontend/src/pages/ActivityReport/Pages/components/Objective.js
index f59d318929..a1a0681fb3 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/Objective.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/Objective.js
@@ -46,6 +46,13 @@ export default function Objective({
}) {
const modalRef = useRef();
+ console.log('citationOptions', citationOptions);
+ console.log('rawCitations', rawCitations);
+
+ if(citationOptions.length > 0) {
+ console.log('\n\n\n----------- has value');
+ }
+
// the below is a concession to the fact that the objective may
// exist pre-migration to the new UI, and might not have complete data
const initialObjective = (() => ({
diff --git a/frontend/src/pages/ActivityReport/Pages/components/__tests__/Objective.js b/frontend/src/pages/ActivityReport/Pages/components/__tests__/Objective.js
index 5ba6d1c8b6..4f5651dccf 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/__tests__/Objective.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/__tests__/Objective.js
@@ -163,6 +163,14 @@ describe('Objective', () => {
expect(helpButton).toBeVisible();
const citationsButton = screen.getByRole('button', { name: /Citation/i });
expect(citationsButton).toBeVisible();
+
+ // Select the option 'Citation 1' from the react-select dropdown
+ userEvent.click(citationsButton);
+ userEvent.click(screen.getByText('Citation 1'));
+ expect(await screen.findByText('Citation 1')).toBeVisible();
+
+
+
});
it('uploads a file', async () => {
diff --git a/src/services/citations.js b/src/services/citations.js
index f0ee0a845c..6b88fe45fc 100644
--- a/src/services/citations.js
+++ b/src/services/citations.js
@@ -3,7 +3,7 @@
import { sequelize } from '../models';
// TODO: Update this to the day we deploy to PROD.
-const cutOffStartDate = '2021-10-01';
+const cutOffStartDate = new Date().toISOString().split('T')[0];
/*
The purpose of this function is to get citations by grant id.
We then need to format the response for how it needs to be
From e5f5e62507efeb54fb1e120fcae288df5056e23d Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Thu, 5 Dec 2024 16:12:16 -0500
Subject: [PATCH 058/198] add test coverage FE
---
.../Pages/components/__tests__/Objective.js | 29 ++++++++++++++-----
1 file changed, 22 insertions(+), 7 deletions(-)
diff --git a/frontend/src/pages/ActivityReport/Pages/components/__tests__/Objective.js b/frontend/src/pages/ActivityReport/Pages/components/__tests__/Objective.js
index 4f5651dccf..ab40e74e82 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/__tests__/Objective.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/__tests__/Objective.js
@@ -158,19 +158,34 @@ describe('Objective', () => {
});
it('renders the citations dropdown when there are citations available', async () => {
- render( );
+ const citationOptions = [{
+ label: 'Label 1',
+ options: [
+ { name: 'Citation 1', id: 1 },
+ ],
+ }];
+
+ const rawCitations = [{
+ citation: 'Citation 1',
+ standardId: 1,
+ grants: [{
+ acro: 'DEF',
+ citation: 'Citation 1',
+ grantId: 1,
+ grantNumber: '12345',
+ }],
+ }];
+
+ render( );
const helpButton = screen.getByRole('button', { name: /get help choosing citation/i });
expect(helpButton).toBeVisible();
const citationsButton = screen.getByRole('button', { name: /Citation/i });
expect(citationsButton).toBeVisible();
- // Select the option 'Citation 1' from the react-select dropdown
- userEvent.click(citationsButton);
- userEvent.click(screen.getByText('Citation 1'));
- expect(await screen.findByText('Citation 1')).toBeVisible();
-
-
+ const citationSelect = await screen.findByLabelText(/citation/i);
+ await selectEvent.select(citationSelect, [/Citation 1/i]);
+ expect(await screen.findByText(/Citation 1/i)).toBeVisible();
});
it('uploads a file', async () => {
From d56f7d6ed1b3c1a3735e39c2ec5ddc67956a349c Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Thu, 5 Dec 2024 16:20:19 -0500
Subject: [PATCH 059/198] lint
---
.../src/pages/ActivityReport/Pages/components/Objective.js | 7 -------
1 file changed, 7 deletions(-)
diff --git a/frontend/src/pages/ActivityReport/Pages/components/Objective.js b/frontend/src/pages/ActivityReport/Pages/components/Objective.js
index a1a0681fb3..f59d318929 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/Objective.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/Objective.js
@@ -46,13 +46,6 @@ export default function Objective({
}) {
const modalRef = useRef();
- console.log('citationOptions', citationOptions);
- console.log('rawCitations', rawCitations);
-
- if(citationOptions.length > 0) {
- console.log('\n\n\n----------- has value');
- }
-
// the below is a concession to the fact that the objective may
// exist pre-migration to the new UI, and might not have complete data
const initialObjective = (() => ({
From f2d3dff31911ce4e16ddde4717960e003be10afe Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Thu, 5 Dec 2024 16:45:48 -0500
Subject: [PATCH 060/198] Add unit test
---
src/services/monitoring.test.js | 187 +----------------
src/services/monitoring.testHelpers.ts | 269 +++++++++++++++++++++++++
src/services/ttaByCitation.test.js | 86 ++++++++
src/services/ttaByReview.test.js | 86 ++++++++
4 files changed, 443 insertions(+), 185 deletions(-)
create mode 100644 src/services/monitoring.testHelpers.ts
create mode 100644 src/services/ttaByCitation.test.js
create mode 100644 src/services/ttaByReview.test.js
diff --git a/src/services/monitoring.test.js b/src/services/monitoring.test.js
index 94a972dcf5..87e302d8a8 100644
--- a/src/services/monitoring.test.js
+++ b/src/services/monitoring.test.js
@@ -1,21 +1,13 @@
-import sequelize from 'sequelize';
import {
classScore,
monitoringData,
- ttaByCitations,
- ttaByReviews,
} from './monitoring';
+import { createMonitoringData, destroyMonitoringData } from './monitoring.testHelpers';
import db from '../models';
const {
Grant,
GrantNumberLink,
- MonitoringReviewGrantee,
- MonitoringReviewStatus,
- MonitoringReview,
- MonitoringReviewLink,
- MonitoringReviewStatusLink,
- MonitoringClassSummary,
} = db;
const RECIPIENT_ID = 9;
@@ -23,142 +15,6 @@ const REGION_ID = 1;
const GRANT_NUMBER = '01HP044446';
const GRANT_ID = 665;
-async function createMonitoringData(grantNumber) {
- await MonitoringClassSummary.findOrCreate({
- where: { grantNumber },
- defaults: {
- reviewId: 'C48EAA67-90B9-4125-9DB5-0011D6D7C808',
- grantNumber,
- emotionalSupport: 6.2303,
- classroomOrganization: 5.2303,
- instructionalSupport: 3.2303,
- reportDeliveryDate: '2023-05-22 21:00:00-07',
- hash: 'seedhashclasssum1',
- sourceCreatedAt: '2023-05-22 21:00:00-07',
- sourceUpdatedAt: '2023-05-22 21:00:00-07',
- },
- });
-
- await MonitoringReviewGrantee.findOrCreate({
- where: { grantNumber },
- defaults: {
- reviewId: 'C48EAA67-90B9-4125-9DB5-0011D6D7C808',
- granteeId: '14FC5A81-8E27-4B06-A107-9C28762BC2F6',
- grantNumber,
- sourceCreatedAt: '2024-02-12 14:31:55.74-08',
- sourceUpdatedAt: '2024-02-12 14:31:55.74-08',
- createTime: '2023-11-14 21:00:00-08',
- updateTime: '2024-02-12 14:31:55.74-08',
- updateBy: 'Support Team',
- },
- });
-
- await MonitoringReview.findOrCreate({
- where: { reviewId: 'C48EAA67-90B9-4125-9DB5-0011D6D7C808' },
- defaults: {
- reviewId: 'C48EAA67-90B9-4125-9DB5-0011D6D7C808',
- contentId: '653DABA6-DE64-4081-B5B3-9A126487E8F',
- statusId: 6006,
- startDate: '2024-02-12',
- endDate: '2024-02-12',
- reviewType: 'FA-1',
- reportDeliveryDate: '2023-02-21 21:00:00-08',
- outcome: 'Complete',
- hash: 'seedhashrev2',
- sourceCreatedAt: '2023-02-22 21:00:00-08',
- sourceUpdatedAt: '2023-02-22 21:00:00-08',
- },
- });
-
- await MonitoringReviewLink.findOrCreate({
- where: { reviewId: 'C48EAA67-90B9-4125-9DB5-0011D6D7C808' },
- defaults: {
- reviewId: 'C48EAA67-90B9-4125-9DB5-0011D6D7C808',
- },
- });
-
- await MonitoringReviewStatusLink.findOrCreate({
- where: { statusId: 6006 },
- defaults: {
- statusId: 6006,
- },
- });
-
- await MonitoringReviewStatus.findOrCreate({
- where: { statusId: 6006 },
- defaults: {
- statusId: 6006,
- name: 'Complete',
- sourceCreatedAt: '2024-02-12 14:31:55.74-08',
- sourceUpdatedAt: '2024-02-12 14:31:55.74-08',
- },
- });
-}
-
-async function destroyMonitoringData(grantNumber) {
- const grantees = await MonitoringReviewGrantee.findAll({
- attribtes: ['id'],
- where: { grantNumber },
- include: [{
- model: MonitoringReview,
- attributes: ['id'],
- include: [{
- model: MonitoringReviewLink,
- attributes: ['id'],
- }],
- }],
- });
-
- const granteeIds = grantees.map((grantee) => grantee.id);
- const reviewIds = grantees.map((grantee) => grantee.monitoringReview.id);
- const reviewLinkIds = grantees.map((grantee) => grantee.monitoringReview.monitoringReviewLink.id);
-
- await MonitoringReviewGrantee.destroy({
- where: { id: granteeIds },
- force: true,
- individualHooks: true,
- });
-
- await MonitoringReview.destroy({
- where: { id: reviewIds },
- force: true,
- individualHooks: true,
- });
-
- await MonitoringReviewLink.destroy({
- where: { id: reviewLinkIds },
- force: true,
- individualHooks: true,
- });
-
- await MonitoringReviewGrantee.destroy(
- { where: { grantNumber }, force: true, individualHooks: true },
- );
- await MonitoringClassSummary.destroy({
- where: { grantNumber }, force: true, individualHooks: true,
- });
- await MonitoringReview.destroy({
- where: { reviewId: 'C48EAA67-90B9-4125-9DB5-0011D6D7C808' },
- force: true,
- individualHooks: true,
- });
- await MonitoringReviewLink.destroy({
- where: { reviewId: 'C48EAA67-90B9-4125-9DB5-0011D6D7C808' },
- force: true,
- individualHooks: true,
- });
- await MonitoringReviewStatus.destroy({
- where: { statusId: 6006 },
- force: true,
- individualHooks: true,
- });
- await MonitoringReviewStatusLink.destroy({
- where: { statusId: 6006 },
- force: true,
- individualHooks: true,
- });
-}
-
describe('monitoring services', () => {
beforeAll(async () => {
await Grant.findOrCreate({
@@ -178,45 +34,7 @@ describe('monitoring services', () => {
afterAll(async () => {
await Grant.destroy({ where: { number: GRANT_NUMBER }, force: true, individualHooks: true });
- await sequelize.close();
- });
-
- describe('ttaByCitations', () => {
- beforeAll(async () => {
- await createMonitoringData(GRANT_NUMBER);
- });
- afterAll(async () => {
- await destroyMonitoringData(GRANT_NUMBER);
- await GrantNumberLink.destroy({ where: { grantNumber: GRANT_NUMBER }, force: true });
- });
-
- it('fetches TTA, ordered by Citations', async () => {
- const data = await ttaByCitations(
- RECIPIENT_ID,
- REGION_ID,
- );
-
- expect(data).toBe([]);
- });
- });
-
- describe('ttaByReview', () => {
- beforeAll(async () => {
- await createMonitoringData(GRANT_NUMBER);
- });
- afterAll(async () => {
- await destroyMonitoringData(GRANT_NUMBER);
- await GrantNumberLink.destroy({ where: { grantNumber: GRANT_NUMBER }, force: true });
- });
-
- it('fetches TTA, ordered by review', async () => {
- const data = await ttaByReviews(
- RECIPIENT_ID,
- REGION_ID,
- );
-
- expect(data).toBe([]);
- });
+ await db.sequelize.close();
});
describe('classScore', () => {
@@ -239,7 +57,6 @@ describe('monitoring services', () => {
regionId: REGION_ID,
grantNumber: GRANT_NUMBER,
received: expect.any(String),
- // sequelize retrieves numeric fields as strings
ES: expect.any(String),
CO: expect.any(String),
IS: expect.any(String),
diff --git a/src/services/monitoring.testHelpers.ts b/src/services/monitoring.testHelpers.ts
new file mode 100644
index 0000000000..abe01a1b65
--- /dev/null
+++ b/src/services/monitoring.testHelpers.ts
@@ -0,0 +1,269 @@
+import { v4 as uuid } from 'uuid';
+import db from '../models';
+
+const {
+ MonitoringReviewGrantee,
+ MonitoringReviewStatus,
+ MonitoringReview,
+ MonitoringReviewLink,
+ MonitoringReviewStatusLink,
+ MonitoringClassSummary,
+ MonitoringFindingHistory,
+ MonitoringFindingLink,
+ MonitoringFinding,
+ MonitoringFindingStandard,
+ MonitoringStandard,
+ MonitoringStandardLink,
+ MonitoringFindingStatusLink,
+ MonitoringFindingStatus,
+} = db;
+
+async function createAdditionalMonitoringData(
+ findingId: string,
+ reviewId: string,
+) {
+ const timestamps = {
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ sourceDeletedAt: null,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ deletedAt: null,
+ };
+
+ await MonitoringFinding.create({
+ findingId,
+ name: 'Finding 1',
+ statusId: 6006,
+ findingType: 'Finding Type',
+ hash: 'hash',
+ source: 'source',
+ ...timestamps,
+ });
+
+ await MonitoringFindingLink.findOrCreate({
+ where: { findingId },
+ });
+
+ await MonitoringFindingHistory.create({
+ reviewId,
+ findingHistoryId: uuid(),
+ findingId,
+ statusId: 6006,
+ narrative: 'narrative',
+ ordinal: 1,
+ determination: 'determination',
+ ...timestamps,
+ });
+
+ await MonitoringStandard.create({
+ contentId: 'contentId',
+ standardId: 99_999,
+ citation: '1234',
+ text: 'text',
+ guidance: 'guidance',
+ citable: 1,
+ hash: 'hash',
+ ...timestamps,
+ });
+
+ await MonitoringStandardLink.findOrCreate({
+ where: { standardId: 99_999 },
+ });
+
+ await MonitoringFindingStandard.create({
+ standardId: 99_999,
+ findingId,
+ name: 'Standard',
+ ...timestamps,
+ });
+
+ await MonitoringFindingStatusLink.findOrCreate({
+ where: { statusId: 6006 },
+ });
+
+ await MonitoringFindingStatus.findOrCreate({
+ where: { statusId: 6006 },
+ defaults: {
+ statusId: 6006,
+ name: 'Complete',
+ ...timestamps,
+ },
+ });
+
+ return {
+ findingId,
+ reviewId,
+
+ };
+}
+
+async function destroyAdditionalMonitoringData(findingId: string, reviewId: string) {
+ await MonitoringFindingStandard.destroy({ where: { findingId }, force: true });
+ await MonitoringFindingStatus.destroy({ where: { statusId: 6006 }, force: true });
+ await MonitoringFindingHistory.destroy({ where: { reviewId }, force: true });
+ await MonitoringStandard.destroy({ where: { standardId: 99_999 }, force: true });
+ await MonitoringFinding.destroy({ where: { findingId }, force: true });
+ await MonitoringFindingStatusLink.destroy({ where: { statusId: 6006 }, force: true });
+ await MonitoringFindingLink.destroy({ where: { findingId }, force: true });
+ await MonitoringStandardLink.destroy({ where: { standardId: 99_999 }, force: true });
+}
+
+async function createMonitoringData(
+ grantNumber: string,
+ reviewId = 'C48EAA67-90B9-4125-9DB5-0011D6D7C808',
+ granteeId = '14FC5A81-8E27-4B06-A107-9C28762BC2F6',
+ statusId = 6006,
+ contentId = '653DABA6-DE64-4081-B5B3-9A126487E8F',
+ findingId = uuid(),
+) {
+ await MonitoringClassSummary.findOrCreate({
+ where: { grantNumber },
+ defaults: {
+ reviewId,
+ grantNumber,
+ emotionalSupport: 6.2303,
+ classroomOrganization: 5.2303,
+ instructionalSupport: 3.2303,
+ reportDeliveryDate: '2023-05-22 21:00:00-07',
+ hash: 'seedhashclasssum1',
+ sourceCreatedAt: '2023-05-22 21:00:00-07',
+ sourceUpdatedAt: '2023-05-22 21:00:00-07',
+ },
+ });
+
+ await MonitoringReviewGrantee.findOrCreate({
+ where: { grantNumber },
+ defaults: {
+ reviewId,
+ granteeId,
+ grantNumber,
+ sourceCreatedAt: '2024-02-12 14:31:55.74-08',
+ sourceUpdatedAt: '2024-02-12 14:31:55.74-08',
+ createTime: '2023-11-14 21:00:00-08',
+ updateTime: '2024-02-12 14:31:55.74-08',
+ updateBy: 'Support Team',
+ },
+ });
+
+ await MonitoringReview.findOrCreate({
+ where: { reviewId },
+ defaults: {
+ reviewId,
+ contentId,
+ statusId,
+ startDate: '2024-02-12',
+ endDate: '2024-02-12',
+ reviewType: 'FA-1',
+ reportDeliveryDate: '2023-02-21 21:00:00-08',
+ outcome: 'Complete',
+ hash: 'seedhashrev2',
+ sourceCreatedAt: '2023-02-22 21:00:00-08',
+ sourceUpdatedAt: '2023-02-22 21:00:00-08',
+ name: 'REVIEW!!!',
+ },
+ });
+
+ await MonitoringReviewLink.findOrCreate({
+ where: { reviewId },
+ defaults: {
+ reviewId,
+ },
+ });
+
+ await MonitoringReviewStatusLink.findOrCreate({
+ where: { statusId },
+ defaults: {
+ statusId,
+ },
+ });
+
+ await MonitoringReviewStatus.findOrCreate({
+ where: { statusId },
+ defaults: {
+ statusId,
+ name: 'Complete',
+ sourceCreatedAt: '2024-02-12 14:31:55.74-08',
+ sourceUpdatedAt: '2024-02-12 14:31:55.74-08',
+ },
+ });
+
+ return {
+ grantNumber,
+ reviewId,
+ granteeId,
+ statusId,
+ contentId,
+ findingId,
+ };
+}
+
+async function destroyMonitoringData(
+ grantNumber: string,
+ reviewId = 'C48EAA67-90B9-4125-9DB5-0011D6D7C808',
+ statusId = 6006,
+) {
+ const grantees = await MonitoringReviewGrantee.findAll({
+ attribtes: ['id'],
+ where: { grantNumber },
+ });
+
+ const granteeIds = grantees.map((grantee) => grantee.id);
+
+ await MonitoringReviewGrantee.destroy({
+ where: { id: granteeIds },
+ force: true,
+ individualHooks: true,
+ });
+
+ await MonitoringReviewGrantee.destroy(
+ { where: { reviewId }, force: true, individualHooks: true },
+ );
+ await MonitoringClassSummary.destroy({
+ where: { reviewId }, force: true, individualHooks: true,
+ });
+ await MonitoringReview.destroy({
+ where: { reviewId },
+ force: true,
+ individualHooks: true,
+ });
+
+ await MonitoringReview.destroy({
+ where: { statusId },
+ force: true,
+ individualHooks: true,
+ });
+
+ await MonitoringReviewStatus.destroy({
+ where: { statusId },
+ force: true,
+ individualHooks: true,
+ });
+
+ await MonitoringFindingHistory.destroy({
+ where: {
+ reviewId,
+ },
+ force: true,
+ individualHooks: true,
+ });
+
+ await MonitoringReviewLink.destroy({
+ where: { reviewId },
+ force: true,
+ individualHooks: true,
+ });
+
+ await MonitoringReviewStatusLink.destroy({
+ where: { statusId },
+ force: true,
+ individualHooks: true,
+ });
+}
+
+export {
+ createMonitoringData,
+ destroyMonitoringData,
+ createAdditionalMonitoringData,
+ destroyAdditionalMonitoringData,
+};
diff --git a/src/services/ttaByCitation.test.js b/src/services/ttaByCitation.test.js
new file mode 100644
index 0000000000..52acaefa29
--- /dev/null
+++ b/src/services/ttaByCitation.test.js
@@ -0,0 +1,86 @@
+import { ttaByCitations } from './monitoring';
+import {
+ createAdditionalMonitoringData,
+ createMonitoringData,
+ destroyAdditionalMonitoringData,
+ destroyMonitoringData,
+} from './monitoring.testHelpers';
+import db from '../models';
+
+const {
+ Grant,
+ GrantNumberLink,
+} = db;
+
+const RECIPIENT_ID = 9;
+const REGION_ID = 1;
+const GRANT_NUMBER = '01HP044446';
+const GRANT_ID = 665;
+
+describe('ttaByCitations', () => {
+ let findingId;
+ let reviewId;
+ beforeAll(async () => {
+ await Grant.findOrCreate({
+ where: { number: GRANT_NUMBER },
+ defaults: {
+ id: GRANT_ID,
+ regionId: REGION_ID,
+ number: GRANT_NUMBER,
+ recipientId: RECIPIENT_ID,
+ status: 'Active',
+ startDate: '2024-02-12 14:31:55.74-08',
+ endDate: '2024-02-12 14:31:55.74-08',
+ cdi: false,
+ },
+ });
+
+ const {
+ reviewId: createdReviewId,
+ findingId: createdFindingId,
+ } = await createMonitoringData(GRANT_NUMBER);
+
+ const result = await createAdditionalMonitoringData(createdFindingId, createdReviewId);
+ findingId = result.findingId;
+ reviewId = result.reviewId;
+ });
+
+ afterAll(async () => {
+ await destroyMonitoringData(GRANT_NUMBER);
+ await destroyAdditionalMonitoringData(findingId, reviewId);
+ await GrantNumberLink.destroy({ where: { grantNumber: GRANT_NUMBER }, force: true });
+ await Grant.destroy({ where: { number: GRANT_NUMBER }, force: true, individualHooks: true });
+ await db.sequelize.close();
+ });
+
+ it('fetches TTA, ordered by Citations', async () => {
+ const data = await ttaByCitations(
+ RECIPIENT_ID,
+ REGION_ID,
+ );
+
+ expect(data).toStrictEqual([
+ {
+ category: 'source',
+ citationNumber: '1234',
+ findingType: 'Finding Type',
+ grantNumbers: [
+ '01HP044446',
+ ],
+ lastTTADate: null,
+ reviews: [
+ {
+ findingStatus: 'Complete',
+ name: 'REVIEW!!!',
+ objectives: [],
+ outcome: 'Complete',
+ reviewReceived: '02/22/2023',
+ reviewType: 'FA-1',
+ specialists: [],
+ },
+ ],
+ status: 'Complete',
+ },
+ ]);
+ });
+});
diff --git a/src/services/ttaByReview.test.js b/src/services/ttaByReview.test.js
new file mode 100644
index 0000000000..c60b10f891
--- /dev/null
+++ b/src/services/ttaByReview.test.js
@@ -0,0 +1,86 @@
+import {
+ createAdditionalMonitoringData,
+ createMonitoringData,
+ destroyAdditionalMonitoringData,
+ destroyMonitoringData,
+} from './monitoring.testHelpers';
+import { ttaByReviews } from './monitoring';
+import db from '../models';
+
+const {
+ Grant,
+ GrantNumberLink,
+} = db;
+
+const RECIPIENT_ID = 9;
+const REGION_ID = 1;
+const GRANT_NUMBER = '01HP044446';
+const GRANT_ID = 665;
+
+describe('ttaByReviews', () => {
+ let findingId;
+ let reviewId;
+ beforeAll(async () => {
+ await Grant.findOrCreate({
+ where: { number: GRANT_NUMBER },
+ defaults: {
+ id: GRANT_ID,
+ regionId: REGION_ID,
+ number: GRANT_NUMBER,
+ recipientId: RECIPIENT_ID,
+ status: 'Active',
+ startDate: '2024-02-12 14:31:55.74-08',
+ endDate: '2024-02-12 14:31:55.74-08',
+ cdi: false,
+ },
+ });
+
+ const {
+ reviewId: createdReviewId,
+ findingId: createdFindingId,
+ } = await createMonitoringData(GRANT_NUMBER);
+
+ const result = await createAdditionalMonitoringData(createdFindingId, createdReviewId);
+ findingId = result.findingId;
+ reviewId = result.reviewId;
+ });
+
+ afterAll(async () => {
+ await destroyMonitoringData(GRANT_NUMBER);
+ await destroyAdditionalMonitoringData(findingId, reviewId);
+ await GrantNumberLink.destroy({ where: { grantNumber: GRANT_NUMBER }, force: true });
+ await Grant.destroy({ where: { number: GRANT_NUMBER }, force: true, individualHooks: true });
+ await db.sequelize.close();
+ });
+ it('fetches TTA, ordered by review', async () => {
+ const data = await ttaByReviews(
+ RECIPIENT_ID,
+ REGION_ID,
+ );
+
+ expect(data).toStrictEqual([
+ {
+ findings: [
+ {
+ category: 'source',
+ citation: '1234',
+ correctionDeadline: 'Invalid date',
+ findingType: 'Finding Type',
+ objectives: [],
+ status: 'Complete',
+ },
+ ],
+ grants: [
+ '01HP044446',
+ ],
+ id: expect.any(Number),
+ lastTTADate: null,
+ name: 'REVIEW!!!',
+ outcome: 'Complete',
+ reviewReceived: '02/22/2023',
+ reviewType: 'FA-1',
+ specialists: [],
+ },
+ ]);
+ });
+});
From b5625f98a661d4d2df6f90336e0cc4452508862c Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Thu, 5 Dec 2024 17:11:45 -0500
Subject: [PATCH 061/198] Update LDM
---
docs/logical_data_model.encoded | 2 +-
docs/logical_data_model.puml | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/docs/logical_data_model.encoded b/docs/logical_data_model.encoded
index 93aff94573..96a0e96342 100644
--- a/docs/logical_data_model.encoded
+++ b/docs/logical_data_model.encoded
@@ -1 +1 @@
-xLrjSzmsalxENy7IVaZYccnjaijLdMhlQcLPTbndoof9pjOcbL9193I3c0Hc0L2Eaij_lmB05m04IO3ao7Q2JzA0O7VpGQFHwCRBFnW7b5TbaQJfPocu9oXFzvJS5h1awNl4Tod0smBcQKR9UvRUGSYJD6Nl4Du32igqG1ZoXprC2UKxOZpA2i1O-bkIfcdwbD0SqbDI49h-vPjl_lFal_wcbL_lKFOEGyRIhvCq_LsASYhUXYn9Qped6KVtY3zSmWpj4SIS_PQaJrAKfySfmXnk8KsFqxzl8OOKS9Z_Jv9kAS3yfLdXykpvsUNP-UdPLNhqGZndHtz5UP8IleFq37F47qRUUDFMGh1-yKaAMidsX8xnEp4gjkzd50g1JtXwd79KGiP1kLVOYv_fhmJ3z3Z5vFtdNuZwrJxth_zxajZu2UOd-pqbzv1EWxZy-oe6wSXRRncaZuD5ciC2JjLEKaouU8V3NtYICWvunTBZ5EAg74Bpb1G5k2KKSFBdGdG0Z-UeW8-1mnqguCXRKemG77o_V-YxNn1GF4Umo2yXsnFCPe0WI7W38Igt7xWbeBY6vRu074eXBYWfzwFsp_t6OI1BQOr9tRL-_6aCnKl8I4MpHmMGrZn7tcx5EWn4YhXiYiRaA9Z4V_BdpGXKDyRhloKf1gN2NPOdajCT5OKrINzgV_-_BxySQwRXbfnkGKBH5c5GJBISqLPBBSSsWDUwRPmLXC6hYkqXpay95bsX7MpqPvSGC8ARWIU5E7y6vBWac2GfOlBBOF5mbYCEnQVS4b9085wv0mMJTIuv1tlG4X4AqOriikNQP-r0TmzejWFqyw-__VOBJMUQzmOwxPlRdw4Kw9HKT8xXQmbuDsRxHjTIalS8WLgu3pasjbBF6oJNWEbZ6pPR2UTR_JLW-GJjVn-vtY2WctzUY3ySN51oZtAO3um3KrGTWq7_bOcclYPxb7sOTkQ2zSZxIhGFqW7y1qiV8FgVldJO9BEu7dPI1-WR4KCmvFbgzZZRVk6f4lexI64tjWuEawpQ2w2kHIIa8INxMyjJwtOJG-byAnZUe9-WpHisTMUdh_xtj7tybi4yIK6U8uu81iKvuIASb2bT4DDyx7gXjPTks-Xl4YSiuNJh5e3njmh3fmz-q_sGm025MqJmQCEB2e_KYv-a8Xg5P4_inFawqJIyZllLVSpiVrOpjxqEg3ZNf5CEYgmIq-3yoeZtDDhIMGu10g6NfEZwUGuBT0_fmsl8ToJlYPPN573rOJdCK0cApx_PVFM5IrsxgN7Vto-5qiWgFpE2RuLwu6q5jZOMukK2Ra6cLCQKnQdp6bTSbRUGlhcx09IZ3Er5VBHha2JTWoqK2C5Q355YrBGbsLEofQ0KRUPwJ1lZst_mxmeoBfN0ihGYNcr0mwhR4Z6XErRZoGUSGnD93kWMyWko5mqk5Dp1Nhe8pWSSEFHFFBb7b5UWU5OhWqjBMtklnmk5AeLRUfs2jzKtmfIj6I2Zam4ftc5z8LqigIYJQ9HWpwx9o2pF8jZ8VmyAb3yVHUpxkifSgrTFiKsTVIBwmHvi0OTly6j8jz2OhdDQmGC5ipMGoxB2A9Cg_ufiVMUVcVNUwEJQhFeHpJTxedbyEMq6dD4Xiab67IeqB-hbW8FExeT_krz0Ph9JBGgT6A6d9-bU3FQCipVuxRuWm1c-Eco7EBk7c2kxU84Ami-1yXzE8UQGodRoK19eKHbfUfjX2oKsE8zGC4Lyb6oGATkvsxY5Udf2O1UfmRRnCrJwYAXvBM9mzlxmqxUNbsVdrpVNBr-VNLsVlBvmzJ_o7fk8kztf06WSRFiEtvvazeUrY7cRSq-J8qps5VeLcb_p3Zuuchy9IGWP1ZJ4pA86cp0HZW_DiMQM4p0wS0tu2o5PWayjorqW_EufoDwr-cMW7Pp5387gmsKg2Jvf9eINUbH7Fh5A5cSrG_EO8H0UCcxZ1aaVbqsmGaTihvZFqipcL2xJcRqKXLUeH0MWY3-yeAJQ-wKmm_NVOZFngbNxGA4u-4jML3dCNzwT4twIbmWx6ldt4R2cWReBJVJEyyBNnKKPz-y0Uudb318Cv5jx4ImBB3a8dku0tdhOWv3nPErizlonnuwNOVZ-oXqn9qK-yAGD2YaIGzwYV2334_reZ86xQ-3mxD7Dgvv3KxYffRde4AQNjPSYQfCbCv7WlT00L7NOKRrt9pxVI53i4ExOa7qJ36k2WRMkrwKotEV4jSlfDJ0S65rYC_J9Hirket1NjM4LElZFnUEJIE8uWPaQsFNHVEBbTbJlrNCrpNIJ728zqjP31BVPWcPAj0DiftVwMj9pCt2Y5xycERf3gpMYp3VL-O29bXwSkVkvI_ELNGMfBW25c8Uu4I5BmJycJxcMv7i4z7IJn_IklrxiEINbLGaePpi8-JK75mN0-fHdVBaOvUuy-z9bP1BvSvbtUeNmktLNzVitvROJ8tYa3GiyIlakuRu06IphVERYwAoJw4gnsaD6OMsvFPlV-j8fRHW2Xgho_4MGIgct4Dw-M5qJvplUjFj4lT-RGFVH3H_d3ZSXCrImwOdBOiq0umeKpokS8xnzJJ3VzKXcEN8Pw5gDNOywqpfi4gIMGdSuL3wxsiCCWG8nVbO2L4nLSX92M2Q5fJinAP5UsLhpL2VffqJgLNCAfUVdHzm8iOZWbEJEznq3YAR7fDf_a5ZmL9Z62CpyWQB1CWhcjXwEQKoGIGp3z40JDh_0bJ_x6QEdyBqkgji2PSPImnsWCFydaaRBt7xsKYQ7dW4EV1C_AftqV3mJ_xAj50owaU59ZzFFNZuxExz--VpbsRFFZe-Fv2pEalf6lNFaZv9SI83lI9SJCoPD56Q4vgUaKckHwYeisy4Iz1LR-rYFiAN_2WQ5z2JAuQi0o7iiKkKdNCm1zNLPwN7wjpfmb476XYluDzBS1kBxvwDjPA6_Ph_BuHt2I0zNoMGjnE2sW0CB3Exu_WfoZl3MUCVgmaA3ycf624wepMl0thKlxW7lG_4Qe09ISrNAhKYfSlSgC6tajfU5pKrxjsCB52E_d6VOwzkUf3-JMyUImYTYcPs8iIrsrYXaLS7RbOMioeGsQnPHHmnpY07Ee0zidABj5j9s6MrRI--L5issTm8EjuG-Q93FMBOZ8pTwwDL_NBXICDnLXHC9WX4d5E6jt2z_LPKbe0Xw3zN6ZDSrLcipywE_7cdl7Ad9m3s2xr_cRWATS9UoNaEbUUieQ82HSORJeDflVTCxdZlt06EKaF8DtGACVhLNzNwx-GzlVmkMH2sikL8R1tfaZ84DOSPfbOs_GQK5QDQVJdlW4g5dGRFrha6V8pcbylOBxrJzQKsf2JUgqhvtrAeiXDDKNgYAXjgAzuwwKN1aWEv9en9L3RNc_QEm1JMIKfr_QOOHA-RxCUWuON1wUfiHIg7KnsSGvAyGXii3PBK2Ut1lGuxwHjQ5clmTOkAtbo-uN9N3LL39zp2YYOzeY-yCcLgM7uSA-obR3Zca9SB0o94Q_H43AdPkt7UQvaZNhgXyUkEkw1VtTUrGaUtnx_9oIWxHCMPEt3yLJLv50zTMEjj8FfhANN60Sq3pntDAMCtHIsaRyJH5hL_-n9pXHzJDFvnPFAWzZ8PmKTiygjs8qv0u3N7CM5UqSlQC-V0sJaqlwi_nZaCjdjH_Oj4iLUyQbV23Z_HF7u9rewatAVzZ66wjDLRiE9OkF5Q_-UFuElhthEtgzB2Adavmo_XjWLvi45TjLR6ve5vUq3JP7st6PA_Rj29IchgIYcT4Nw2f3lRb76IxqeGfC3MW6j3m4GCqhdz2XqC7ZNdmkfQkDna_lAqB2Jgi6Yv0N2ItpKzTqai-lea0ucQ4rgyNRIkKYqpkBFtLDp2rZJtlU82FRMj_-F8eIx2bwvG7llEGQwkwJu6_y2iEzo7RSd3zpbZJrLK-hUquFJ3wMt48Tst5cNXHRiFfrR6Ru36EjlFMAVsuT_NpUivIZOoKUs5Ud2EVTZpkuozTdNTgunzd2UkC3OeVo-wlQslhfr7-0VP_zJ7rs-tbjqzBtzqrvu3JDJQ6BhW89BGN77hxcBOjUzvXgBDtdzqXolXfRH1zwhx45s9tWLozEbtxdF0hCSdmawF-DY_zzvHPLFKMMLJsbbXKzXLuS4lUq1uypPPq7LdNv9LcOXu7uiqxIlr2hDcGHptwXI_2Z3ZBoEW0UoMUVyINi5oe3JeNa3A2CIoMEkisFUCKQMYUbXFwXRsD8lK2Ti6mTxIMEcrrsUfYU7UdxFra-0CwTpyfm-DIPjc_NG_s5DwXNnndHT-vYflkwTxtLBU5QmVV8NOtptVMkW2MTVIi6w7sUDZHZMIE9_-2W8Pk7XkvE-n3cRQnZOPqzCZZ2YAhJEpe-EpuMFm6cJWxznxG1uItEW1RJzk9nyxdkrIdyvvHQanYXZOjh3kvBTt7B9CzRCzPji-JaT8-9Vb3kno_ERRBMUZSbrXHD8A7VqJ1VxjF2H2UavgZHVkMz2RKiKFgcrfjBo47vhc6JObV0Az49NIgfTlB4EVqU5_v478aySTmcOFl4Sw-t17hzjYQe-m2Y-FjkbT5uHpDR6UX-txb5nt0TymXooZQEtlrofxb5PHkfNRegE_Eof2zWQp7PSpTx5K-i1xt6TPxJH3UTdcbFpictiS2ppaBUCrZQcUVFTjzF2glCaAXF7yLPBjLhPm82trb-R4455_Rn-hMFj2DxQqn8LdlKeNWzaKSdXJGRolg8T8nFgT9F4bBo9Ywww7DpQdJUPkTl4rciDt7sttOZQZQ9ECzpcQOU-2STSZlFQ08I7VGtBlkxGO49eKEEIWEZtrlcqC8L_PhFdpcJx2eDqU_kOUKeXoktKUgZiN4AyqkxKCTy7aFYWhsCmSwQn-DbI_rLJ5W2pleja3V3X-v7TwItxU7EQhvm0UoTfoyWovVZhkc4hxDrnxA6e37D00xKiBB0z-Hj6Fo2XuwOCB4zEqLsQffuFgH6dUSgm3RBccS7gSdbvSd__hnfsPSaU3To5f7ubm0yGad5TyHAYF8ZfC3cezqtKVa30hj28RdpWEWZ1Is9ed8LmDyBm-iG8oZZ3Lho6-4Jt5JYZjNRaXjneGz5-T2tPI0lfCLn1WfJp9Eqh9TV7CZcrm55PBqtnKe47-u0OnLLAmjkSa1szqIGFCcNGWAnKrxm2b0MD1IIx2Ozgn9gEkV8RFdig3HrUO-cgMntANtsY2ct5LtDkeHJupxuUHPFIlq0AMRGXBQYqdySF8S5HnIlonyLMBdj5Ykm1gzpUA7arSdzYLc7b4puSxVQ3LaMLdS0DOhkdte5NL0JWmvekulaarLATMGHVeAY0v3omd7yFqD9Uzlp9a7AN97QJtDks5ewsY9L-OYZZelIYDCep0DB2Q9KFMXig8v4Q8-H6FtVaIyt7Wv8lyHAZTK4Q9HYBGLWjPzRH6j4RIYNVV7AXghCBpiI6K4QmIiYDCeJh7lUS4KuR7W6RagEW9a0sHTfH01KW2DtlmSGNA1507g0HHd9GvOdZrxvG597l1XkVLXuW9sWFCR3hQzsBEeB45iFstmczNMc1kKGxcrfRzyKZrtvk7K2UetpcJD13v1jsn7bN915W6o08e3c0EKaIko8q0DG36wFOLGdAQEW3D-DNJ4QG2dY94B8_Tbq16W1g3nMviZJZPEOuSSHBHijE8uvm5I1CS1JWJ51cNm0j-8JulFy7s_4IGNI14W5q0HbHugmxV1HeHQXABo348JKR5G6RnQDiBJmHEX6OGcmcB14dAzwvyEcP1hNFzqy4pUxHfnCd4Qk9KqmkI4oN7bPKHg12dt_OmHicd8eefDuXMroWSbnqKB4QaXfAAj2i8g6gb9e292WmYvx--BA9ae3LqE675ouHILoGEaED05GXNxtHHE4Q11W0RkROimkM6A45aGnVqVWH15U6nuXcznYm3f04LWB62cxc2BA1aenLMWKmX5P6nat7T9HN04ECwxgo8q4fh1YwiYFXe-KMyiNE9ngiF8RAEXSS4I9mrkO1ptCqLa6cGYA1OezerF4HM9ojGTKX4cDqmJrqn5sDomHOXiY4Rkgue8AOJwrrr5P0GoqVjneUAutYe2qm0pV3jMnCZ2A6BZm6RuJgw8KODH1CK2fNyXLCH5uEA-5ofYex2nRxhwK-EIV9QUC4Oo96qlIv94pjFJew-EqY-__F4ljmN8tcts1K1umtSeUuSXO__zzKzVVYchQ5k0XLpjhusyzZXHkyzyYTcdfwJ7wLOKRTIVxacIZDpzP74Jbyp1t6BtMveUuQEqTw1uUYokDpFBDYaOP9OuhdO9u3zrvr1wRF4MLJQEClErrQLgGyNU0hLl94DN9CmMoBuSfYTJMs4pzA1-yu2oDJqYL3BvtW7b6cx9QTaUeqtLp7E6SEf5Ix9v0ZLvj9uKElEcqYeSZMl-N6oRPwjnCotcs8VsQgycWxkFR5BQFLsDbxA1KzyfQwt6K-iNKuoxggjn7-xfDlEXVXt2krrebnwdR0yp10h4U5kwwUwkryfCCPBUxHKpHtBrxd5z2wpzP2FBdrDIzjvP947lq6dXlLXb6yEx41oMiUoT-2Gz9gjmPQRkbOFfSNrgllN0oTowNYAecHttTqYdpIpVz4gKEMbSXXLzZqp-D-bgX7FkM7GAgrWrYYr-9RqZyWDOUDn88EOWejP6_Hcll4WOHqv761EzbDLIEESeuGFFdPoewCFDChezxqzdOBbuePo6D5-nS6jxA8-1d6tN2l9dFRLRJjoPwHJCNENDXD9nf7QWaNQTN5xRQklHnbN8ag4JcTUT-EqbqKPxkhdT2jmd-BauXP9tIgX0RXvjQfRhCEelrUwnsuvMNqKAtUaQPjuvN4juDZoOIJnPFjrlA6zIGaLC2nH3qtpfs076q1R3t44eM9WXBshJywNcqUwflI9kAdqeKRlLbR1cAwzefPncQJxSFNCg1fMspGSjO30R_P3_zakONZgBhhHTW49SnwAo7IhoUMWO4xRMdQtxLp2r_W8CCRQ9aZSORjH_r--9eYwBP3T-Jv9U7DIVwT0vQSAB77ZKZwwkmjyyti3WRpuQu6-V4XqqBtAzt_dHrgF6eRQALnUZZMVhPPjUKSTxSVYgZfuxebR1z5i3L0VaLGSBfkA0XAgV-Gj5MVriJX7Pc8zDvCog-hCvahLd0pUJQFMhRg1mYlP63kLZ6KLBQREEvdmvAvp3EuHXtgbdeP91EzH6QU2BtfCatFiUZ5Ti-c6BkVJDDjpeJPdesIf_ijOfm5QU2vbFQnxXz4tQ6uq7Iwp3GKFiiVNcSSefscuXXVdJXqtt8_4uvs_MC62VhsWC1BtpkJRzX-sRZoDYUqgVIr2hIkE-o47BVOhT4e5QJRxh23e0gdLxVVwZRHRUMelbQG-I6D0XzN2ZmtK0APpSfxYkfAAjHh2n7ZEa42i9CTP1_7h6Ie8CjOIJjNucsIfDiEq8-hJ1Olu1_dJfufeuJzHQUshHaTuCJZXMb6JKKhbb6J5KIwMPR29R5lE5rILnYS0sJgcIaQF45aZWj1Nceu7-kgOK0pWu-Hg7EgqrMesQwxRYgbAu1ykcr8BTEcPdzGmIDahzKadpPVAJ0H-GPvTzFhUuDY48gtxIvS_3S-vw-17NPPlslouCwGYHhnXh9tRXPC1zaJo3iRgU7CcQwvCCRbryV7fzqaYaN5lyCKlKwOpynfsSNygl2xrgto_Zh_edwUUWB0reaPROWpEiDPUPsRuwaqwrabv0Pr4igB445xsOaB8STddIndpM245jUh6eH0DwVbfQ1VHgGlsyD9l7PI1DeJ5ofYdcyMeSV9swykPdqNEd0Uf2VM5KKfzJCTj8AQ453TiAiHcqpTHaHratwhFeTlZM11jmGoavXw66AkcFD-4Sj6EADDBxhdG6pSOVgSJVJReZ9LH4acuSNskjDgt_7r-XlPolStdn47F7UkBb4lWmNiPl6DljOZI7bX2OFFhB1-RmSdcD7P--rwZsHIpRKq69MgHaFGcctPtCqXAbRL4mjNO_RzqRwzqUWcAY3lMORIgOUDsq5AxbRIikpMgMzkehjLRdERXYooc3itPIjzzwEmMu-6TgNq0mLqFLQXQQhkdRUGDLpjufBV6JHHkfXlRpH8qOQ-mh4EIR5MLj-KBwzgH3vs5EDfRnaUlOAxuJQueBmFZRVIvsweai5gsTd506BkUrOT7tN4Ef-uL655m-0P5G_gwbL8fRVodL7DhsWfwEjl4adBOhIrmJxoU9GwcFjG9U0VN1oRXYCJ0F3-LYZZQAj3xOXDBhZa8HAzefL4ltMLiQJD0UMpVT23kBbiDeTcGNMqpN0GGjXtlhGTBiEb-uFUM-9jFV8Z6BHVjA4thylgNVX1gQaedji_We_LtZIJXty6p6oCOJO-Pd3En1zIVjy4wPptOjFQBbpStUpS5wY_PNOQElsJyG2o_SrcLfUaaiP6lRCkIM4BGMKDlXaWM4ZVpmggG7Gjx_H7n7gcOvjbdUArnL7YFOgiTDqRgCl9JQkNjlhZvaFbi30tIY3bXA_sLryeEfa7NP686hwnqbrNRNyp4tvkEUlsO-TpjB3i47s8tKdDu5EIg4QvVYN6jLYDb3lUZGcyReRZZMph5bBxyQtZ0bQZgduUwDL7Fx2_FV8pMNnS1ceQL6n0ndh-IcRFNj1_CwcBt4UhwngUFhmKriYwCQjfSYdqiF3sV0byBV137D_EPSGRKCO1ljz-_6AevOHmIbwx9LYGDIBFEog1v1dVjjYaWJx9nBHNrRalm3uhmgY-J_0G00
\ No newline at end of file
+xLrjSzmsalxENy7IVaZYccnjaijLdMhlQcLPTbndoof9pjOcbL9193I3c0Hc0L2Eaij_lmB05m04IO3ao7Q2JzA0O7VpGQFHwCRBFnW7b5TbaQJfPocu9oXFzvJS5h1awNl4Tod0smBcQKR9UvRUGSYJD6Nl4Du32igqG1ZoXprC2UKxOZpA2i1O-bkIfcdwbD0SqbDI49h-vPjl_lFal_wcbL_lKFOEGyRIhvCq_LsASYhUXYn9Qped6KVtY3zSmWpj4SIS_PQaJrAKfySfmXnk8KsFqxzl8OOKS9Z_Jv9kAS3yfLdXykpvsUNP-UdPLNhqGZndHtz5UP8IleFq37F47qRUUDFMGh1-yKaAMidsX8xnEp4gjkzd50g1JtXwd79KGiP1kLVOYv_fhmJ3z3Z5vFtdNuZwrJxth_zxajZu2UOd-pqbzv1EWxZy-oe6wSXRRncaZuD5ciC2JjLEKaouU8V3NtYICWvunTBZ5EAg74Bpb1G5k2KKSFBdGdG0Z-UeW8-1mnqguCXRKemG77o_V-YxNn1GF4Umo2yXsnFCPe0WI7W38Igt7xWbeBY6vRu074eXBYWfzwFsp_t6OI1BQOr9tRL-_6aCnKl8I4MpHmMGrZn7tcx5EWn4YhXiYiRaA9Z4V_BdpGXKDyRhloKf1gN2NPOdajCT5OKrINzgV_-_BxySQwRXbfnkGKBH5c5GJBISqLPBBSSsWDUwRPmLXC6hYkqXpay95bsX7MpqPvSGC8ARWIU5E7y6vBWac2GfOlBBOF5mbYCEnQVS4b9085wv0mMJTIuv1tlG4X4AqOriikNQP-r0TmzejWFqyw-__VOBJMUQzmOwxPlRdw4Kw9HKT8xXQmbuDsRxHjTIalS8WLgu3pasjbBF6oJNWEbZ6pPR2UTR_JLW-GJjVn-vtY2WctzUY3ySN51oZtAO3um3KrGTWq7_bOcclYPxb7sOTkQ2zSZxIhGFqW7y1qiV8FgVldJO9BEu7dPI1-WR4KCmvFbgzZZRVk6f4lexI64tjWuEawpQ2w2kHIIa8INxMyjJwtOJG-byAnZUe9-WpHisTMUdh_xtj7tybi4yIK6U8uu81iKvuIASb2bT4DDyx7gXjPTks-Xl4YSiuNJh5e3njmh3fmz-q_sGm025MqJmQCEB2e_KYv-a8Xg5P4_inFawqJIyZllLVSpiVrOpjxqEg3ZNf5CEYgmIq-3yoeZtDDhIMGu10g6NfEZwUGuBT0_fmsl8ToJlYPPN573rOJdCK0cApx_PVFM5IrsxgN7Vto-5qiWgFpE2RuLwu6q5jZOMukK2Ra6cLCQKnQdp6bTSbRUGlhcx09IZ3Er5VBHha2JTWoqK2C5Q355YrBGbsLEofQ0KRUPwJ1lZst_mxmeoBfN0ihGYNcr0mwhR4Z6XErRZoGUSGnD93kWMyWko5mqk5Dp1Nhe8pWSSEFHFFBb7b5UWU5OhWqjBMtklnmk5AeLRUfs2jzKtmfIj6I2Zam4ftc5z8LqigIYJQ9HWpwx9o2pF8jZ8VmyAb3yVHUpxkifSgrTFiKsTVIBwmHvi0OTly6j8jz2OhdDQmGC5ipMGoxB2A9Cg_ufiVMUVcVNUwEJQhFeHpJTxedbyEMq6dD4Xiab67IeqB-hbW8FExeT_krz0Ph9JBGgT6A6d9-bU3FQCipVuxRuWm1c-Eco7EBk7c2kxU84Ami-1yXzE8UQGodRoK19eKHbfUfjX2oKsE8zGC4Lyb6oGATkvsxY5Udf2O1UfmRRnCrJwYAXvBM9mzlxmqxUNbsVdrpVNBr-VNLsVlBvmzJ_o7fk8kztf06WSRFiEtvvazeUrY7cRSq-J8qps5VeLcb_p3Zuuchy9IGWP1ZJ4pA86cp0HZW_DiMQM4p0wS0tu2o5PWayjorqW_EufoDwr-cMW7Pp5387gmsKg2Jvf9eINUbH7Fh5A5cSrG_EO8H0UCcxZ1aaVbqsmGaTihvZFqipcL2xJcRqKXLUeH0MWY3-yeAJQ-wKmm_NVOZFngbNxGA4u-4jML3dCNzwT4twIbmWx6ldt4R2cWReBJVJEyyBNnKKPz-y0Uudb318Cv5jx4ImBB3a8dku0tdhOWv3nPErizlonnuwNOVZ-oXqn9qK-yAGD2YaIGzwYV2334_reZ86xQ-3mxD7Dgvv3KxYffRde4AQNjPSYQfCbCv7WlT00L7NOKRrt9pxVI53i4ExOa7qJ36k2WRMkrwKotEV4jSlfDJ0S65rYC_J9Hirket1NjM4LElZFnUEJIE8uWPaQsFNHVEBbTbJlrNCrpNIJ728zqjP31BVPWcPAj0DiftVwMj9pCt2Y5xycERf3gpMYp3VL-O29bXwSkVkvI_ELNGMfBW25c8Uu4I5BmJycJxcMv7i4z7IJn_IklrxiEINbLGaePpi8-JK75mN0-fHdVBaOvUuy-z9bP1BvSvbtUeNmktLNzVitvROJ8tYa3GiyIlakuRu06IphVERYwAoJw4gnsaD6OMsvFPlV-j8fRHW2Xgho_4MGIgct4Dw-M5qJvplUjFj4lT-RGFVH3H_d3ZSXCrImwOdBOiq0umeKpokS8xnzJJ3VzKXcEN8Pw5gDNOywqpfi4gIMGdSuL3wxsiCCWG8nVbO2L4nLSX92M2Q5fJinAP5UsLhpL2VffqJgLNCAfUVdHzm8iOZWbEJEznq3YAR7fDf_a5ZmL9Z62CpyWQB1CWhcjXwEQKoGIGp3z40JDh_0bJ_x6QEdyBqkgji2PSPImnsWCFydaaRBt7xsKYQ7dW4EV1C_AftqV3mJ_xAj50owaU59ZzFFNZuxExz--VpbsRFFZe-Fv2pEalf6lNFaZv9SI83lI9SJCoPD56Q4vgUaKckHwYeisy4Iz1LR-rYFiAN_2WQ5z2JAuQi0o7iiKkKdNCm1zNLPwN7wjpfmb476XYluDzBS1kBxvwDjPA6_Ph_BuHt2I0zNoMGjnE2sW0CB3Exu_WfoZl3MUCVgmaA3ycf624wepMl0thKlxW7lG_4Qe09ISrNAhKYfSlSgC6tajfU5pKrxjsCB52E_d6VOwzkUf3-JMyUImYTYcPs8iIrsrYXaLS7RbOMioeGsQnPHHmnpY07Ee0zidABj5j9s6MrRI--L5issTm8EjuG-Q93FMBOZ8pTwwDL_NBXICDnLXHC9WX4d5E6jt2z_LPKbe0Xw3zN6ZDSrLcipywE_7cdl7Ad9m3s2xr_cRWATS9UoNaEbUUieQ82HSORJeDflVTCxdZlt06EKaF8DtGACVhLNzNwx-GzlVmkMH2sikL8R1tfaZ84DOSPfbOs_GQK5QDQVJdlW4g5dGRFrha6V8pcbylOBxrJzQKsf2JUgqhvtrAeiXDDKNgYAXjgAzuwwKN1aWEv9en9L3RNc_QEm1JMIKfr_QOOHA-RxCUWuON1wUfiHIg7KnsSGvAyGXii3PBK2Ut1lGuxwHjQ5clmTOkAtbo-uN9N3LL39zp2YYOzeY-yCcLgM7uSA-obR3Zca9SB0o94Q_H43AdPkt7UQvaZNhgXyUkEkw1VtTUrGaUtnx_9oIWxHCMPEt3yLJLv50zTMEjj8FfhANN60Sq3pntDAMCtHIsaRyJH5hL_-n9pXHzJDFvnPFAWzZ8PmKTiygjs8qv0u3N7CM5UqSlQC-V0sJaqlwi_nZaCjdjH_Oj4iLUyQbV23Z_HF7u9rewatAVzZ66wjDLRiE9OkF5Q_-UFuElhthEtgzB2Adavmo_XjWLvi45TjLR6ve5vUq3JP7st6PA_Rj29IchgIYcT4Nw2f3lRb76IxqeGfC3MW6j3m4GCqhdz2XqC7ZNdmkfQkDna_lAqB2Jgi6Yv0N2ItpKzTqai-lea0ucQ4rgyNRIkKYqpkBFtLDp2rZJtlU82FRMj_-F8eIx2bwvG7llEGQwkwJu6_y2iEzo7RSd3zpbZJrLK-hUquFJ3wMt48Tst5cNXHRiFfrR6Ru36EjlFMAVsuT_NpUivIZOoKUs5Ud2EVTZpkuozTdNTgunzd2UkC3OeVo-wlQslhfr7-0VP_zJ7rs-tbjqzBtzqrvu3JDJQ6BhW89BGN77hxcBOjUzvXgBDtdzqXolXfRH1zwhx45s9tWLozEbtxdF0hCSdmawF-DY_zzvHPLFKMMLJsbbXKzXLuS4lUq1uypPPq7LdNv9LcOXu7uiqxIlr2hDcGHptwXI_2Z3ZBoEW0UoMUVyINi5oe3JeNa3A2CIoMEkisFUCKQMYUbXFwXRsD8lK2Ti6mTxIMEcrrsUfYU7UdxFra-0CwTpyfm-DIPjc_NG_s5DwXNnndHT-vYflkwTxtLBU5QmVV8NOtptVMx00MTYFJ7WlhSy2hAFaN13HqizZ8NM4VofIDRIoaeqSSLn1PPM87nsV7n-8toCHPkVU0FY2yrG7OUjfAF7OUtwKwZVUCKcFIChH9OTt9REa-LfdiONkkidr-Z9JsASaVskNtpJ5TpaBdFiIAeX4y_2C8_jjxIe3md5GSBTgtfFUWZXbItzHgUmavCCynR4hy0dObAg5JBTrUXJYNn_kAXv0ZY3-6onjyZspsvOvOjwVM75KNM1nlLxqg2kVePJi7tVShF-W0lc8EMKRHtjchL_Oahw1q8xL3HdrtKeRi1MGzBcFkPQ_oWVMupx3UMuBmjiiZ_TanyJu6UCxPlsiUIplxhDdkur5vbH4AvNch8DkjQkL46EWhosybeFZQFaQtze5kR6-D2SbwboW4jo_YyAI0VbjF3v5-yJazuabQGiRKtGvjBawTnjlit6qoWUs-tUx3RaJL9ldkSHR3t0Fch4Dzxm55GBg3vbvrRpSWC0bqo45nUEnxsnmwkB9Vy-4pVO95lHdwpJsa5ELmxJvITImcNcbsPH_gGCzxK5ImdodGMVjeh7ofhue1MzP1j-NvTlY8xV2M_Buzl57D1pwGjUFa7NJvSjmrbV1jllOGrW0yfW7OaH9U7lXEf1sJL-xG0HCcfc-lo5PD0zUFpBZZMWNOTadYz3WzkRmy-TUFFpNZYWJlHjOw4kS0Y4yugVYEK1anTfmKq7oaw_uXPp1eHp0yTnxuPAIiDKb4VXhWUtb01M8SPQnPCtuZV8gRKjonSaDgCoRikJW7wgO4z9si8CP8UN9nafRjOfiRsUGgg92a_wv0WVZ33M2iec8joKqEsEwM09etwK5GA6xQ0qu1meAMMdB5j6TD0bt_39ajbmMDhZRtpooDtIwznmOnvQwwiq-FU4BS3oVFwbcX1oZT59BGNapYXvFdgE2G-cNTgn8xfyLonTNexHK_dBmwiIyjyuYQ2NV-HgiXoyhY1h1UqUr3hwW3ScB84NL_aMogIgc6BD9NF7GOMauuXkzlA7b_OyuyI94xIkjftWr3MqTBl38NRTHvK1fX6eLfOF58XAeFbHNDYH3r8HgxvoFYvSRB4VcFKBcXYX2DGgIj2BJkPujeZQ0LxRe_Lj5OXU5bHoeZM2DWHPn6SOfzpmkc28y5pyZLq14W6o3hweG8a09ezARd298Bf0XG3w0uAt70ykpPAmz8ye0FowqF4nUk0vpVSB3jnPv5PGbY-sc3twosmjoW7CgjBFtba-gvCm-dJao_SIPh9l09kbKxgfGBiWYG1L0Sm1oWZ5o97G5g08hHxIc4uZHr19pngw0ZJWCuHOnS6BalVuu0DG2Dtz8SSR9n6ZlK8A9benN7EG-G8ZWES28eDYY35_P6V5fyXk_tZI2vG8e0kG2AgFHG6RxzDIBK8XILPn2QY8g5ok1LiX6U3fu8pI0q4HOBbf3hNFzqo8HSs_gdWsVoBjU8audJm2sa4IOdJ8uhBYDI8qYvxsECa4r35bLi4g-eLJueEIvMY4WD8HLjLH1MqKXD0XKH6aJ8VLzVHCb4QUWom8YJ2wUeJ1uWnO4k4AhOxwPmYW0D0ZHmJrk4oGrJXCY2AEx_28GemcF3CtnhMmH81oW2Om4pTUTQHCb0AQu2dK8e8cCZuxuxAe8Zm77ULHMXaT0CNbGLyT7mYNfZuHAFKnr6PXiDZGkME6bm0-EudoaYqo0JGR50iMzyYgX8LARka8emkM6Qk6OgmkQ6BK9aGpHoNrL4I27Kl-ug8YEGYTwF2nN7zLOHc0QOuTkn8aOMHX4R1pR3Tt95Z1gC8YWMAFqBeI8k0XVtlL0H6uMDVTFLdngNvBFqX36I8MbxMP8aSfwU7drrb7xvvOz_kIr0zcwpBm326xv3sJiC6Vllht_wyajPHTi2BEHgVstajSU9sdtcIym-FIKzJhUYQAF-T4-IPEJk9ukRk6GEvXQxtzBq21-blGF5qsDnlPbPjad18BF4SRLB0F-fEuVIOuktgB9nb9clhIvL6ohs5gXz8ncw8c6sGFRbC3sPsWgReGVrcmUKhUaHePB9zmuert1BJils66sgPfupX5ClMfBD4wZ9e_Maq9atbLRXQ5ppvsBRF5kDcsannpwqLtir6TnzP9FIxkbgl9GDdFfEMMirdbg_c6BSLLsD-t1FjvaFzUiGtkr2k_GuPNkO8L0WnjlIJNTtlLPcYf7qRg-OEPAhTu_hMs3j9njP_foIilVE8WbwXquBxyKgsnZUXU2mZ6Nln2VfCLc5ApDrhnnCZ-jJywq7JkRMzH91pUoulqCwRMRvfbUYp4dZCgpeUsJolqbN8vnpnw9JMCMgKMpnA-aTaH_0mkD60Z474RKswizuvKN2E78wmfZefQoMmZb72HzuxkH4HHzkbj7jUNyw0yl52-KqfFcAXLlRGtaCv6oxLf0_wwdTSkBEJATWvYfl9fIE8xK3ZRJhvFBQLbkFDgv2bWoToBpkn6ylYJRQqytjLk0-mS_5APIyKq85SlDeKxDSXr5_gNQFtNQq-YfIw4xNCFFEuLh2i-71JEB9ykb-GtcJ4YfYMg0OckPBnmumXhOOvWv2mS8CUL6RdYyrZtPFxHLnK-b3YTgjheGrMtb5A-KqI_FXxfbJCQYqRJve0OFPw8Vyjr_2zD9PTANj0XJYEXQLxL2HpqF3cB2rxcpTluAfzHTWYB5DbBp1SAF-ktvF57LP9BlnUvBqug3-J8VEIHDUvC2ZVdHr5VxcyGO6V_FH07_xbEYWUP7h_SwFjXur3hLLlBeORJnRBzlqYZhUYyDNTVJS4RKAfjyQe3eWhpfOC1K795Jzp5yeokfdSuh8ndni8cTMrP_DaQmz6xYPHAjVTGE5KxGtSYWVooXQIfjrDEVBMU4StY4Cyquz2vKCsg4sIWDVz9uauTxtOBnYqmzRpALljk56Ryf4prRvaxLE0RRoNCXyMlC8fs_ItcWyM68T3XfYZwqtZrDEqNOBASoVFcow7ud7EN-pXW7xV4LZ8EYTpxNfFspVV1mHsrFwN89QLHhtHGvQxbFibWZKQl9THz01KAtRw_CVRRFmrbeiJtkGn80EgOSR7Qu3IE7aFiLr9HLjDOADzPWXXLX9YB4EujSpLX9ag2MShFKpoLPfWcr7qASD5lCFyATB5zV4UQBMsbEBZlHcSCAneYIZbiejouYXMYhDP1FPifWlh2w9ImIsSKgLZ1mbja02fwqm7mtqrpMb6C31oTSur6cjqOffH3SZOzUgShY7IssSrALAh9DsLayYOQFKBviqNoO_6V01UNO9w7EBQnE5iEedNGezFEskHo1nNRPf_-iYaOue-OhXTc8NJiRS5yap6AxhoP6ikKJ9uDQ5oAUNA8q4oxNr7xD4dKxARzpv-QNykkYhzlqo1Aj_aduKojAO5cc9FJWZMtEPcUshC-bKA-d5T1Q5WnfBSD4B2odBOPSdRTXdZIZKfXUEIZIWvsUcLKol9jHFJRDvN0Oo5HeZPwPY7g_6nzUfAuT-5Zrt9g0krDT69VLH5REDb19QqB2j49j1synETfHrepwBReTlJR01DvIISnXgg9B-U1FEGJkfI2Ezxqfdq6pyaKhytQGxKa959Ed6yRMMQzFw__55silvEbSO3p5tB2bUBe4FusNyXh7TBaPpQ1M1oQE_Wc0EBvVNsF7fUurgQyIoRHMKg7LArfDWsjtXB2rHsXO5KsktzDQziTxm8IWcwboFsQQ2YTTDI-5QuRRYwgncTQ--M6jrdeEBjfmoE6idTVkjjrc0Yt-cjWO4SpLKhMQXwPg-epTGvUpDqX8_MRIHQcW_JzI8iSRM0a6-MLJMbY-aRqywT1xcP6CP7xcKlU8xiQou08o_tUTYivv8PD7QoGbbu79U6nPzppMKkbzeHC7b4n09vUzQJQMuzLTJpQ5jh-dfQ1jFmccx4iKryNwUE9JwM2lW5L0Vd8mh5WFpy50kvcYZIEk3JIYzxZGKiQYTGhLwdxMbmGJjiNNJZR2pQ3cCPaTsiSLqdK7GShYt7YJDnlE5ErJkQpBzNHApMBIlDA77xrlwJQYXAPdOGOkFqjyrdeHp1yzcZ6myCMP-ni0sL7_I3k-K-UpOqYDQtjpi3Hsijsv_5JRwilq3iV32RLsMffV4GdQtBKDk2K5h3RKTBbb0s4WHhqfw8EJzJyHtf6fMP9lblS9LvJ62hNhS5AtBo4kfdHluvkf3vhCxC40dwe2bz5zM9p-O2bcNZH6ukjvHKcrtFHzpOpxk-MlcSxUpv11yWDr8pUcDaAD2k3OgphLMXPWzrEiElMcCReXZxHnRzlAhuJrJegPp6kxUIpMtlp3_RL9pMGpi5rfgHCnmzafkobRVWJ2lWzchhESRcpg-7jp4jZYgONifyhdqy706V2dyJH3Jp6_E5LJB0BRHVlzkhEA8TKHGkYvRbJWWnJ8lY-eHrBdVfeWqmSwvLDIt9Sa_8CwhiaZ-7m00
\ No newline at end of file
diff --git a/docs/logical_data_model.puml b/docs/logical_data_model.puml
index ef38c648e2..7960a64b0d 100644
--- a/docs/logical_data_model.puml
+++ b/docs/logical_data_model.puml
@@ -2559,8 +2559,8 @@ MonitoringReviewLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringReview
MonitoringReviewLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringReviews : monitoringReviews, monitoringReviewLink
MonitoringReviewStatusLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringReviewStatuses : monitoringReviewStatuses, statusLink
MonitoringReviewStatusLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringReviews : monitoringReviews, statusLink
-MonitoringStandardLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringFindingStandards : statusLink, monitoringFindingStandards
-MonitoringStandardLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringStandards : monitoringStandardes, statusLink
+MonitoringStandardLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringFindingStandards : standardLink, monitoringFindingStandards
+MonitoringStandardLinks "1" --[#black,dashed,thickness=2]--{ "n" MonitoringStandards : monitoringStandards, standardLink
NationalCenters "1" --[#black,dashed,thickness=2]--{ "n" EventReportPilotNationalCenterUsers : nationalCenter, eventReportPilotNationalCenterUsers
NationalCenters "1" --[#black,dashed,thickness=2]--{ "n" NationalCenterUsers : nationalCenter, nationalCenterUsers
NationalCenters "1" --[#black,dashed,thickness=2]--{ "n" NationalCenters : mapsToNationalCenter, mapsFromNationalCenters
From dcf1b41e2a80ecfb3f71068336a195688c82b16f Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Thu, 5 Dec 2024 18:59:38 -0500
Subject: [PATCH 062/198] fixes cleanup and FE test
---
frontend/src/fetchers/activityReports.js | 5 +-
frontend/src/fetchers/citations.js | 5 +-
.../Pages/components/GoalPicker.js | 3 +-
.../Pages/components/__tests__/GoalPicker.js | 72 +++++++++++++++++
.../ActivityReport/Pages/goalsObjectives.js | 2 +-
.../pages/ActivityReport/__tests__/index.js | 8 +-
src/goalServices/goals.js | 30 +------
src/goalServices/goals.test.js | 39 ---------
src/routes/activityReports/handlers.js | 4 +-
src/services/citations.js | 81 +------------------
src/services/citations.test.js | 30 +------
src/services/reportCache.js | 5 +-
12 files changed, 93 insertions(+), 191 deletions(-)
diff --git a/frontend/src/fetchers/activityReports.js b/frontend/src/fetchers/activityReports.js
index ab8292a54b..6f98a9dcd8 100644
--- a/frontend/src/fetchers/activityReports.js
+++ b/frontend/src/fetchers/activityReports.js
@@ -108,9 +108,8 @@ export const getRecipientsForExistingAR = async (reportId) => {
return recipients.json();
};
-export const getGoals = async (grantIds, reportStartDate = null) => {
- const reportStartDateParam = reportStartDate ? `&reportStartDate=${new Date(reportStartDate).toISOString().split('T')[0]}` : '';
- const params = grantIds.map((grantId) => `grantIds=${grantId}${reportStartDateParam}`);
+export const getGoals = async (grantIds) => {
+ const params = grantIds.map((grantId) => `grantIds=${grantId}`);
const url = join(activityReportUrl, 'goals', `?${params.join('&')}`);
const goals = await get(url);
return goals.json();
diff --git a/frontend/src/fetchers/citations.js b/frontend/src/fetchers/citations.js
index 9dbef2f32e..f7747dd6bd 100644
--- a/frontend/src/fetchers/citations.js
+++ b/frontend/src/fetchers/citations.js
@@ -6,13 +6,14 @@ import {
export async function fetchCitationsByGrant(region, grantIds, reportStartDate) {
const formattedDate = new Date(reportStartDate).toISOString().split('T')[0];
- const citations = await get(join(
+ const url = join(
'/',
'api',
'citations',
'region',
String(region),
`?grantIds=${grantIds.join('&')}&reportStartDate=${formattedDate}`,
- ));
+ );
+ const citations = await get(url);
return citations.json();
}
diff --git a/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js b/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js
index e54d781087..418df00e32 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js
@@ -119,9 +119,10 @@ const GoalPicker = ({
async function fetchCitations() {
// If its a monitoring goal and the source is CLASS or RANs, fetch the citations.
if (goalForEditing && goalForEditing.source === 'Federal monitoring issues, including CLASS and RANs') {
+ const monitoringGrantIds = goalForEditing.goals.map((g) => g.grantId);
const retrievedCitationOptions = await fetchCitationsByGrant(
regionId,
- goalForEditing.grantIds,
+ monitoringGrantIds,
startDate,
);
if (retrievedCitationOptions) {
diff --git a/frontend/src/pages/ActivityReport/Pages/components/__tests__/GoalPicker.js b/frontend/src/pages/ActivityReport/Pages/components/__tests__/GoalPicker.js
index e3cf2cbc7e..c16318e9d2 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/__tests__/GoalPicker.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/__tests__/GoalPicker.js
@@ -34,6 +34,8 @@ const GP = ({ availableGoals, selectedGoals, goalForEditing, goalTemplates }) =>
const hookForm = useForm({
mode: 'onChange',
defaultValues: {
+ startDate: '2024-12-03',
+ regionId: 1,
goals: selectedGoals,
goalForEditing,
author: {
@@ -346,4 +348,74 @@ describe('GoalPicker', () => {
expect(input.value).toBe(availableGoal.value.toString());
});
});
+ describe('monitoring goals', () => {
+ it('correctly retrieves citations for monitoring goals', async () => {
+ fetchMock.get('/api/goal-templates/1/prompts?goalIds=1', []);
+ fetchMock.get('/api/goal-templates/1/source?grantIds=1', {
+ source: 'Federal monitoring issues, including CLASS and RANs',
+ });
+
+ fetchMock.get('/api/citations/region/1?grantIds=1&reportStartDate=2024-12-03', [
+ {
+ citation: 'test citation 1',
+ grants: [
+ {
+ acro: 'DEF',
+ citation: 'test citation 1',
+ findingId: 1,
+ findingSource: 'source',
+ findingType: 'Deficiency',
+ grantId: 1,
+ grantNumber: '123',
+ monitoringFindingStatusName: 'Active',
+ reportDeliveryDate: '2024-12-03',
+ reviewName: 'review name',
+ severity: 1,
+ },
+ ],
+ standardId: 1,
+ },
+ ]);
+
+ const availableGoals = [{
+ label: 'Monitoring Goal',
+ value: 1,
+ goalIds: [1],
+ isCurated: true,
+ goalTemplateId: 1,
+ source: 'Federal monitoring issues, including CLASS and RANs',
+ goals: [
+ {
+ grantId: 1,
+ },
+ ],
+ }];
+
+ act(() => {
+ renderGoalPicker(availableGoals, null);
+ });
+
+ const selector = await screen.findByLabelText(/Select recipient's goal*/i);
+ const [availableGoal] = availableGoals;
+
+ await act(async () => {
+ await selectEvent.select(selector, [availableGoal.label]);
+ });
+
+ const input = document.querySelector('[name="goalForEditing"]');
+ expect(input.value).toBe(availableGoal.value.toString());
+
+ // Select 'Create a new objective' from the dropdown.
+ const objectiveSelector = await screen.findByLabelText(/Select TTA objective/i);
+ await selectEvent.select(objectiveSelector, 'Create a new objective');
+
+ // Open the citations dropdown.
+ const citationSelector = await screen.findByLabelText(/citation/i);
+ await selectEvent.select(citationSelector, /test citation 1/i);
+
+ // Check that the citation is displayed.
+ const citation = await screen.findByText(/test citation 1/i);
+ expect(citation).toBeVisible();
+ });
+ });
});
diff --git a/frontend/src/pages/ActivityReport/Pages/goalsObjectives.js b/frontend/src/pages/ActivityReport/Pages/goalsObjectives.js
index 6d74f56e9e..48ddf6e494 100644
--- a/frontend/src/pages/ActivityReport/Pages/goalsObjectives.js
+++ b/frontend/src/pages/ActivityReport/Pages/goalsObjectives.js
@@ -165,7 +165,7 @@ const GoalsObjectives = ({
const fetch = async () => {
try {
if (isRecipientReport && hasGrant) {
- const fetchedGoals = await getGoals(grantIds, startDate);
+ const fetchedGoals = await getGoals(grantIds);
const formattedGoals = fetchedGoals.map((g) => {
// if the goal is on an "old" grant, we should
// treat it like a new goal for now
diff --git a/frontend/src/pages/ActivityReport/__tests__/index.js b/frontend/src/pages/ActivityReport/__tests__/index.js
index 370d57a1e6..de3797b921 100644
--- a/frontend/src/pages/ActivityReport/__tests__/index.js
+++ b/frontend/src/pages/ActivityReport/__tests__/index.js
@@ -752,7 +752,7 @@ describe('ActivityReport', () => {
});
fetchMock.get('/api/topic', [{ id: 64, name: 'Communication' }]);
- fetchMock.get('/api/activity-reports/goals?grantIds=10431&reportStartDate=2012-05-20', [{
+ fetchMock.get('/api/activity-reports/goals?grantIds=10431', [{
endDate: null,
grantIds: [10431],
goalIds: [37502],
@@ -765,7 +765,7 @@ describe('ActivityReport', () => {
source: null,
isCurated: false,
}]);
- fetchMock.get('/api/goal-templates?grantIds=10431', []);
+ fetchMock.get('/api/goal-templates?grantIds=10431&reportStartDate=2012-05-20', []);
fetchMock.get('/api/activity-reports/1', {
...formData(),
activityRecipientType: 'recipient',
@@ -955,8 +955,8 @@ describe('ActivityReport', () => {
it('you can add a goal and objective and add a file after saving', async () => {
const data = formData();
fetchMock.get('/api/topic', [{ id: 64, name: 'Communication' }]);
- fetchMock.get('/api/activity-reports/goals?grantIds=12539&reportStartDate=2012-05-20', []);
- fetchMock.get('/api/goal-templates?grantIds=12539', []);
+ fetchMock.get('/api/activity-reports/goals?grantIds=12539', []);
+ fetchMock.get('/api/goal-templates?grantIds=12539&reportStartDate=2012-05-20', []);
fetchMock.put('/api/activity-reports/1/goals/edit?goalIds=37504', {});
fetchMock.get('/api/activity-reports/1', {
...data,
diff --git a/src/goalServices/goals.js b/src/goalServices/goals.js
index 38ad17b309..eb3531a483 100644
--- a/src/goalServices/goals.js
+++ b/src/goalServices/goals.js
@@ -40,7 +40,6 @@ import {
destroyActivityReportObjectiveMetadata,
} from '../services/reportCache';
import { setFieldPromptsForCuratedTemplate } from '../services/goalTemplates';
-import { getMonitoringGoals } from '../services/citations';
import { auditLogger } from '../logger';
import {
mergeCollaborators,
@@ -692,7 +691,7 @@ export async function createOrUpdateGoals(goals) {
return goalsByIdAndRecipient(goalIds, recipient);
}
-export async function goalsForGrants(grantIds, reportStartDate, user) {
+export async function goalsForGrants(grantIds) {
/**
* get all the matching grants
*/
@@ -735,10 +734,10 @@ export async function goalsForGrants(grantIds, reportStartDate, user) {
.filter((g) => g)));
/*
- * Get all matching goals
+ * finally, return all matching goals
*/
- const regularGoals = await Goal.findAll({
+ return Goal.findAll({
attributes: [
[sequelize.fn(
'ARRAY_AGG',
@@ -836,29 +835,6 @@ export async function goalsForGrants(grantIds, reportStartDate, user) {
),
), 'desc']],
});
-
- /*
- * Get all monitoring goals
- */
- let goalsToReturn = regularGoals;
- const hasGoalMonitoringOverride = !!(user && new Users(user).canSeeBehindFeatureFlag('monitoring_integration'));
- if (hasGoalMonitoringOverride && reportStartDate) {
- const monitoringGoals = await getMonitoringGoals(ids, reportStartDate);
- // Combine goalsToReturn with monitoringGoals.
- const allGoals = await Promise.all([regularGoals, monitoringGoals]);
-
- // Flatten the array of arrays.
- goalsToReturn = allGoals.flat();
-
- // Sort goals by created date desc.
- goalsToReturn.sort((a, b) => {
- if (a.created < b.created) {
- return 1;
- }
- return -1;
- });
- }
- return goalsToReturn;
}
async function removeActivityReportObjectivesFromReport(reportId, objectiveIdsToRemove) {
diff --git a/src/goalServices/goals.test.js b/src/goalServices/goals.test.js
index 8cd85ec64c..21e8912329 100644
--- a/src/goalServices/goals.test.js
+++ b/src/goalServices/goals.test.js
@@ -50,7 +50,6 @@ import {
import {
mergeCollaborators,
} from '../models/helpers/genericCollaborator';
-import { getMonitoringGoals } from '../services/citations';
jest.mock('./changeGoalStatus', () => ({
__esModule: true,
@@ -1525,44 +1524,6 @@ describe('Goals DB service', () => {
506,
]);
});
-
- it('does not return monitoring goals if the user is missing the feature flag', async () => {
- Grant.findAll = jest.fn();
- Grant.findAll.mockResolvedValue([{ id: 505, oldGrantId: 506 }]);
- Goal.findAll = jest.fn();
- Goal.findAll.mockResolvedValue([{ id: 505 }, { id: 506 }]);
-
- await goalsForGrants([506]);
-
- const { where } = Goal.findAll.mock.calls[0][0];
- expect(where['$grant.id$']).toStrictEqual([
- 505,
- 506,
- ]);
- });
-
- it('returns monitoring goals if the user has the feature flag', async () => {
- Grant.findAll = jest.fn();
- Grant.findAll.mockResolvedValue([{ id: 505, oldGrantId: 506 }]);
- Goal.findAll = jest.fn();
- Goal.findAll.mockResolvedValue([{ id: 505 }, { id: 506 }]);
-
- // Mock getMonitoringGoals to return a list of monitoring goals.
- getMonitoringGoals.mockResolvedValue([{ id: 507 }]);
-
- // Mock the feature flag function canSeeBehindFeatureFlag in user to return true.
- const reportStartDate = new Date().toISOString().split('T')[0];
- const result = await goalsForGrants([506], '2024-11-27', {
- flags: ['monitoring_integration'],
- });
-
- // Assert result contains the goals we expect including the monitoring goal.
- expect(result).toEqual(expect.arrayContaining([
- { id: 506 },
- { id: 507 },
- { id: 507 },
- ]));
- });
});
describe('createMultiRecipientGoalsFromAdmin', () => {
diff --git a/src/routes/activityReports/handlers.js b/src/routes/activityReports/handlers.js
index 1b0e94086e..db6521d2bf 100644
--- a/src/routes/activityReports/handlers.js
+++ b/src/routes/activityReports/handlers.js
@@ -293,10 +293,10 @@ export async function getLegacyReport(req, res) {
*/
export async function getGoals(req, res) {
try {
- const { grantIds, reportStartDate } = req.query;
+ const { grantIds } = req.query;
const userId = await currentUserId(req, res);
const user = await userById(userId);
- const goals = await goalsForGrants(grantIds, reportStartDate, user);
+ const goals = await goalsForGrants(grantIds);
res.json(goals);
} catch (error) {
await handleErrors(req, res, error, logContext);
diff --git a/src/services/citations.js b/src/services/citations.js
index 6b88fe45fc..4386c68315 100644
--- a/src/services/citations.js
+++ b/src/services/citations.js
@@ -4,17 +4,13 @@ import { sequelize } from '../models';
// TODO: Update this to the day we deploy to PROD.
const cutOffStartDate = new Date().toISOString().split('T')[0];
+// const cutOffStartDate = '2021-01-01';
/*
The purpose of this function is to get citations by grant id.
We then need to format the response for how it needs to be
displayed on the FE for selection on objectives.
*/
export async function getCitationsByGrantIds(grantIds, reportStartDate) {
- /*
- Questions:
- - Do we need to take into account the grant replacements table? (what if a grant was replaced?)
- - Is it enough to join on grant number? Or do we need to use links table?
- */
// Query to get the citations by grant id.
const grantsByCitations = await sequelize.query(
/* sql */
@@ -90,78 +86,3 @@ export async function getCitationsByGrantIds(grantIds, reportStartDate) {
return grantsByCitations[0];
}
-
-/* Get the monitoring goals for the given grant ids and report start date */
-/* We need to produce the same objects that come for the goals endpoint */
-export async function getMonitoringGoals(grantIds, reportStartDate) {
- const monitoringGoals = await sequelize.query(
- /* sql */
- `SELECT
-
- g.id,
- g."name",
- g."status",
- g."endDate",
- g."onApprovedAR",
- g."source",
- g."createdVia",
-
- ARRAY_AGG(DISTINCT gr.id) AS "grantIds",
- ARRAY_AGG(DISTINCT g.id) AS "goalIds",
- ARRAY_AGG(DISTINCT grta."grantId") AS "oldGrantIds",
- MAX(g."createdAt") AS created,
- MAX(g."goalTemplateId") AS "goalTemplateId"
-
- FROM "Grants" gr
- JOIN "MonitoringReviewGrantees" mrg
- ON gr.number = mrg."grantNumber"
- JOIN "GrantRelationshipToActive" grta
- ON gr.id = grta."grantId"
- JOIN "Goals" g
- ON gr.id = g."grantId"
- JOIN "GoalTemplates" gt
- ON g."goalTemplateId" = gt.id
- JOIN (
- -- The below 'DISTINCT ON' determines the single record to return values from by the 'ORDER BY' clause.
- SELECT DISTINCT ON (mfh."findingId", gr.id)
- mfh."findingId",
- gr.id AS "grantId",
- mr."reviewId",
- mr."name",
- mr."reportDeliveryDate"
- FROM "MonitoringFindingHistories" mfh
- JOIN "MonitoringReviews" mr
- ON mfh."reviewId" = mr."reviewId"
- JOIN "MonitoringReviewGrantees" mrg
- ON mrg."reviewId" = mr."reviewId"
- JOIN "Grants" gr
- ON gr.number = mrg."grantNumber"
- ORDER BY mfh."findingId", gr.id, mr."reportDeliveryDate" DESC
- ) mfh -- Subquery ensures only the most recent history for each finding-grant combination
- ON mfh."grantId" = gr.id
- JOIN "MonitoringFindings" mf
- ON mfh."findingId" = mf."findingId"
- JOIN "MonitoringFindingStatuses" mfs
- ON mf."statusId" = mfs."statusId"
- JOIN "MonitoringFindingStandards" mfs2
- ON mf."findingId" = mfs2."findingId"
- JOIN "MonitoringStandards" ms
- ON mfs2."standardId" = ms."standardId"
- JOIN "MonitoringFindingGrants" mfg
- ON mf."findingId" = mfg."findingId"
- AND mrg."granteeId" = mfg."granteeId"
- WHERE 1 = 1
- AND gr.id IN (${grantIds.join(',')}) -- :grantIds
- AND mfh."reportDeliveryDate"::date BETWEEN '${cutOffStartDate}' AND '${reportStartDate}'
- AND gr.status = 'Active'
- AND mfs.name = 'Active'
- AND g."createdVia" = 'monitoring'
- AND g.status NOT IN ('Closed', 'Suspended')
- AND gt.standard = 'Monitoring'
- AND g.name != ''
- GROUP BY 1, 2, 3, 4, 5, 6, 7
- ORDER BY 11 DESC;
- `,
- );
- return monitoringGoals[0];
-}
diff --git a/src/services/citations.test.js b/src/services/citations.test.js
index 660338729f..df6b3ac65b 100644
--- a/src/services/citations.test.js
+++ b/src/services/citations.test.js
@@ -2,7 +2,7 @@
/* eslint-disable prefer-destructuring */
import { v4 as uuidv4 } from 'uuid';
import faker from '@faker-js/faker';
-import { getCitationsByGrantIds, getMonitoringGoals } from './citations';
+import { getCitationsByGrantIds } from './citations';
import db, {
Recipient,
Grant,
@@ -420,32 +420,4 @@ describe('citations service', () => {
expect(citation3.grants[0].reportDeliveryDate).toBeDefined();
expect(citation3.grants[0].findingType).toBe('Citation 4 Monitoring Finding Type');
});
-
- it('getMonitoringGoals', async () => {
- const goalsToAssert = await getMonitoringGoals([grant1.id, grant1a.id], new Date().toISOString().split('T')[0]);
- expect(goalsToAssert.length).toBe(2);
-
- // Assert the goals.
- // Assert the monitoring goal.
- const monitoringGoalToAssert = goalsToAssert.find((g) => g.id === monitoringGoal.id);
- expect(monitoringGoalToAssert).toBeDefined();
- expect(monitoringGoalToAssert.name).toBe('Monitoring Goal 1');
- expect(monitoringGoalToAssert.status).toBe('Not started');
- expect(monitoringGoalToAssert.grantIds).toStrictEqual([grant1.id]);
- expect(monitoringGoalToAssert.created).toBeDefined();
- expect(monitoringGoalToAssert.onApprovedAR).toBe(true);
- expect(monitoringGoalToAssert.createdVia).toBe('monitoring');
- expect(monitoringGoalToAssert.goalTemplateId).toBe(monitoringGoal.goalTemplateId);
-
- // Assert the grant1a monitoring goal.
- const monitoringGoalToAssert1a = goalsToAssert.find((g) => g.id === grant1aMonitoringGoal.id);
- expect(monitoringGoalToAssert1a).toBeDefined();
- expect(monitoringGoalToAssert1a.name).toBe('Monitoring Goal 3');
- expect(monitoringGoalToAssert1a.status).toBe('Not started');
- expect(monitoringGoalToAssert1a.grantIds).toStrictEqual([grant1a.id]);
- expect(monitoringGoalToAssert1a.created).toBeDefined();
- expect(monitoringGoalToAssert1a.onApprovedAR).toBe(true);
- expect(monitoringGoalToAssert1a.createdVia).toBe('monitoring');
- expect(monitoringGoalToAssert1a.goalTemplateId).toBe(monitoringGoal.goalTemplateId);
- });
});
diff --git a/src/services/reportCache.js b/src/services/reportCache.js
index 0034492cac..cfab4881d6 100644
--- a/src/services/reportCache.js
+++ b/src/services/reportCache.js
@@ -148,6 +148,7 @@ const cacheTopics = async (objectiveId, activityReportObjectiveId, topics = [])
Citations to remove will be determined by id.
*/
export const cacheCitations = async (objectiveId, activityReportObjectiveId, citations = []) => {
+ let newCitations = [];
// Delete all existing citations for this activity report objective.
await ActivityReportObjectiveCitation.destroy({
where: { activityReportObjectiveId },
@@ -156,13 +157,12 @@ export const cacheCitations = async (objectiveId, activityReportObjectiveId, cit
});
// Create citations to save.
- let newCitations = [];
if (citations && citations.length > 0) {
newCitations = citations.map((citation) => (
{
- ...citation,
activityReportObjectiveId,
citation: citation.citation,
+ monitoringReferences: citation.monitoringReferences,
}));
// If we have citations to save, create them.
@@ -170,7 +170,6 @@ export const cacheCitations = async (objectiveId, activityReportObjectiveId, cit
return ActivityReportObjectiveCitation.bulkCreate(newCitations, { individualHooks: true });
}
}
-
return newCitations;
};
From 96935490d906d7f88e541ee61d1cf0e70793075c Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Thu, 5 Dec 2024 19:31:20 -0500
Subject: [PATCH 063/198] update test
---
src/services/reportCache.test.js | 82 +++++---------------------------
1 file changed, 12 insertions(+), 70 deletions(-)
diff --git a/src/services/reportCache.test.js b/src/services/reportCache.test.js
index 2f9b4cbbf2..0ccb3c743e 100644
--- a/src/services/reportCache.test.js
+++ b/src/services/reportCache.test.js
@@ -204,36 +204,19 @@ describe('activityReportObjectiveCitation', () => {
reviewName: 'Review 1',
}]);
- // Assert deleted.
- expect(result[2]).toBeUndefined();
-
// Update the ActivityReportObjectiveCitation.
const citationsToUpdate = [
{
- id: createdAroCitations[0].id,
- citation: 'Citation 1 UPDATED',
+ id: citation1Id,
+ citation: 'Citation 1 Updated',
monitoringReferences: [{
grantId: 1,
findingId: 1,
- reviewName: 'Review 1',
- },
- {
- grantId: 2,
- findingId: 2,
- reviewName: 'Review 2',
- }],
- },
- {
- citation: 'Citation 2',
- monitoringReferences: [{
- grantId: 3,
- findingId: 3,
- reviewName: 'Review 3',
+ reviewName: 'Review 1 Updated',
}],
},
];
- // Save updated ActivityReportObjectiveCitation.
result = await cacheCitations(objective.id, aro.id, citationsToUpdate);
// Assert updated.
@@ -245,68 +228,27 @@ describe('activityReportObjectiveCitation', () => {
},
});
- expect(updatedAroCitations).toHaveLength(2);
- expect(updatedAroCitations[0].citation).toEqual('Citation 1 UPDATED');
+ expect(updatedAroCitations).toHaveLength(1);
+ expect(updatedAroCitations[0].citation).toEqual('Citation 1 Updated');
expect(updatedAroCitations[0].monitoringReferences).toEqual([{
grantId: 1,
findingId: 1,
- reviewName: 'Review 1',
- },
- {
- grantId: 2,
- findingId: 2,
- reviewName: 'Review 2',
- }]);
- const secondCitationId = updatedAroCitations[1].id;
- expect(updatedAroCitations[1].citation).toEqual('Citation 2');
- expect(updatedAroCitations[1].monitoringReferences).toEqual([{
- grantId: 3,
- findingId: 3,
- reviewName: 'Review 3',
+ reviewName: 'Review 1 Updated',
}]);
// Delete the ActivityReportObjectiveCitation.
- const citationsToDelete = [
- {
- id: secondCitationId,
- citation: 'Citation 2',
- monitoringReferences: [{
- grantId: 4,
- findingId: 4,
- reviewName: 'Review 4',
- }],
- },
- ];
-
- // Save deleted ActivityReportObjectiveCitation.
- result = await cacheCitations(objective.id, aro.id, citationsToDelete);
-
- // Assert updated.
- expect(result[0]).toBeDefined();
-
- // Retrieve deleted citation 1.
- const deletedCitation = await ActivityReportObjectiveCitation.findOne({
- where: {
- id: citation1Id,
- },
- });
+ result = await cacheCitations(objective.id, aro.id, []);
- expect(deletedCitation).toBeNull();
+ // Assert deleted.
+ expect(result).toHaveLength(0);
- // Retrieve citation 2.
- const remainingCitation = await ActivityReportObjectiveCitation.findOne({
+ const deletedAroCitations = await ActivityReportObjectiveCitation.findAll({
where: {
- id: secondCitationId,
+ activityReportObjectiveId: aro.id,
},
});
- expect(remainingCitation).toBeDefined();
- expect(remainingCitation.citation).toEqual('Citation 2');
- expect(remainingCitation.monitoringReferences).toEqual([{
- grantId: 4,
- findingId: 4,
- reviewName: 'Review 4',
- }]);
+ expect(deletedAroCitations).toHaveLength(0);
});
});
From 37c04bc177cd6c631955361cfedeff4746fd9213 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Fri, 6 Dec 2024 09:25:40 -0500
Subject: [PATCH 064/198] check standard instead of source per Matt
---
.../src/pages/ActivityReport/Pages/components/GoalPicker.js | 2 +-
.../ActivityReport/Pages/components/__tests__/GoalPicker.js | 1 +
src/services/goalTemplates.ts | 1 +
3 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js b/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js
index 418df00e32..117fb02eda 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js
@@ -118,7 +118,7 @@ const GoalPicker = ({
useEffect(() => {
async function fetchCitations() {
// If its a monitoring goal and the source is CLASS or RANs, fetch the citations.
- if (goalForEditing && goalForEditing.source === 'Federal monitoring issues, including CLASS and RANs') {
+ if (goalForEditing && goalForEditing.standard && goalForEditing.standard === 'Monitoring') {
const monitoringGrantIds = goalForEditing.goals.map((g) => g.grantId);
const retrievedCitationOptions = await fetchCitationsByGrant(
regionId,
diff --git a/frontend/src/pages/ActivityReport/Pages/components/__tests__/GoalPicker.js b/frontend/src/pages/ActivityReport/Pages/components/__tests__/GoalPicker.js
index c16318e9d2..bda24efc8c 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/__tests__/GoalPicker.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/__tests__/GoalPicker.js
@@ -384,6 +384,7 @@ describe('GoalPicker', () => {
isCurated: true,
goalTemplateId: 1,
source: 'Federal monitoring issues, including CLASS and RANs',
+ standard: 'Monitoring',
goals: [
{
grantId: 1,
diff --git a/src/services/goalTemplates.ts b/src/services/goalTemplates.ts
index 8ca63bddb8..017199abc0 100644
--- a/src/services/goalTemplates.ts
+++ b/src/services/goalTemplates.ts
@@ -68,6 +68,7 @@ export async function getCuratedTemplates(
'id',
'source',
'isSourceEditable',
+ 'standard',
['templateName', 'label'],
['id', 'value'],
['templateName', 'name'],
From aeaf2a8e28ac089a286a3f6b3225c41faa3acd65 Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Fri, 6 Dec 2024 11:24:56 -0500
Subject: [PATCH 065/198] add API to get citation text for drawers
---
src/routes/citations/handlers.js | 18 +-
src/routes/citations/handlers.test.js | 312 ++++++++++++--------
src/routes/citations/index.js | 6 +
src/services/citations.test.js | 28 +-
src/services/{citations.js => citations.ts} | 44 ++-
5 files changed, 269 insertions(+), 139 deletions(-)
rename src/services/{citations.js => citations.ts} (76%)
diff --git a/src/routes/citations/handlers.js b/src/routes/citations/handlers.js
index e9b06b9019..218ab22c32 100644
--- a/src/routes/citations/handlers.js
+++ b/src/routes/citations/handlers.js
@@ -7,12 +7,28 @@ import handleErrors from '../../lib/apiErrorHandler';
import User from '../../policies/user';
import { currentUserId } from '../../services/currentUser';
import { userById } from '../../services/users';
-import { getCitationsByGrantIds } from '../../services/citations';
+import { getCitationsByGrantIds, textByCitation } from '../../services/citations';
const namespace = 'SERVICE:CITATIONS';
const logContext = { namespace };
+export const getTextByCitation = async (req, res) => {
+ try {
+ // citations are available with a site access permission
+ const { citationIds } = req.query;
+
+ // Get the citations for the grant.
+ const citations = await textByCitation([citationIds].flat());
+
+ // Return the text
+ res.status(httpCodes.OK).send(citations);
+ } catch (error) {
+ // Handle any errors that occur.
+ await handleErrors(req, res, error, logContext);
+ }
+};
+
export const getCitationsByGrants = async (req, res) => {
try {
// Get the grant we need citations for.
diff --git a/src/routes/citations/handlers.test.js b/src/routes/citations/handlers.test.js
index 48d97e5fa4..b81e7ca64e 100644
--- a/src/routes/citations/handlers.test.js
+++ b/src/routes/citations/handlers.test.js
@@ -1,7 +1,7 @@
-import { getCitationsByGrants } from './handlers';
+import { getCitationsByGrants, getTextByCitation } from './handlers';
import { currentUserId } from '../../services/currentUser';
import { userById } from '../../services/users';
-import { getCitationsByGrantIds } from '../../services/citations';
+import { getCitationsByGrantIds, textByCitation } from '../../services/citations';
import User from '../../policies/user';
import handleErrors from '../../lib/apiErrorHandler';
@@ -13,141 +13,201 @@ jest.mock('../../lib/apiErrorHandler');
jest.mock('../../services/citations');
describe('Citation handlers', () => {
- afterEach(() => {
- jest.clearAllMocks();
- });
-
afterAll(() => {
jest.restoreAllMocks();
});
- it('should get citations by grant id', async () => {
+ describe('getTextByCitation', () => {
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should get text by citation', async () => {
+ const req = {
+ query: {
+ citationIds: [1],
+ },
+ };
+
+ const res = {
+ sendStatus: jest.fn(),
+ status: jest.fn().mockReturnThis(),
+ send: jest.fn(),
+ };
+
+ const text = [
+ {
+ id: 1,
+ },
+ ];
+
+ textByCitation.mockResolvedValue(text);
+
+ await getTextByCitation(req, res);
+
+ expect(textByCitation).toHaveBeenCalledWith([1]);
+ expect(res.status).toHaveBeenCalledWith(200);
+ expect(res.send).toHaveBeenCalledWith(text);
+ });
+
+ it('should handle errors', async () => {
+ const req = {
+ query: {
+ citationIds: [1],
+ },
+ };
+ const res = {
+ sendStatus: jest.fn(),
+ status: jest.fn().mockReturnThis(),
+ send: jest.fn(),
+ };
+
+ textByCitation.mockRejectedValueOnce(new Error('Something went wrong!'));
+
+ handleErrors.mockImplementation(() => {
+ res.sendStatus(500);
+ });
+
+ await getTextByCitation(req, res);
+
+ expect(textByCitation).toHaveBeenCalledWith([1]);
+ expect(res.sendStatus).toHaveBeenCalledWith(500);
+ });
+ });
+
+ describe('getCitationsByGrantS', () => {
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should get citations by grant id', async () => {
// Mock request.
- const req = {
- query: {
- grantIds: [1],
- reportStartDate: '2024-10-01',
- },
- params: {
- regionId: 1,
- },
- };
-
- // Mock response.
- const res = {
- sendStatus: jest.fn(),
- status: jest.fn().mockReturnThis(),
- send: jest.fn(),
- };
-
- // Mock the user.
- const user = {
- id: 1,
- };
-
- // Mock the response citations.
- const citations = [
- {
+ const req = {
+ query: {
+ grantIds: [1],
+ reportStartDate: '2024-10-01',
+ },
+ params: {
+ regionId: 1,
+ },
+ };
+
+ // Mock response.
+ const res = {
+ sendStatus: jest.fn(),
+ status: jest.fn().mockReturnThis(),
+ send: jest.fn(),
+ };
+
+ // Mock the user.
+ const user = {
id: 1,
- },
- ];
-
- // Mock the functions.
- User.mockImplementation(() => ({
- canWriteInRegion: () => true,
- }));
- currentUserId.mockResolvedValue(user.id);
- userById.mockResolvedValue(user);
- getCitationsByGrantIds.mockResolvedValue(citations);
-
- await getCitationsByGrants(req, res);
-
- expect(currentUserId).toHaveBeenCalledWith(req, res);
- expect(userById).toHaveBeenCalledWith(user.id);
- expect(getCitationsByGrantIds).toHaveBeenCalledWith(req.query.grantIds, '2024-10-01');
- expect(res.status).toHaveBeenCalledWith(200);
- expect(res.send).toHaveBeenCalledWith(citations);
- });
+ };
+
+ // Mock the response citations.
+ const citations = [
+ {
+ id: 1,
+ },
+ ];
+
+ // Mock the functions.
+ User.mockImplementation(() => ({
+ canWriteInRegion: () => true,
+ }));
+ currentUserId.mockResolvedValue(user.id);
+ userById.mockResolvedValue(user);
+ getCitationsByGrantIds.mockResolvedValue(citations);
+
+ await getCitationsByGrants(req, res);
+
+ expect(currentUserId).toHaveBeenCalledWith(req, res);
+ expect(userById).toHaveBeenCalledWith(user.id);
+ expect(getCitationsByGrantIds).toHaveBeenCalledWith(req.query.grantIds, '2024-10-01');
+ expect(res.status).toHaveBeenCalledWith(200);
+ expect(res.send).toHaveBeenCalledWith(citations);
+ });
- it('should handle errors', async () => {
+ it('should handle errors', async () => {
// Mock request.
- const req = {
- query: {
- grantIds: [1],
- reportStartDate: '2024-10-01',
- },
- params: {
- regionId: 1,
- },
- };
-
- // Mock response.
- const res = {
- sendStatus: jest.fn(),
- status: jest.fn().mockReturnThis(),
- send: jest.fn(),
- };
-
- // Mock the user.
- const user = {
- id: 1,
- };
-
- // Mock the functions.
- User.mockImplementation(() => ({
- canWriteInRegion: () => true,
- }));
- currentUserId.mockResolvedValue(user.id);
- userById.mockResolvedValue(user);
- getCitationsByGrantIds.mockRejectedValueOnce(new Error('Something went wrong!'));
-
- // Mock the handleErrors function to return a 500 status code.
- handleErrors.mockImplementation(() => {
- res.sendStatus(500);
+ const req = {
+ query: {
+ grantIds: [1],
+ reportStartDate: '2024-10-01',
+ },
+ params: {
+ regionId: 1,
+ },
+ };
+
+ // Mock response.
+ const res = {
+ sendStatus: jest.fn(),
+ status: jest.fn().mockReturnThis(),
+ send: jest.fn(),
+ };
+
+ // Mock the user.
+ const user = {
+ id: 1,
+ };
+
+ // Mock the functions.
+ User.mockImplementation(() => ({
+ canWriteInRegion: () => true,
+ }));
+ currentUserId.mockResolvedValue(user.id);
+ userById.mockResolvedValue(user);
+ getCitationsByGrantIds.mockRejectedValueOnce(new Error('Something went wrong!'));
+
+ // Mock the handleErrors function to return a 500 status code.
+ handleErrors.mockImplementation(() => {
+ res.sendStatus(500);
+ });
+
+ await getCitationsByGrants(req, res);
+
+ expect(currentUserId).toHaveBeenCalledWith(req, res);
+ expect(userById).toHaveBeenCalledWith(user.id);
+ expect(getCitationsByGrantIds).toHaveBeenCalledWith(req.query.grantIds, '2024-10-01');
+ expect(res.sendStatus).toHaveBeenCalledWith(500);
});
- await getCitationsByGrants(req, res);
+ it('should return a 403 status code if the user cannot write in the region', async () => {
+ // Mock request.
+ const req = {
+ query: {
+ grantIds: [1],
+ reportStartDate: '2024-10-01',
+ },
+ params: {
+ regionId: 1,
+ },
+ };
+
+ // Mock response.
+ const res = {
+ sendStatus: jest.fn(),
+ status: jest.fn().mockReturnThis(),
+ send: jest.fn(),
+ };
+
+ // Mock the user.
+ const user = {
+ id: 1,
+ };
- expect(currentUserId).toHaveBeenCalledWith(req, res);
- expect(userById).toHaveBeenCalledWith(user.id);
- expect(getCitationsByGrantIds).toHaveBeenCalledWith(req.query.grantIds, '2024-10-01');
- expect(res.sendStatus).toHaveBeenCalledWith(500);
- });
+ // Mock the functions.
+ User.mockImplementation(() => ({
+ canWriteInRegion: () => false,
+ }));
+ currentUserId.mockResolvedValue(user.id);
+ userById.mockResolvedValue(user);
+ getCitationsByGrantIds.mockResolvedValue([]);
- it('should return a 403 status code if the user cannot write in the region', async () => {
- // Mock request.
- const req = {
- query: {
- grantIds: [1],
- reportStartDate: '2024-10-01',
- },
- params: {
- regionId: 1,
- },
- };
-
- // Mock response.
- const res = {
- sendStatus: jest.fn(),
- status: jest.fn().mockReturnThis(),
- send: jest.fn(),
- };
-
- // Mock the user.
- const user = {
- id: 1,
- };
-
- // Mock the functions.
- User.mockImplementation(() => ({
- canWriteInRegion: () => false,
- }));
- currentUserId.mockResolvedValue(user.id);
- userById.mockResolvedValue(user);
- getCitationsByGrantIds.mockResolvedValue([]);
-
- await getCitationsByGrants(req, res);
-
- expect(res.sendStatus).toHaveBeenCalledWith(403);
+ await getCitationsByGrants(req, res);
+
+ expect(res.sendStatus).toHaveBeenCalledWith(403);
+ });
});
});
diff --git a/src/routes/citations/index.js b/src/routes/citations/index.js
index 64ca6dec72..5c20b76aad 100644
--- a/src/routes/citations/index.js
+++ b/src/routes/citations/index.js
@@ -2,6 +2,7 @@ import express from 'express';
import transactionWrapper from '../transactionWrapper';
import {
getCitationsByGrants,
+ getTextByCitation,
} from './handlers';
import {
checkRegionIdParam,
@@ -15,4 +16,9 @@ router.get(
transactionWrapper(getCitationsByGrants),
);
+router.get(
+ '/text',
+ transactionWrapper(getTextByCitation),
+);
+
export default router;
diff --git a/src/services/citations.test.js b/src/services/citations.test.js
index df6b3ac65b..355924ab3f 100644
--- a/src/services/citations.test.js
+++ b/src/services/citations.test.js
@@ -2,7 +2,7 @@
/* eslint-disable prefer-destructuring */
import { v4 as uuidv4 } from 'uuid';
import faker from '@faker-js/faker';
-import { getCitationsByGrantIds } from './citations';
+import { getCitationsByGrantIds, textByCitation } from './citations';
import db, {
Recipient,
Grant,
@@ -143,6 +143,7 @@ const createMonitoringData = async (
sourceUpdatedAt: new Date(),
contentId: uuidv4(),
hash: uuidv4(),
+ text: faker.random.words(10),
citable,
}, { individualHooks: true });
}));
@@ -165,10 +166,6 @@ describe('citations service', () => {
let grant2; // Recipient 2
let grant3; // Recipient 2 (Inactive)
- // Goals.
- let monitoringGoal;
- let grant1aMonitoringGoal;
-
beforeAll(async () => {
// Capture a snapshot of the database before running the test.
snapShot = await captureSnapshot();
@@ -249,7 +246,7 @@ describe('citations service', () => {
grant3 = grants[3];
// Create Goals and Link them to Grants.
- monitoringGoal = await Goal.create({
+ await Goal.create({
name: 'Monitoring Goal 1',
status: 'Not started',
timeframe: '12 months',
@@ -287,7 +284,7 @@ describe('citations service', () => {
});
// Create monitoring goal for grant 2.
- grant1aMonitoringGoal = await Goal.create({
+ await Goal.create({
name: 'Monitoring Goal 3',
status: 'Not started',
timeframe: '12 months',
@@ -420,4 +417,21 @@ describe('citations service', () => {
expect(citation3.grants[0].reportDeliveryDate).toBeDefined();
expect(citation3.grants[0].findingType).toBe('Citation 4 Monitoring Finding Type');
});
+
+ describe('textByCitation', () => {
+ it('gets text by citation', async () => {
+ const response = await textByCitation(['Grant 2 - Citation 1 - Good']);
+
+ expect(response.map((citation) => citation.toJSON())).toStrictEqual([
+ {
+ citation: 'Grant 2 - Citation 1 - Good',
+ text: expect.any(String),
+ },
+ {
+ citation: 'Grant 2 - Citation 1 - Good',
+ text: expect.any(String),
+ },
+ ]);
+ });
+ });
});
diff --git a/src/services/citations.js b/src/services/citations.ts
similarity index 76%
rename from src/services/citations.js
rename to src/services/citations.ts
index 4386c68315..e4a51ed7ff 100644
--- a/src/services/citations.js
+++ b/src/services/citations.ts
@@ -1,16 +1,50 @@
/* eslint-disable no-plusplus */
-/* eslint-disable import/prefer-default-export */
-import { sequelize } from '../models';
+import db, { sequelize } from '../models';
+
+const { MonitoringStandard } = db;
+
+export async function textByCitation(
+ citationIds: string[],
+): Promise<{ text: string, citation: string }[]> {
+ return MonitoringStandard.findAll({
+ attributes: ['text', 'citation'],
+ where: {
+ citation: citationIds,
+ },
+ });
+}
// TODO: Update this to the day we deploy to PROD.
-const cutOffStartDate = new Date().toISOString().split('T')[0];
-// const cutOffStartDate = '2021-01-01';
+// const cutOffStartDate = new Date().toISOString().split('T')[0];
+const cutOffStartDate = '2021-01-01';
/*
The purpose of this function is to get citations by grant id.
We then need to format the response for how it needs to be
displayed on the FE for selection on objectives.
*/
-export async function getCitationsByGrantIds(grantIds, reportStartDate) {
+
+export interface CitationsByGrantId {
+ standardId: number;
+ citation: string;
+ grants: {
+ acro: string;
+ grantId: number;
+ citation: string;
+ severity: number;
+ findingId: string;
+ reviewName: string;
+ findingType: string;
+ grantNumber: string;
+ findingSource: string;
+ reportDeliveryDate: Date;
+ monitoringFindingStatusName: string;
+ }[];
+}
+
+export async function getCitationsByGrantIds(
+ grantIds: number[],
+ reportStartDate: string,
+): Promise {
// Query to get the citations by grant id.
const grantsByCitations = await sequelize.query(
/* sql */
From 2ba38c146ec9f0017d2dd83696af79b88847b3b0 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Fri, 6 Dec 2024 12:35:44 -0500
Subject: [PATCH 066/198] update goal template retrieve
---
src/routes/goalTemplates/handlers.ts | 8 +-
src/services/goalTemplates.test.js | 499 ++++++++++++++++-----------
src/services/goalTemplates.ts | 28 ++
3 files changed, 336 insertions(+), 199 deletions(-)
diff --git a/src/routes/goalTemplates/handlers.ts b/src/routes/goalTemplates/handlers.ts
index cf4d7f16a4..3c6fc8eb11 100644
--- a/src/routes/goalTemplates/handlers.ts
+++ b/src/routes/goalTemplates/handlers.ts
@@ -1,6 +1,9 @@
/* eslint-disable import/prefer-default-export */
import { Request, Response } from 'express';
import { DECIMAL_BASE } from '@ttahub/common';
+import { currentUserId } from '../../services/currentUser';
+import { userById } from '../../services/users';
+import User from '../../policies/user';
import handleErrors from '../../lib/apiErrorHandler';
import {
getCuratedTemplates,
@@ -17,7 +20,10 @@ export async function getGoalTemplates(req: Request, res: Response) {
const parsedGrantIds = [grantIds].flat().map((id: string) => parseInt(id, DECIMAL_BASE))
.filter((id: number) => !Number.isNaN(id));
- const templates = await getCuratedTemplates(parsedGrantIds);
+ const userId = await currentUserId(req, res);
+ const user = await userById(userId);
+
+ const templates = await getCuratedTemplates(parsedGrantIds, user);
res.json(templates);
} catch (err) {
await handleErrors(req, res, err, 'goalTemplates.getGoalTemplates');
diff --git a/src/services/goalTemplates.test.js b/src/services/goalTemplates.test.js
index 913fe6a63a..b11558d447 100644
--- a/src/services/goalTemplates.test.js
+++ b/src/services/goalTemplates.test.js
@@ -2,8 +2,9 @@ import faker from '@faker-js/faker';
import { GOAL_SOURCES } from '@ttahub/common';
import crypto from 'crypto';
import db from '../models';
-import { setFieldPromptsForCuratedTemplate, getSourceFromTemplate } from './goalTemplates';
+import { setFieldPromptsForCuratedTemplate, getSourceFromTemplate, getCuratedTemplates } from './goalTemplates';
import { AUTOMATIC_CREATION } from '../constants';
+import { captureSnapshot, rollbackToSnapshot } from '../lib/programmaticTransaction';
const {
Goal,
@@ -20,272 +21,374 @@ describe('goalTemplates services', () => {
await sequelize.close();
});
- describe('getSourceFromTemplate', () => {
- let template;
- let templateTwo;
- let grant;
- let grantTwo;
- let grantThree;
- let recipient;
+ describe('getCuratedTemplates', () => {
+ // Recipients.
+ let monitoringRecipient;
+ let regularRecipient;
+ // Grants.
+ let monitoringGrant;
+ let regularGrant;
+
+ // Templates.
+ let monitoringTemplate;
+ let regularTemplate;
+
+ // Db snapshot.
+ let snapShot;
beforeAll(async () => {
- recipient = await Recipient.create({
+ snapShot = await captureSnapshot();
+ // Create recipient.
+ monitoringRecipient = await Recipient.create({
id: faker.datatype.number({ min: 56000 }),
name: faker.datatype.string(20),
});
- grant = await Grant.create({
- regionId: 2,
- status: 'Active',
+ regularRecipient = await Recipient.create({
id: faker.datatype.number({ min: 56000 }),
- number: faker.datatype.string(255),
- recipientId: recipient.id,
+ name: faker.datatype.string(20),
});
- grantTwo = await Grant.create({
- regionId: 2,
+ // Create grants.
+ monitoringGrant = await Grant.create({
+ regionId: 1,
status: 'Active',
id: faker.datatype.number({ min: 56000 }),
number: faker.datatype.string(255),
- recipientId: recipient.id,
+ recipientId: monitoringRecipient.id,
});
- grantThree = await Grant.create({
- regionId: 2,
+ regularGrant = await Grant.create({
+ regionId: 1,
status: 'Active',
id: faker.datatype.number({ min: 56000 }),
number: faker.datatype.string(255),
- recipientId: recipient.id,
+ recipientId: regularRecipient.id,
});
- const n = faker.lorem.sentence(5);
+ // Get the monitoring template.
+ monitoringTemplate = await GoalTemplate.findOne({ where: { standard: 'Monitoring' } });
- const secret = 'secret';
- const hash = crypto
- .createHmac('md5', secret)
- .update(n)
- .digest('hex');
-
- template = await GoalTemplate.create({
- hash,
- templateName: n,
- creationMethod: AUTOMATIC_CREATION,
- source: GOAL_SOURCES[1],
- });
+ // Get the regular template.
+ regularTemplate = await GoalTemplate.findOne({ where: { standard: 'FEI' } });
+ // Create goals.
await Goal.create({
- grantId: grant.id,
- goalTemplateId: template.id,
- name: n,
- source: GOAL_SOURCES[0],
+ grantId: monitoringGrant.id,
+ goalTemplateId: monitoringTemplate.id,
+ createdVia: 'monitoring',
+ name: 'Monitoring goal for template test',
+ status: 'In Progress',
});
await Goal.create({
- grantId: grantTwo.id,
- goalTemplateId: template.id,
- name: n,
+ grantId: regularGrant.id,
+ goalTemplateId: regularTemplate.id,
+ createdVia: 'activityReport',
+ name: 'Regular goal for template test',
+ status: 'In Progress',
});
+ });
- const n2 = faker.lorem.sentence(5);
+ afterAll(async () => {
+ await rollbackToSnapshot(snapShot);
+ });
- const hash2 = crypto
- .createHmac('md5', secret)
- .update(n2)
- .digest('hex');
+ it('returns only non-monitoring templates', async () => {
+ const templates = await getCuratedTemplates(
+ [monitoringGrant.id, regularGrant.id],
+ { id: 1, name: 'regular user', flags: [] },
+ );
- templateTwo = await GoalTemplate.create({
- hash: hash2,
- templateName: n2,
- creationMethod: AUTOMATIC_CREATION,
- });
+ // Make sure the results contain the regular template and NOT the monitoring template.
+ const regularTemplateToAssert = templates.find((t) => t.id === regularTemplate.id);
+ expect(regularTemplateToAssert).toBeTruthy();
- await Goal.create({
- grantId: grantThree.id,
- goalTemplateId: templateTwo.id,
- name: n2,
- });
+ const monitoringTemplateToAssert = templates.find((t) => t.id === monitoringTemplate.id);
+ expect(monitoringTemplateToAssert).toBeFalsy();
});
- afterAll(async () => {
- await Goal.destroy({
- where: {
- goalTemplateId: [
- template.id,
- templateTwo.id,
- ],
- },
- force: true,
- paranoid: true,
- individualHooks: true,
- });
- await GoalTemplate.destroy({
- where: {
- id: [
- template.id,
- templateTwo.id],
- },
- individualHooks: true,
- });
- await Grant.destroy({
- where: {
- id: [
- grant.id,
- grantTwo.id,
- grantThree.id,
- ],
- },
- individualHooks: true,
- });
- await Recipient.destroy({ where: { id: recipient.id }, individualHooks: true });
+ it('returns both regular and only monitoring templates', async () => {
+ const templates = await getCuratedTemplates(
+ [monitoringGrant.id, regularGrant.id],
+ { id: 1, name: 'regular user', flags: ['monitoring_integration'] },
+ );
+
+ // Make sure the results contain the regular template and NOT the monitoring template.
+ const regularTemplateToAssert = templates.find((t) => t.id === regularTemplate.id);
+ expect(regularTemplateToAssert).toBeTruthy();
+
+ const monitoringTemplateToAssert = templates.find((t) => t.id === monitoringTemplate.id);
+ expect(monitoringTemplateToAssert).toBeTruthy();
+ });
+ });
+});
+
+describe('getSourceFromTemplate', () => {
+ let template;
+ let templateTwo;
+ let grant;
+ let grantTwo;
+ let grantThree;
+ let recipient;
+
+ beforeAll(async () => {
+ recipient = await Recipient.create({
+ id: faker.datatype.number({ min: 56000 }),
+ name: faker.datatype.string(20),
+ });
+
+ grant = await Grant.create({
+ regionId: 2,
+ status: 'Active',
+ id: faker.datatype.number({ min: 56000 }),
+ number: faker.datatype.string(255),
+ recipientId: recipient.id,
});
- it('returns source from the goal', async () => {
- const source = await getSourceFromTemplate(template.id, [grant.id]);
+ grantTwo = await Grant.create({
+ regionId: 2,
+ status: 'Active',
+ id: faker.datatype.number({ min: 56000 }),
+ number: faker.datatype.string(255),
+ recipientId: recipient.id,
+ });
- expect(source).toBe(GOAL_SOURCES[0]);
+ grantThree = await Grant.create({
+ regionId: 2,
+ status: 'Active',
+ id: faker.datatype.number({ min: 56000 }),
+ number: faker.datatype.string(255),
+ recipientId: recipient.id,
});
- it('returns source from the template', async () => {
- const source = await getSourceFromTemplate(template.id, [grantTwo.id]);
- expect(source).toBe(GOAL_SOURCES[1]);
+ const n = faker.lorem.sentence(5);
+
+ const secret = 'secret';
+ const hash = crypto
+ .createHmac('md5', secret)
+ .update(n)
+ .digest('hex');
+
+ template = await GoalTemplate.create({
+ hash,
+ templateName: n,
+ creationMethod: AUTOMATIC_CREATION,
+ source: GOAL_SOURCES[1],
});
- it('returns null source', async () => {
- const source = await getSourceFromTemplate(template.id, [grantThree.id]);
+ await Goal.create({
+ grantId: grant.id,
+ goalTemplateId: template.id,
+ name: n,
+ source: GOAL_SOURCES[0],
+ });
- expect(source).toBeFalsy();
+ await Goal.create({
+ grantId: grantTwo.id,
+ goalTemplateId: template.id,
+ name: n,
+ });
+
+ const n2 = faker.lorem.sentence(5);
+
+ const hash2 = crypto
+ .createHmac('md5', secret)
+ .update(n2)
+ .digest('hex');
+
+ templateTwo = await GoalTemplate.create({
+ hash: hash2,
+ templateName: n2,
+ creationMethod: AUTOMATIC_CREATION,
+ });
+
+ await Goal.create({
+ grantId: grantThree.id,
+ goalTemplateId: templateTwo.id,
+ name: n2,
});
});
- describe('setFieldPromptsForCuratedTemplate', () => {
- let promptResponses;
- let template;
- let goalIds;
- let grant;
- let recipient;
- let promptId;
- let promptTitle;
+ afterAll(async () => {
+ await Goal.destroy({
+ where: {
+ goalTemplateId: [
+ template.id,
+ templateTwo.id,
+ ],
+ },
+ force: true,
+ paranoid: true,
+ individualHooks: true,
+ });
+ await GoalTemplate.destroy({
+ where: {
+ id: [
+ template.id,
+ templateTwo.id],
+ },
+ individualHooks: true,
+ });
+ await Grant.destroy({
+ where: {
+ id: [
+ grant.id,
+ grantTwo.id,
+ grantThree.id,
+ ],
+ },
+ individualHooks: true,
+ });
+ await Recipient.destroy({ where: { id: recipient.id }, individualHooks: true });
+ });
- beforeAll(async () => {
- recipient = await Recipient.create({
- id: faker.datatype.number({ min: 56000 }),
- name: faker.datatype.string(20),
- });
+ it('returns source from the goal', async () => {
+ const source = await getSourceFromTemplate(template.id, [grant.id]);
- grant = await Grant.create({
- regionId: 2,
- status: 'Active',
- id: faker.datatype.number({ min: 56000 }),
- number: faker.datatype.string(255),
- recipientId: recipient.id,
- });
+ expect(source).toBe(GOAL_SOURCES[0]);
+ });
+ it('returns source from the template', async () => {
+ const source = await getSourceFromTemplate(template.id, [grantTwo.id]);
- const n = faker.lorem.sentence(5);
+ expect(source).toBe(GOAL_SOURCES[1]);
+ });
- const secret = 'secret';
- const hash = crypto
- .createHmac('md5', secret)
- .update(n)
- .digest('hex');
+ it('returns null source', async () => {
+ const source = await getSourceFromTemplate(template.id, [grantThree.id]);
- template = await GoalTemplate.create({
- hash,
- templateName: n,
- creationMethod: AUTOMATIC_CREATION,
- });
+ expect(source).toBeFalsy();
+ });
+});
- promptTitle = faker.datatype.string(255);
-
- const prompt = await GoalTemplateFieldPrompt.create({
- goalTemplateId: template.id,
- ordinal: 1,
- title: promptTitle,
- prompt: promptTitle,
- hint: '',
- options: ['option 1', 'option 2', 'option 3'],
- fieldType: 'multiselect',
- validations: { required: 'Select a root cause', rules: [{ name: 'maxSelections', value: 2, message: 'You can only select 2 options' }] },
- });
+describe('setFieldPromptsForCuratedTemplate', () => {
+ let promptResponses;
+ let template;
+ let goalIds;
+ let grant;
+ let recipient;
+ let promptId;
+ let promptTitle;
+
+ beforeAll(async () => {
+ recipient = await Recipient.create({
+ id: faker.datatype.number({ min: 56000 }),
+ name: faker.datatype.string(20),
+ });
- promptId = prompt.id;
+ grant = await Grant.create({
+ regionId: 2,
+ status: 'Active',
+ id: faker.datatype.number({ min: 56000 }),
+ number: faker.datatype.string(255),
+ recipientId: recipient.id,
+ });
- promptResponses = [
- { promptId: prompt.id, response: ['option 1', 'option 2'] },
- ];
+ const n = faker.lorem.sentence(5);
- const goal = await Goal.create({
- grantId: grant.id,
- goalTemplateId: template.id,
- name: n,
- });
+ const secret = 'secret';
+ const hash = crypto
+ .createHmac('md5', secret)
+ .update(n)
+ .digest('hex');
- goalIds = [goal.id];
+ template = await GoalTemplate.create({
+ hash,
+ templateName: n,
+ creationMethod: AUTOMATIC_CREATION,
});
- afterAll(async () => {
- await GoalFieldResponse.destroy({ where: { goalId: goalIds }, individualHooks: true });
- // eslint-disable-next-line max-len
- await GoalTemplateFieldPrompt.destroy({ where: { goalTemplateId: template.id }, individualHooks: true });
- await Goal.destroy({
- where: { goalTemplateId: template.id }, force: true, paranoid: true, individualHooks: true,
- });
- await GoalTemplate.destroy({ where: { id: template.id }, individualHooks: true });
- await Grant.destroy({ where: { id: grant.id }, individualHooks: true });
- await Recipient.destroy({ where: { id: recipient.id }, individualHooks: true });
+ promptTitle = faker.datatype.string(255);
+
+ const prompt = await GoalTemplateFieldPrompt.create({
+ goalTemplateId: template.id,
+ ordinal: 1,
+ title: promptTitle,
+ prompt: promptTitle,
+ hint: '',
+ options: ['option 1', 'option 2', 'option 3'],
+ fieldType: 'multiselect',
+ validations: { required: 'Select a root cause', rules: [{ name: 'maxSelections', value: 2, message: 'You can only select 2 options' }] },
});
- it('should call setFieldPromptForCuratedTemplate for each prompt response', async () => {
- // save initial field responses
- await setFieldPromptsForCuratedTemplate(goalIds, promptResponses);
+ promptId = prompt.id;
- // check that the field responses were saved
- const fieldResponses = await GoalFieldResponse.findAll({
- where: { goalId: goalIds },
- });
+ promptResponses = [
+ { promptId: prompt.id, response: ['option 1', 'option 2'] },
+ ];
- expect(fieldResponses.length).toBe(promptResponses.length);
- expect(fieldResponses[0].response).toEqual(promptResponses[0].response);
+ const goal = await Goal.create({
+ grantId: grant.id,
+ goalTemplateId: template.id,
+ name: n,
+ });
- // update field responses
- await setFieldPromptsForCuratedTemplate(goalIds, [
- { promptId, response: ['option 1'] },
- ]);
+ goalIds = [goal.id];
+ });
- // check that the field responses were updated
- const updatedFieldResponses = await GoalFieldResponse.findAll({
- where: { goalId: goalIds },
- });
+ afterAll(async () => {
+ await GoalFieldResponse.destroy({ where: { goalId: goalIds }, individualHooks: true });
+ // eslint-disable-next-line max-len
+ await GoalTemplateFieldPrompt.destroy({ where: { goalTemplateId: template.id }, individualHooks: true });
+ await Goal.destroy({
+ where: { goalTemplateId: template.id }, force: true, paranoid: true, individualHooks: true,
+ });
+ await GoalTemplate.destroy({ where: { id: template.id }, individualHooks: true });
+ await Grant.destroy({ where: { id: grant.id }, individualHooks: true });
+ await Recipient.destroy({ where: { id: recipient.id }, individualHooks: true });
+ });
+
+ it('should call setFieldPromptForCuratedTemplate for each prompt response', async () => {
+ // save initial field responses
+ await setFieldPromptsForCuratedTemplate(goalIds, promptResponses);
- expect(updatedFieldResponses.length).toBe(promptResponses.length);
- expect(updatedFieldResponses[0].response).toEqual(['option 1']);
+ // check that the field responses were saved
+ const fieldResponses = await GoalFieldResponse.findAll({
+ where: { goalId: goalIds },
});
- it('should use the provided validations', async () => {
- const fieldResponses = await GoalFieldResponse.findAll({
- where: { goalId: goalIds },
- raw: true,
- });
+ expect(fieldResponses.length).toBe(promptResponses.length);
+ expect(fieldResponses[0].response).toEqual(promptResponses[0].response);
- // test validation error (no more than 2 options can be selected)
- await expect(setFieldPromptsForCuratedTemplate(goalIds, [
- { promptId, response: ['option 1', 'option 2', 'option 3'] },
- ])).rejects.toThrow();
+ // update field responses
+ await setFieldPromptsForCuratedTemplate(goalIds, [
+ { promptId, response: ['option 1'] },
+ ]);
- // check that the field responses were not updated
- const notUpdatedFieldResponses = await GoalFieldResponse.findAll({
- where: { goalId: goalIds },
- raw: true,
- });
+ // check that the field responses were updated
+ const updatedFieldResponses = await GoalFieldResponse.findAll({
+ where: { goalId: goalIds },
+ });
+
+ expect(updatedFieldResponses.length).toBe(promptResponses.length);
+ expect(updatedFieldResponses[0].response).toEqual(['option 1']);
+ });
- expect(notUpdatedFieldResponses.length).toBe(fieldResponses.length);
- expect(notUpdatedFieldResponses[0].response).toStrictEqual(fieldResponses[0].response);
+ it('should use the provided validations', async () => {
+ const fieldResponses = await GoalFieldResponse.findAll({
+ where: { goalId: goalIds },
+ raw: true,
});
- it('does nothing if the prompt doesn\'t exist', async () => {
- const fictionalId = 123454345345;
- await expect(setFieldPromptsForCuratedTemplate(goalIds, [
- { promptId: fictionalId, response: ['option 1'] },
- ])).rejects.toThrow(`No prompt found with ID ${fictionalId}`);
+ // test validation error (no more than 2 options can be selected)
+ await expect(setFieldPromptsForCuratedTemplate(goalIds, [
+ { promptId, response: ['option 1', 'option 2', 'option 3'] },
+ ])).rejects.toThrow();
+
+ // check that the field responses were not updated
+ const notUpdatedFieldResponses = await GoalFieldResponse.findAll({
+ where: { goalId: goalIds },
+ raw: true,
});
+
+ expect(notUpdatedFieldResponses.length).toBe(fieldResponses.length);
+ expect(notUpdatedFieldResponses[0].response).toStrictEqual(fieldResponses[0].response);
+ });
+
+ it('does nothing if the prompt doesn\'t exist', async () => {
+ const fictionalId = 123454345345;
+ await expect(setFieldPromptsForCuratedTemplate(goalIds, [
+ { promptId: fictionalId, response: ['option 1'] },
+ ])).rejects.toThrow(`No prompt found with ID ${fictionalId}`);
});
});
diff --git a/src/services/goalTemplates.ts b/src/services/goalTemplates.ts
index 017199abc0..427f547317 100644
--- a/src/services/goalTemplates.ts
+++ b/src/services/goalTemplates.ts
@@ -1,5 +1,6 @@
/* eslint-disable import/prefer-default-export */
import { Sequelize, Op } from 'sequelize';
+import Users from '../policies/user';
import db from '../models';
import { CREATION_METHOD, GOAL_STATUS, PROMPT_FIELD_TYPE } from '../constants';
@@ -59,10 +60,36 @@ specified region.
*/
export async function getCuratedTemplates(
grantIds: number[] | null,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ user: any,
): Promise {
// Collect all the templates that either have a null regionId or a grant within the specified
// region.
+ // Check if the user has the monitoring feature flag.
+ const hasGoalMonitoringOverride = !!(user && new Users(user).canSeeBehindFeatureFlag('monitoring_integration'));
+ // If they have the monitoring flag include monitoring goals.
+ let monitoringGoalIds = [];
+ if (hasGoalMonitoringOverride) {
+ const monitoringGoals = await GoalModel.findAll({
+ attributes: ['id'],
+ where: {
+ createdVia: 'monitoring',
+ grantId: grantIds,
+ },
+ });
+ monitoringGoalIds = monitoringGoals.map((goal) => goal.id);
+ }
+
+ const filterForMonitoringGoals = monitoringGoalIds.length > 0
+ ? {
+ [Op.or]: [
+ { '$goals.id$': monitoringGoalIds },
+ { '$goals.createdVia$': { [Op.not]: 'monitoring' } },
+ ],
+ }
+ : { '$goals.createdVia$': { [Op.not]: 'monitoring' } };
+
return GoalTemplateModel.findAll({
attributes: [
'id',
@@ -122,6 +149,7 @@ export async function getCuratedTemplates(
],
where: {
creationMethod: CREATION_METHOD.CURATED,
+ ...filterForMonitoringGoals,
[Op.or]: [
{ '$"region.grants"."id"$': { [Op.not]: null } },
{ regionId: null },
From 1785882e6d00aa3672642fec6847d03060a24a6d Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Fri, 6 Dec 2024 13:14:32 -0500
Subject: [PATCH 067/198] Revert change to cut-off date
---
src/services/citations.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/services/citations.ts b/src/services/citations.ts
index e4a51ed7ff..4b64528883 100644
--- a/src/services/citations.ts
+++ b/src/services/citations.ts
@@ -15,8 +15,8 @@ export async function textByCitation(
}
// TODO: Update this to the day we deploy to PROD.
-// const cutOffStartDate = new Date().toISOString().split('T')[0];
-const cutOffStartDate = '2021-01-01';
+const cutOffStartDate = new Date().toISOString().split('T')[0];
+// const cutOffStartDate = '2021-01-01';
/*
The purpose of this function is to get citations by grant id.
We then need to format the response for how it needs to be
From ecc1e190f60bdbb8e3ee304b6ff80f215fcd4fd2 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Fri, 6 Dec 2024 14:17:51 -0500
Subject: [PATCH 068/198] dont require user
---
src/services/goalTemplates.ts | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/src/services/goalTemplates.ts b/src/services/goalTemplates.ts
index 427f547317..e7010cffa7 100644
--- a/src/services/goalTemplates.ts
+++ b/src/services/goalTemplates.ts
@@ -61,13 +61,16 @@ specified region.
export async function getCuratedTemplates(
grantIds: number[] | null,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- user: any,
+ user: any = null,
): Promise {
// Collect all the templates that either have a null regionId or a grant within the specified
// region.
// Check if the user has the monitoring feature flag.
- const hasGoalMonitoringOverride = !!(user && new Users(user).canSeeBehindFeatureFlag('monitoring_integration'));
+ let hasGoalMonitoringOverride = false;
+ if (user) {
+ hasGoalMonitoringOverride = !!(new Users(user).canSeeBehindFeatureFlag('monitoring_integration'));
+ }
// If they have the monitoring flag include monitoring goals.
let monitoringGoalIds = [];
if (hasGoalMonitoringOverride) {
From 6645cee03845a5161a87148ec54f33912b1e9006 Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Fri, 6 Dec 2024 15:29:20 -0500
Subject: [PATCH 069/198] Update query
---
src/services/monitoring.ts | 44 ++++++++++++++++++++++++++++++++++++++
1 file changed, 44 insertions(+)
diff --git a/src/services/monitoring.ts b/src/services/monitoring.ts
index d11970a250..1c49119d0e 100644
--- a/src/services/monitoring.ts
+++ b/src/services/monitoring.ts
@@ -1,4 +1,5 @@
/* eslint-disable max-len */
+import { Op } from 'sequelize';
import moment from 'moment';
import { uniq } from 'lodash';
import db from '../models';
@@ -43,13 +44,35 @@ export async function grantNumbersByRecipientAndRegion(recipientId: number, regi
return grants.map((grant) => grant.number);
}
+const MIN_DELIVERY_DATE = '2023-01-01';
+const REVIEW_STATUS_COMPLETE = 'Complete';
+
export async function ttaByReviews(
recipientId: number,
regionId: number,
): Promise {
const grantNumbers = await grantNumbersByRecipientAndRegion(recipientId, regionId) as string[];
const reviews = await MonitoringReview.findAll({
+ where: {
+ reportDeliveryDate: {
+ [Op.gte]: MIN_DELIVERY_DATE,
+ },
+ },
include: [
+ {
+ model: MonitoringReviewStatusLink,
+ as: 'statusLink',
+ include: [
+ {
+ model: MonitoringReviewStatus,
+ as: 'monitoringReviewStatuses',
+ required: true,
+ where: {
+ name: REVIEW_STATUS_COMPLETE,
+ },
+ },
+ ],
+ },
{
model: MonitoringReviewLink,
as: 'monitoringReviewLink',
@@ -221,6 +244,27 @@ export async function ttaByCitations(
model: MonitoringReview,
as: 'monitoringReviews',
required: true,
+ where: {
+ reportDeliveryDate: {
+ [Op.gte]: MIN_DELIVERY_DATE,
+ },
+ },
+ include: [
+ {
+ model: MonitoringReviewStatusLink,
+ as: 'statusLink',
+ include: [
+ {
+ model: MonitoringReviewStatus,
+ as: 'monitoringReviewStatuses',
+ required: true,
+ where: {
+ name: REVIEW_STATUS_COMPLETE,
+ },
+ },
+ ],
+ },
+ ],
},
{
model: MonitoringReviewGrantee,
From 398a59bc8d5c4c856226e76300c0c6fc9aafe9b5 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Fri, 6 Dec 2024 15:37:14 -0500
Subject: [PATCH 070/198] rework check for monitoring goal so we dont create
moniotring goals for grants that shouldnt have them
---
frontend/src/components/selectOptionsReset.js | 1 -
src/goalServices/goals.js | 40 ++++++++++---------
src/goalServices/goals.test.js | 15 ++++---
3 files changed, 31 insertions(+), 25 deletions(-)
diff --git a/frontend/src/components/selectOptionsReset.js b/frontend/src/components/selectOptionsReset.js
index ba332d6e54..0200f8fae5 100644
--- a/frontend/src/components/selectOptionsReset.js
+++ b/frontend/src/components/selectOptionsReset.js
@@ -17,7 +17,6 @@ const selectOptionsReset = {
groupHeading: (provided) => ({
...provided,
fontWeight: 'bold',
- fontFamily: 'SourceSansPro',
textTransform: 'capitalize',
fontSize: '14px',
color: colors.smartHubTextInk,
diff --git a/src/goalServices/goals.js b/src/goalServices/goals.js
index ffc05e1e57..238ff4b846 100644
--- a/src/goalServices/goals.js
+++ b/src/goalServices/goals.js
@@ -1296,33 +1296,37 @@ export async function saveGoalsForReport(goals, report) {
// Loop and Create or Update goals.
const currentGoals = await Promise.all(goals.map(async (goal, index) => {
// We need to skip creation of monitoring goals for non monitoring grants.
- if (goal.createdVia === 'monitoring') {
+ if (goal.goalTemplateId) {
+ const goalTemplate = await GoalTemplate.findByPk(goal.goalTemplateId);
+
+ if (goalTemplate.standard === 'Monitoring') {
// Find the corresponding monitoring goals.
- const monitoringGoals = await Goal.findAll({
- attributes: ['grantId'],
- raw: true,
- where: {
- grantId: goal.grantIds,
- createdVia: 'monitoring',
- status: { [Op.not]: GOAL_STATUS.CLOSED },
- },
- });
+ const monitoringGoals = await Goal.findAll({
+ attributes: ['grantId'],
+ raw: true,
+ where: {
+ grantId: goal.grantIds,
+ createdVia: 'monitoring',
+ status: { [Op.not]: GOAL_STATUS.CLOSED },
+ },
+ });
- const distinctMonitoringGoalGrantIds = [...new Set(
- monitoringGoals.map((monitoringGoal) => monitoringGoal.grantId),
- )];
+ const distinctMonitoringGoalGrantIds = [...new Set(
+ monitoringGoals.map((monitoringGoal) => monitoringGoal.grantId),
+ )];
- if (distinctMonitoringGoalGrantIds.length > 0) {
+ if (distinctMonitoringGoalGrantIds.length > 0) {
// Replace the goal granIds only with the grants that should have monitoring goals created.
// eslint-disable-next-line no-param-reassign
- goals[index].grantIds = distinctMonitoringGoalGrantIds;
- } else {
+ goals[index].grantIds = distinctMonitoringGoalGrantIds;
+ } else {
// Do not create monitoring goals for any of these recipients.
// eslint-disable-next-line no-param-reassign
// delete goals[index];
// eslint-disable-next-line no-param-reassign
- goals[index].grantIds = [];
- return [];
+ goals[index].grantIds = [];
+ return [];
+ }
}
}
diff --git a/src/goalServices/goals.test.js b/src/goalServices/goals.test.js
index 1994593290..faee00faea 100644
--- a/src/goalServices/goals.test.js
+++ b/src/goalServices/goals.test.js
@@ -614,27 +614,29 @@ describe('Goals DB service', () => {
objectives: [],
};
+ GoalTemplate.findByPk = jest.fn().mockResolvedValue({ standard: 'Monitoring' });
Goal.findAll = jest.fn().mockResolvedValue([{ ...mockMonitoringGoal }]);
await saveGoalsForReport([
{
- isNew: true, grantIds: [mockGrantId], name: 'name', status: 'In progress', objectives: [],
+ isNew: true, grantIds: [mockGrantId], name: 'Create Monitoring Goal', status: 'In progress', objectives: [], goalTemplateId: 1,
},
], { id: mockActivityReportId });
expect(Goal.create).toHaveBeenCalledWith(expect.objectContaining({
createdVia: 'activityReport',
grantId: mockGrantId,
- name: 'name',
+ name: 'Create Monitoring Goal',
status: 'In progress',
}), { individualHooks: true });
});
it('does not create a monitoring goal when the grant does not have an existing monitoring goal', async () => {
+ GoalTemplate.findByPk = jest.fn().mockResolvedValue({ standard: 'Monitoring' });
Goal.findAll = jest.fn().mockResolvedValue([]);
await saveGoalsForReport([
{
- isNew: true, grantIds: [mockGrantId], name: 'name', status: 'In progress', objectives: [],
+ isNew: true, grantIds: [mockGrantId], name: 'Dont create a monitoring goal', status: 'In progress', objectives: [], goalTemplateId: 1,
},
], { id: mockActivityReportId });
@@ -642,6 +644,7 @@ describe('Goals DB service', () => {
});
it('creates a monitoring goal for only the grants that has an existing monitoring goal', async () => {
+ GoalTemplate.findByPk = jest.fn().mockResolvedValue({ standard: 'Monitoring' });
const mockMonitoringGoal = {
id: 2,
grantId: 2,
@@ -657,7 +660,7 @@ describe('Goals DB service', () => {
await saveGoalsForReport([
{
- isNew: true, grantIds: [1, 2, 3], name: 'name', status: 'In progress', objectives: [],
+ isNew: true, grantIds: [1, 2, 3], name: 'Create some monitoring goals', status: 'In progress', objectives: [], goalTemplateId: 1,
},
], { id: mockActivityReportId });
@@ -665,13 +668,13 @@ describe('Goals DB service', () => {
{
createdVia: 'activityReport',
grantId: 2,
- name: 'name',
+ name: 'Create some monitoring goals',
status: 'In progress',
},
{
createdVia: 'activityReport',
grantId: 3,
- name: 'name',
+ name: 'Create some monitoring goals',
status: 'In progress',
},
), { individualHooks: true });
From 0ff128dfb8c26fb42be9d29e349decd774fb8d0f Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Sat, 7 Dec 2024 13:22:19 -0500
Subject: [PATCH 071/198] updates for citations
---
.../ActivityReport/Pages/components/GoalPicker.js | 10 +++++++---
src/goalServices/getGoalsForReport.ts | 1 +
2 files changed, 8 insertions(+), 3 deletions(-)
diff --git a/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js b/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js
index 117fb02eda..364fcdc6fb 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js
@@ -119,12 +119,12 @@ const GoalPicker = ({
async function fetchCitations() {
// If its a monitoring goal and the source is CLASS or RANs, fetch the citations.
if (goalForEditing && goalForEditing.standard && goalForEditing.standard === 'Monitoring') {
- const monitoringGrantIds = goalForEditing.goals.map((g) => g.grantId);
const retrievedCitationOptions = await fetchCitationsByGrant(
regionId,
- monitoringGrantIds,
+ grantIds,
startDate,
);
+
if (retrievedCitationOptions) {
// Reduce the citation options to only unique values.
const uniqueCitationOptions = Object.values(retrievedCitationOptions.reduce(
@@ -147,13 +147,17 @@ const GoalPicker = ({
return acc;
}, {},
));
+
setCitationOptions(uniqueCitationOptions);
setRawCitations(retrievedCitationOptions);
}
+ } else {
+ setCitationOptions([]);
+ setRawCitations([]);
}
}
fetchCitations();
- }, [goalForEditing, regionId, startDate]);
+ }, [goalForEditing, regionId, startDate, grantIds]);
const uniqueAvailableGoals = uniqBy(allAvailableGoals, 'name');
diff --git a/src/goalServices/getGoalsForReport.ts b/src/goalServices/getGoalsForReport.ts
index 45be9c00d4..813ec6e3c3 100644
--- a/src/goalServices/getGoalsForReport.ts
+++ b/src/goalServices/getGoalsForReport.ts
@@ -47,6 +47,7 @@ export default async function getGoalsForReport(reportId: number) {
[sequelize.col('grant.regionId'), 'regionId'],
[sequelize.col('grant.recipient.id'), 'recipientId'],
[sequelize.literal(`"goalTemplate"."creationMethod" = '${CREATION_METHOD.CURATED}'`), 'isCurated'],
+ [sequelize.literal('"goalTemplate"."standard"'), 'standard'],
[sequelize.literal(`(
SELECT
jsonb_agg( DISTINCT jsonb_build_object(
From 7383c7bc611ca37f071500aa23c41cce479cfdaf Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Sat, 7 Dec 2024 13:34:54 -0500
Subject: [PATCH 072/198] add citations to read only
---
frontend/src/components/GoalForm/ReadOnlyObjective.js | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/frontend/src/components/GoalForm/ReadOnlyObjective.js b/frontend/src/components/GoalForm/ReadOnlyObjective.js
index a3be96afcb..b53909d0f9 100644
--- a/frontend/src/components/GoalForm/ReadOnlyObjective.js
+++ b/frontend/src/components/GoalForm/ReadOnlyObjective.js
@@ -34,6 +34,14 @@ export default function ReadOnlyObjective({ objective }) {
{objective.title}
+ {objective.citations && objective.citations.length
+ ? (
+
+
Citations
+
{objective.citations.map((citation) => citation.name).join(', ')}
+
+ ) : null }
+
{objective.topics && objective.topics.length
? (
@@ -135,6 +143,9 @@ ReadOnlyObjective.propTypes = {
topics: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string,
})),
+ citations: PropTypes.arrayOf(PropTypes.shape({
+ name: PropTypes.string,
+ })),
courses: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string,
})),
From e7fa0c36a404d6fea60f93844384a70070fb0e7b Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Sat, 7 Dec 2024 13:41:11 -0500
Subject: [PATCH 073/198] remove comment for testing
---
src/goalServices/reduceGoals.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/goalServices/reduceGoals.ts b/src/goalServices/reduceGoals.ts
index ec613998ce..c6955540d8 100644
--- a/src/goalServices/reduceGoals.ts
+++ b/src/goalServices/reduceGoals.ts
@@ -187,7 +187,7 @@ export function reduceObjectivesForActivityReport(
(c) => ({
...c.dataValues,
id: c.monitoringReferences[0].standardId,
- name: `${c.monitoringReferences[0].acro} - ${c.citation} - ${c.monitoringReferences[0].findingType123}`,
+ name: `${c.monitoringReferences[0].acro} - ${c.citation} - ${c.monitoringReferences[0].findingType}`,
}),
))
: null;
@@ -278,7 +278,7 @@ export function reduceObjectivesForActivityReport(
{
...c.dataValues,
id: c.monitoringReferences[0].standardId,
- name: `${c.monitoringReferences[0].acro} - ${c.citation} - ${c.monitoringReferences[0].findingType}456`,
+ name: `${c.monitoringReferences[0].acro} - ${c.citation} - ${c.monitoringReferences[0].findingType}`,
}),
),
)
From c8274e360b9ff3dcbd5f3e37f0abddfef2b43301 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Sat, 7 Dec 2024 14:08:24 -0500
Subject: [PATCH 074/198] fix FE tets
---
.../ActivityReport/Pages/components/__tests__/GoalPicker.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/src/pages/ActivityReport/Pages/components/__tests__/GoalPicker.js b/frontend/src/pages/ActivityReport/Pages/components/__tests__/GoalPicker.js
index bda24efc8c..beeca4ce53 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/__tests__/GoalPicker.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/__tests__/GoalPicker.js
@@ -63,7 +63,7 @@ const GP = ({ availableGoals, selectedGoals, goalForEditing, goalTemplates }) =>
From c802c3c8eb432ab710c795c323cc5c833f8bf7b0 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Sat, 7 Dec 2024 14:13:44 -0500
Subject: [PATCH 075/198] see if moving rollback fixes connection issue
---
src/services/goalTemplates.test.js | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/services/goalTemplates.test.js b/src/services/goalTemplates.test.js
index b11558d447..c41e374371 100644
--- a/src/services/goalTemplates.test.js
+++ b/src/services/goalTemplates.test.js
@@ -17,7 +17,10 @@ const {
} = db;
describe('goalTemplates services', () => {
+ // Db snapshot.
+ let snapShot;
afterAll(async () => {
+ await rollbackToSnapshot(snapShot);
await sequelize.close();
});
@@ -33,9 +36,6 @@ describe('goalTemplates services', () => {
let monitoringTemplate;
let regularTemplate;
- // Db snapshot.
- let snapShot;
-
beforeAll(async () => {
snapShot = await captureSnapshot();
// Create recipient.
@@ -91,7 +91,7 @@ describe('goalTemplates services', () => {
});
afterAll(async () => {
- await rollbackToSnapshot(snapShot);
+ // await rollbackToSnapshot(snapShot);
});
it('returns only non-monitoring templates', async () => {
From 5935449e565990a5fdf486b357b0ba74146ba262 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Sat, 7 Dec 2024 14:41:41 -0500
Subject: [PATCH 076/198] remove unwanted change to get tests passing
---
src/goalServices/goals.js | 23 +++-----------------
src/goalServices/goals.test.js | 38 ---------------------------------
src/goalServices/reduceGoals.ts | 2 ++
3 files changed, 5 insertions(+), 58 deletions(-)
diff --git a/src/goalServices/goals.js b/src/goalServices/goals.js
index e96809e2f4..0334148617 100644
--- a/src/goalServices/goals.js
+++ b/src/goalServices/goals.js
@@ -692,7 +692,7 @@ export async function createOrUpdateGoals(goals) {
return goalsByIdAndRecipient(goalIds, recipient);
}
-export async function goalsForGrants(grantIds, reportStartDate, user) {
+export async function goalsForGrants(grantIds) {
/**
* get all the matching grants
*/
@@ -735,10 +735,10 @@ export async function goalsForGrants(grantIds, reportStartDate, user) {
.filter((g) => g)));
/*
- * Get all matching goals
+ * finally, return all matching goals
*/
- const regularGoals = await Goal.findAll({
+ return Goal.findAll({
attributes: [
[sequelize.fn(
'ARRAY_AGG',
@@ -840,23 +840,6 @@ export async function goalsForGrants(grantIds, reportStartDate, user) {
),
), 'desc']],
});
-
- /*
- * Get all monitoring goals
- */
- let goalsToReturn = regularGoals;
- const hasGoalMonitoringOverride = !!(user && new Users(user).canSeeBehindFeatureFlag('monitoring_integration'));
-
- if (hasGoalMonitoringOverride && reportStartDate) {
- const monitoringGoals = await getMonitoringGoals(ids, reportStartDate);
-
- // Combine goalsToReturn with monitoringGoals.
- const allGoals = await Promise.all([regularGoals, monitoringGoals]);
-
- // Flatten the array of arrays.
- goalsToReturn = allGoals.flat();
- }
- return goalsToReturn;
}
async function removeActivityReportObjectivesFromReport(reportId, objectiveIdsToRemove) {
diff --git a/src/goalServices/goals.test.js b/src/goalServices/goals.test.js
index fb2f2caf9f..1994593290 100644
--- a/src/goalServices/goals.test.js
+++ b/src/goalServices/goals.test.js
@@ -50,7 +50,6 @@ import {
import {
mergeCollaborators,
} from '../models/helpers/genericCollaborator';
-import { getMonitoringGoals } from '../services/citations';
jest.mock('./changeGoalStatus', () => ({
__esModule: true,
@@ -1526,43 +1525,6 @@ describe('Goals DB service', () => {
'$grant.grantRelationships.activeGrantId$': [505, 506],
});
});
-
- it('does not return monitoring goals if the user is missing the feature flag', async () => {
- Grant.findAll = jest.fn();
- Grant.findAll.mockResolvedValue([{ id: 505, oldGrantId: 506 }]);
- Goal.findAll = jest.fn();
- Goal.findAll.mockResolvedValue([{ id: 505 }, { id: 506 }]);
-
- await goalsForGrants([506]);
-
- const { where } = Goal.findAll.mock.calls[0][0];
- expect(where['$grant.id$']).toStrictEqual([
- 505,
- 506,
- ]);
- });
-
- it('returns monitoring goals if the user has the feature flag', async () => {
- Grant.findAll = jest.fn();
- Grant.findAll.mockResolvedValue([{ id: 505, oldGrantId: 506 }]);
- Goal.findAll = jest.fn();
- Goal.findAll.mockResolvedValue([{ id: 505 }, { id: 506 }]);
-
- // Mock getMonitoringGoals to return a list of monitoring goals.
- getMonitoringGoals.mockResolvedValue([{ id: 507 }]);
-
- // Mock the feature flag function canSeeBehindFeatureFlag in user to return true.
- const result = await goalsForGrants([506], '2024-11-27', {
- flags: ['monitoring_integration'],
- });
-
- // Assert result contains the goals we expect including the monitoring goal.
- expect(result).toEqual([
- { id: 505 },
- { id: 506 },
- { id: 507 },
- ]);
- });
});
describe('createMultiRecipientGoalsFromAdmin', () => {
diff --git a/src/goalServices/reduceGoals.ts b/src/goalServices/reduceGoals.ts
index 2638dd8ed0..27b029d156 100644
--- a/src/goalServices/reduceGoals.ts
+++ b/src/goalServices/reduceGoals.ts
@@ -181,6 +181,7 @@ export function reduceObjectivesForActivityReport(
exists.citations = uniq(objective.activityReportObjectives
&& objective.activityReportObjectives.length > 0
+ && objective.activityReportObjectives[0].activityReportObjectiveCitations
? objective.activityReportObjectives[0].activityReportObjectiveCitations.map(
(c) => c.citation,
)
@@ -264,6 +265,7 @@ export function reduceObjectivesForActivityReport(
citations: uniq(
objective.activityReportObjectives
&& objective.activityReportObjectives.length > 0
+ && objective.activityReportObjectives[0].activityReportObjectiveCitations
? objective.activityReportObjectives[0].activityReportObjectiveCitations.map(
(c) => c.citation,
)
From 2d8c2203534e4c8834db7d16a9034886e1c1b3e9 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Sat, 7 Dec 2024 15:08:34 -0500
Subject: [PATCH 077/198] update migration names
---
...8-AddMonitoringEnum.js => 20241207194714-AddMonitoringEnum.js} | 0
... => 20241207194714-add-activity-report-objective-citations.js} | 0
...-import.js => 20241207194714-add-post-processing-to-import.js} | 0
....js => 20241207194714-create-standard-goal-template-column.js} | 0
4 files changed, 0 insertions(+), 0 deletions(-)
rename src/migrations/{20241115143738-AddMonitoringEnum.js => 20241207194714-AddMonitoringEnum.js} (100%)
rename src/migrations/{20241118093025-add-activity-report-objective-citations.js => 20241207194714-add-activity-report-objective-citations.js} (100%)
rename src/migrations/{20241115203616-add-post-processing-to-import.js => 20241207194714-add-post-processing-to-import.js} (100%)
rename src/migrations/{20241120031623-create-standard-goal-template-column.js => 20241207194714-create-standard-goal-template-column.js} (100%)
diff --git a/src/migrations/20241115143738-AddMonitoringEnum.js b/src/migrations/20241207194714-AddMonitoringEnum.js
similarity index 100%
rename from src/migrations/20241115143738-AddMonitoringEnum.js
rename to src/migrations/20241207194714-AddMonitoringEnum.js
diff --git a/src/migrations/20241118093025-add-activity-report-objective-citations.js b/src/migrations/20241207194714-add-activity-report-objective-citations.js
similarity index 100%
rename from src/migrations/20241118093025-add-activity-report-objective-citations.js
rename to src/migrations/20241207194714-add-activity-report-objective-citations.js
diff --git a/src/migrations/20241115203616-add-post-processing-to-import.js b/src/migrations/20241207194714-add-post-processing-to-import.js
similarity index 100%
rename from src/migrations/20241115203616-add-post-processing-to-import.js
rename to src/migrations/20241207194714-add-post-processing-to-import.js
diff --git a/src/migrations/20241120031623-create-standard-goal-template-column.js b/src/migrations/20241207194714-create-standard-goal-template-column.js
similarity index 100%
rename from src/migrations/20241120031623-create-standard-goal-template-column.js
rename to src/migrations/20241207194714-create-standard-goal-template-column.js
From e3ad8113a2d8da8bc74d54f12932559a4da345c0 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Mon, 9 Dec 2024 08:58:53 -0500
Subject: [PATCH 078/198] see if this fixes the sequelize close connection
issue
---
src/services/goalTemplates.test.js | 31 ++++++++++++++++++++++++------
1 file changed, 25 insertions(+), 6 deletions(-)
diff --git a/src/services/goalTemplates.test.js b/src/services/goalTemplates.test.js
index c41e374371..067c9cf70d 100644
--- a/src/services/goalTemplates.test.js
+++ b/src/services/goalTemplates.test.js
@@ -4,7 +4,6 @@ import crypto from 'crypto';
import db from '../models';
import { setFieldPromptsForCuratedTemplate, getSourceFromTemplate, getCuratedTemplates } from './goalTemplates';
import { AUTOMATIC_CREATION } from '../constants';
-import { captureSnapshot, rollbackToSnapshot } from '../lib/programmaticTransaction';
const {
Goal,
@@ -17,10 +16,7 @@ const {
} = db;
describe('goalTemplates services', () => {
- // Db snapshot.
- let snapShot;
afterAll(async () => {
- await rollbackToSnapshot(snapShot);
await sequelize.close();
});
@@ -37,7 +33,6 @@ describe('goalTemplates services', () => {
let regularTemplate;
beforeAll(async () => {
- snapShot = await captureSnapshot();
// Create recipient.
monitoringRecipient = await Recipient.create({
id: faker.datatype.number({ min: 56000 }),
@@ -91,7 +86,31 @@ describe('goalTemplates services', () => {
});
afterAll(async () => {
- // await rollbackToSnapshot(snapShot);
+ // Delete the goals.
+ await Goal.destroy({
+ where: {
+ goalTemplateId: [monitoringTemplate.id, regularTemplate.id],
+ },
+ force: true,
+ paranoid: true,
+ individualHooks: true,
+ });
+
+ // Delete the grants.
+ await Grant.destroy({
+ where: {
+ id: [monitoringGrant.id, regularGrant.id],
+ },
+ individualHooks: true,
+ });
+
+ // Delete the recipients.
+ await Recipient.destroy({
+ where: {
+ id: [monitoringRecipient.id, regularRecipient.id],
+ },
+ individualHooks: true,
+ });
});
it('returns only non-monitoring templates', async () => {
From 2d22d8ceebff676a81dd8e3f64594499e7cbf1c7 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Mon, 9 Dec 2024 09:32:01 -0500
Subject: [PATCH 079/198] fix parens
---
src/services/goalTemplates.test.js | 440 ++++++++++++++---------------
1 file changed, 220 insertions(+), 220 deletions(-)
diff --git a/src/services/goalTemplates.test.js b/src/services/goalTemplates.test.js
index 067c9cf70d..e56be189bc 100644
--- a/src/services/goalTemplates.test.js
+++ b/src/services/goalTemplates.test.js
@@ -141,273 +141,273 @@ describe('goalTemplates services', () => {
expect(monitoringTemplateToAssert).toBeTruthy();
});
});
-});
-describe('getSourceFromTemplate', () => {
- let template;
- let templateTwo;
- let grant;
- let grantTwo;
- let grantThree;
- let recipient;
-
- beforeAll(async () => {
- recipient = await Recipient.create({
- id: faker.datatype.number({ min: 56000 }),
- name: faker.datatype.string(20),
- });
+ describe('getSourceFromTemplate', () => {
+ let template;
+ let templateTwo;
+ let grant;
+ let grantTwo;
+ let grantThree;
+ let recipient;
- grant = await Grant.create({
- regionId: 2,
- status: 'Active',
- id: faker.datatype.number({ min: 56000 }),
- number: faker.datatype.string(255),
- recipientId: recipient.id,
- });
+ beforeAll(async () => {
+ recipient = await Recipient.create({
+ id: faker.datatype.number({ min: 56000 }),
+ name: faker.datatype.string(20),
+ });
- grantTwo = await Grant.create({
- regionId: 2,
- status: 'Active',
- id: faker.datatype.number({ min: 56000 }),
- number: faker.datatype.string(255),
- recipientId: recipient.id,
- });
+ grant = await Grant.create({
+ regionId: 2,
+ status: 'Active',
+ id: faker.datatype.number({ min: 56000 }),
+ number: faker.datatype.string(255),
+ recipientId: recipient.id,
+ });
- grantThree = await Grant.create({
- regionId: 2,
- status: 'Active',
- id: faker.datatype.number({ min: 56000 }),
- number: faker.datatype.string(255),
- recipientId: recipient.id,
- });
+ grantTwo = await Grant.create({
+ regionId: 2,
+ status: 'Active',
+ id: faker.datatype.number({ min: 56000 }),
+ number: faker.datatype.string(255),
+ recipientId: recipient.id,
+ });
- const n = faker.lorem.sentence(5);
+ grantThree = await Grant.create({
+ regionId: 2,
+ status: 'Active',
+ id: faker.datatype.number({ min: 56000 }),
+ number: faker.datatype.string(255),
+ recipientId: recipient.id,
+ });
- const secret = 'secret';
- const hash = crypto
- .createHmac('md5', secret)
- .update(n)
- .digest('hex');
+ const n = faker.lorem.sentence(5);
- template = await GoalTemplate.create({
- hash,
- templateName: n,
- creationMethod: AUTOMATIC_CREATION,
- source: GOAL_SOURCES[1],
- });
+ const secret = 'secret';
+ const hash = crypto
+ .createHmac('md5', secret)
+ .update(n)
+ .digest('hex');
- await Goal.create({
- grantId: grant.id,
- goalTemplateId: template.id,
- name: n,
- source: GOAL_SOURCES[0],
- });
+ template = await GoalTemplate.create({
+ hash,
+ templateName: n,
+ creationMethod: AUTOMATIC_CREATION,
+ source: GOAL_SOURCES[1],
+ });
- await Goal.create({
- grantId: grantTwo.id,
- goalTemplateId: template.id,
- name: n,
- });
+ await Goal.create({
+ grantId: grant.id,
+ goalTemplateId: template.id,
+ name: n,
+ source: GOAL_SOURCES[0],
+ });
- const n2 = faker.lorem.sentence(5);
+ await Goal.create({
+ grantId: grantTwo.id,
+ goalTemplateId: template.id,
+ name: n,
+ });
- const hash2 = crypto
- .createHmac('md5', secret)
- .update(n2)
- .digest('hex');
+ const n2 = faker.lorem.sentence(5);
- templateTwo = await GoalTemplate.create({
- hash: hash2,
- templateName: n2,
- creationMethod: AUTOMATIC_CREATION,
- });
+ const hash2 = crypto
+ .createHmac('md5', secret)
+ .update(n2)
+ .digest('hex');
- await Goal.create({
- grantId: grantThree.id,
- goalTemplateId: templateTwo.id,
- name: n2,
- });
- });
+ templateTwo = await GoalTemplate.create({
+ hash: hash2,
+ templateName: n2,
+ creationMethod: AUTOMATIC_CREATION,
+ });
- afterAll(async () => {
- await Goal.destroy({
- where: {
- goalTemplateId: [
- template.id,
- templateTwo.id,
- ],
- },
- force: true,
- paranoid: true,
- individualHooks: true,
+ await Goal.create({
+ grantId: grantThree.id,
+ goalTemplateId: templateTwo.id,
+ name: n2,
+ });
});
- await GoalTemplate.destroy({
- where: {
- id: [
- template.id,
- templateTwo.id],
- },
- individualHooks: true,
+
+ afterAll(async () => {
+ await Goal.destroy({
+ where: {
+ goalTemplateId: [
+ template.id,
+ templateTwo.id,
+ ],
+ },
+ force: true,
+ paranoid: true,
+ individualHooks: true,
+ });
+ await GoalTemplate.destroy({
+ where: {
+ id: [
+ template.id,
+ templateTwo.id],
+ },
+ individualHooks: true,
+ });
+ await Grant.destroy({
+ where: {
+ id: [
+ grant.id,
+ grantTwo.id,
+ grantThree.id,
+ ],
+ },
+ individualHooks: true,
+ });
+ await Recipient.destroy({ where: { id: recipient.id }, individualHooks: true });
});
- await Grant.destroy({
- where: {
- id: [
- grant.id,
- grantTwo.id,
- grantThree.id,
- ],
- },
- individualHooks: true,
+
+ it('returns source from the goal', async () => {
+ const source = await getSourceFromTemplate(template.id, [grant.id]);
+
+ expect(source).toBe(GOAL_SOURCES[0]);
});
- await Recipient.destroy({ where: { id: recipient.id }, individualHooks: true });
- });
+ it('returns source from the template', async () => {
+ const source = await getSourceFromTemplate(template.id, [grantTwo.id]);
- it('returns source from the goal', async () => {
- const source = await getSourceFromTemplate(template.id, [grant.id]);
+ expect(source).toBe(GOAL_SOURCES[1]);
+ });
- expect(source).toBe(GOAL_SOURCES[0]);
- });
- it('returns source from the template', async () => {
- const source = await getSourceFromTemplate(template.id, [grantTwo.id]);
+ it('returns null source', async () => {
+ const source = await getSourceFromTemplate(template.id, [grantThree.id]);
- expect(source).toBe(GOAL_SOURCES[1]);
+ expect(source).toBeFalsy();
+ });
});
- it('returns null source', async () => {
- const source = await getSourceFromTemplate(template.id, [grantThree.id]);
+ describe('setFieldPromptsForCuratedTemplate', () => {
+ let promptResponses;
+ let template;
+ let goalIds;
+ let grant;
+ let recipient;
+ let promptId;
+ let promptTitle;
- expect(source).toBeFalsy();
- });
-});
+ beforeAll(async () => {
+ recipient = await Recipient.create({
+ id: faker.datatype.number({ min: 56000 }),
+ name: faker.datatype.string(20),
+ });
-describe('setFieldPromptsForCuratedTemplate', () => {
- let promptResponses;
- let template;
- let goalIds;
- let grant;
- let recipient;
- let promptId;
- let promptTitle;
-
- beforeAll(async () => {
- recipient = await Recipient.create({
- id: faker.datatype.number({ min: 56000 }),
- name: faker.datatype.string(20),
- });
+ grant = await Grant.create({
+ regionId: 2,
+ status: 'Active',
+ id: faker.datatype.number({ min: 56000 }),
+ number: faker.datatype.string(255),
+ recipientId: recipient.id,
+ });
- grant = await Grant.create({
- regionId: 2,
- status: 'Active',
- id: faker.datatype.number({ min: 56000 }),
- number: faker.datatype.string(255),
- recipientId: recipient.id,
- });
+ const n = faker.lorem.sentence(5);
- const n = faker.lorem.sentence(5);
+ const secret = 'secret';
+ const hash = crypto
+ .createHmac('md5', secret)
+ .update(n)
+ .digest('hex');
- const secret = 'secret';
- const hash = crypto
- .createHmac('md5', secret)
- .update(n)
- .digest('hex');
+ template = await GoalTemplate.create({
+ hash,
+ templateName: n,
+ creationMethod: AUTOMATIC_CREATION,
+ });
- template = await GoalTemplate.create({
- hash,
- templateName: n,
- creationMethod: AUTOMATIC_CREATION,
- });
+ promptTitle = faker.datatype.string(255);
+
+ const prompt = await GoalTemplateFieldPrompt.create({
+ goalTemplateId: template.id,
+ ordinal: 1,
+ title: promptTitle,
+ prompt: promptTitle,
+ hint: '',
+ options: ['option 1', 'option 2', 'option 3'],
+ fieldType: 'multiselect',
+ validations: { required: 'Select a root cause', rules: [{ name: 'maxSelections', value: 2, message: 'You can only select 2 options' }] },
+ });
- promptTitle = faker.datatype.string(255);
-
- const prompt = await GoalTemplateFieldPrompt.create({
- goalTemplateId: template.id,
- ordinal: 1,
- title: promptTitle,
- prompt: promptTitle,
- hint: '',
- options: ['option 1', 'option 2', 'option 3'],
- fieldType: 'multiselect',
- validations: { required: 'Select a root cause', rules: [{ name: 'maxSelections', value: 2, message: 'You can only select 2 options' }] },
- });
+ promptId = prompt.id;
- promptId = prompt.id;
+ promptResponses = [
+ { promptId: prompt.id, response: ['option 1', 'option 2'] },
+ ];
- promptResponses = [
- { promptId: prompt.id, response: ['option 1', 'option 2'] },
- ];
+ const goal = await Goal.create({
+ grantId: grant.id,
+ goalTemplateId: template.id,
+ name: n,
+ });
- const goal = await Goal.create({
- grantId: grant.id,
- goalTemplateId: template.id,
- name: n,
+ goalIds = [goal.id];
});
- goalIds = [goal.id];
- });
-
- afterAll(async () => {
- await GoalFieldResponse.destroy({ where: { goalId: goalIds }, individualHooks: true });
- // eslint-disable-next-line max-len
- await GoalTemplateFieldPrompt.destroy({ where: { goalTemplateId: template.id }, individualHooks: true });
- await Goal.destroy({
- where: { goalTemplateId: template.id }, force: true, paranoid: true, individualHooks: true,
+ afterAll(async () => {
+ await GoalFieldResponse.destroy({ where: { goalId: goalIds }, individualHooks: true });
+ // eslint-disable-next-line max-len
+ await GoalTemplateFieldPrompt.destroy({ where: { goalTemplateId: template.id }, individualHooks: true });
+ await Goal.destroy({
+ where: { goalTemplateId: template.id }, force: true, paranoid: true, individualHooks: true,
+ });
+ await GoalTemplate.destroy({ where: { id: template.id }, individualHooks: true });
+ await Grant.destroy({ where: { id: grant.id }, individualHooks: true });
+ await Recipient.destroy({ where: { id: recipient.id }, individualHooks: true });
});
- await GoalTemplate.destroy({ where: { id: template.id }, individualHooks: true });
- await Grant.destroy({ where: { id: grant.id }, individualHooks: true });
- await Recipient.destroy({ where: { id: recipient.id }, individualHooks: true });
- });
- it('should call setFieldPromptForCuratedTemplate for each prompt response', async () => {
+ it('should call setFieldPromptForCuratedTemplate for each prompt response', async () => {
// save initial field responses
- await setFieldPromptsForCuratedTemplate(goalIds, promptResponses);
+ await setFieldPromptsForCuratedTemplate(goalIds, promptResponses);
- // check that the field responses were saved
- const fieldResponses = await GoalFieldResponse.findAll({
- where: { goalId: goalIds },
- });
+ // check that the field responses were saved
+ const fieldResponses = await GoalFieldResponse.findAll({
+ where: { goalId: goalIds },
+ });
+
+ expect(fieldResponses.length).toBe(promptResponses.length);
+ expect(fieldResponses[0].response).toEqual(promptResponses[0].response);
- expect(fieldResponses.length).toBe(promptResponses.length);
- expect(fieldResponses[0].response).toEqual(promptResponses[0].response);
+ // update field responses
+ await setFieldPromptsForCuratedTemplate(goalIds, [
+ { promptId, response: ['option 1'] },
+ ]);
- // update field responses
- await setFieldPromptsForCuratedTemplate(goalIds, [
- { promptId, response: ['option 1'] },
- ]);
+ // check that the field responses were updated
+ const updatedFieldResponses = await GoalFieldResponse.findAll({
+ where: { goalId: goalIds },
+ });
- // check that the field responses were updated
- const updatedFieldResponses = await GoalFieldResponse.findAll({
- where: { goalId: goalIds },
+ expect(updatedFieldResponses.length).toBe(promptResponses.length);
+ expect(updatedFieldResponses[0].response).toEqual(['option 1']);
});
- expect(updatedFieldResponses.length).toBe(promptResponses.length);
- expect(updatedFieldResponses[0].response).toEqual(['option 1']);
- });
+ it('should use the provided validations', async () => {
+ const fieldResponses = await GoalFieldResponse.findAll({
+ where: { goalId: goalIds },
+ raw: true,
+ });
- it('should use the provided validations', async () => {
- const fieldResponses = await GoalFieldResponse.findAll({
- where: { goalId: goalIds },
- raw: true,
- });
+ // test validation error (no more than 2 options can be selected)
+ await expect(setFieldPromptsForCuratedTemplate(goalIds, [
+ { promptId, response: ['option 1', 'option 2', 'option 3'] },
+ ])).rejects.toThrow();
- // test validation error (no more than 2 options can be selected)
- await expect(setFieldPromptsForCuratedTemplate(goalIds, [
- { promptId, response: ['option 1', 'option 2', 'option 3'] },
- ])).rejects.toThrow();
+ // check that the field responses were not updated
+ const notUpdatedFieldResponses = await GoalFieldResponse.findAll({
+ where: { goalId: goalIds },
+ raw: true,
+ });
- // check that the field responses were not updated
- const notUpdatedFieldResponses = await GoalFieldResponse.findAll({
- where: { goalId: goalIds },
- raw: true,
+ expect(notUpdatedFieldResponses.length).toBe(fieldResponses.length);
+ expect(notUpdatedFieldResponses[0].response).toStrictEqual(fieldResponses[0].response);
});
- expect(notUpdatedFieldResponses.length).toBe(fieldResponses.length);
- expect(notUpdatedFieldResponses[0].response).toStrictEqual(fieldResponses[0].response);
- });
-
- it('does nothing if the prompt doesn\'t exist', async () => {
- const fictionalId = 123454345345;
- await expect(setFieldPromptsForCuratedTemplate(goalIds, [
- { promptId: fictionalId, response: ['option 1'] },
- ])).rejects.toThrow(`No prompt found with ID ${fictionalId}`);
+ it('does nothing if the prompt doesn\'t exist', async () => {
+ const fictionalId = 123454345345;
+ await expect(setFieldPromptsForCuratedTemplate(goalIds, [
+ { promptId: fictionalId, response: ['option 1'] },
+ ])).rejects.toThrow(`No prompt found with ID ${fictionalId}`);
+ });
});
});
From a60b1beb8756eb63541987d2fc07fc545b474644 Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Mon, 9 Dec 2024 10:46:53 -0500
Subject: [PATCH 080/198] Not started > Not Started
---
src/tools/createMonitoringGoals.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/tools/createMonitoringGoals.js b/src/tools/createMonitoringGoals.js
index 18bf07586b..6dc72a8744 100644
--- a/src/tools/createMonitoringGoals.js
+++ b/src/tools/createMonitoringGoals.js
@@ -5,7 +5,7 @@ import {
import { auditLogger } from '../logger';
const createMonitoringGoals = async () => {
- const cutOffDate = '2024-11-26'; // TODO: Set this before we deploy to prod.
+ const cutOffDate = '2024-10-01'; // TODO: Set this before we deploy to prod.
// Verify that the monitoring goal template exists.
const monitoringGoalTemplate = await GoalTemplate.findOne({
@@ -71,7 +71,7 @@ const createMonitoringGoals = async () => {
new_goals AS (
SELECT
gt."templateName" "name",
- 'Not started' "status",
+ 'Not Started' "status",
NULL "timeframe",
FALSE "isFromSmartsheetTtaPlan",
NOW() "createdAt",
From 1890fa7df7ad77d28cde56817a0b87fab4dd58de Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Mon, 9 Dec 2024 10:49:59 -0500
Subject: [PATCH 081/198] revert date change
---
src/tools/createMonitoringGoals.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/tools/createMonitoringGoals.js b/src/tools/createMonitoringGoals.js
index 6dc72a8744..0d53df0122 100644
--- a/src/tools/createMonitoringGoals.js
+++ b/src/tools/createMonitoringGoals.js
@@ -5,7 +5,7 @@ import {
import { auditLogger } from '../logger';
const createMonitoringGoals = async () => {
- const cutOffDate = '2024-10-01'; // TODO: Set this before we deploy to prod.
+ const cutOffDate = '2024-11-26'; // TODO: Set this before we deploy to prod.
// Verify that the monitoring goal template exists.
const monitoringGoalTemplate = await GoalTemplate.findOne({
From 7948c46e35b5a2c7c7dbb1421a4e5ed810693acf Mon Sep 17 00:00:00 2001
From: Adam Levin <84350609+AdamAdHocTeam@users.noreply.github.com>
Date: Mon, 9 Dec 2024 10:58:31 -0500
Subject: [PATCH 082/198] Update
frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js
Co-authored-by: Matt Bevilacqua
---
.../src/pages/ActivityReport/Pages/components/GoalPicker.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js b/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js
index 364fcdc6fb..83cce22265 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js
@@ -115,7 +115,7 @@ const GoalPicker = ({
}, []);
// Fetch citations for the goal if the source is CLASS or RANs.
- useEffect(() => {
+ useDeepCompareEffect(() => {
async function fetchCitations() {
// If its a monitoring goal and the source is CLASS or RANs, fetch the citations.
if (goalForEditing && goalForEditing.standard && goalForEditing.standard === 'Monitoring') {
From fbbd76b587fea4c42eee0f81cdc42eb02d870f65 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Mon, 9 Dec 2024 11:01:53 -0500
Subject: [PATCH 083/198] fix selected optiosn
---
.../ActivityReport/Pages/components/Objective.js | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
diff --git a/frontend/src/pages/ActivityReport/Pages/components/Objective.js b/frontend/src/pages/ActivityReport/Pages/components/Objective.js
index f59d318929..135db27e5f 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/Objective.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/Objective.js
@@ -353,7 +353,6 @@ export default function Objective({
// Store the complete citation in ActivityReportObjectiveCitations in the DB row.
const selectedCitationsChanged = (newCitations) => {
const newCitationStandardIds = newCitations.map((newCitation) => newCitation.id);
-
// From rawCitations get all the raw citations with the same standardId as the newCitations.
const newCitationsObjects = rawCitations.filter(
(rawCitation) => newCitationStandardIds.includes(rawCitation.standardId),
@@ -364,7 +363,16 @@ export default function Objective({
name: newCitations.find(
(newCitation) => newCitation.id === rawCitation.standardId,
).name,
- monitoringReferences: rawCitation.grants,
+ monitoringReferences:
+ [
+ ...rawCitation.grants.map((grant) => ({
+ ...grant,
+ standardId: rawCitation.standardId,
+ name: newCitations.find(
+ (newCitation) => newCitation.id === rawCitation.standardId,
+ ).name,
+ })),
+ ],
}));
onChangeCitations([...newCitationsObjects]);
};
From e5139aa71acd364c3f843e711b638aadac4f68ed Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Mon, 9 Dec 2024 11:10:29 -0500
Subject: [PATCH 084/198] add missing import from git commit
---
frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js | 1 +
1 file changed, 1 insertion(+)
diff --git a/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js b/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js
index 83cce22265..8d37f2eca0 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js
@@ -1,4 +1,5 @@
import React, { useState, useEffect, useRef } from 'react';
+import useDeepCompareEffect from 'use-deep-compare-effect';
import { v4 as uuidv4 } from 'uuid';
import { uniqBy } from 'lodash';
import PropTypes from 'prop-types';
From f454213ca6386ec7d06ac34be7de7f3f64dd777b Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Mon, 9 Dec 2024 11:24:19 -0500
Subject: [PATCH 085/198] change per Matt
---
frontend/src/fetchers/citations.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/src/fetchers/citations.js b/frontend/src/fetchers/citations.js
index f7747dd6bd..51a663722b 100644
--- a/frontend/src/fetchers/citations.js
+++ b/frontend/src/fetchers/citations.js
@@ -12,7 +12,7 @@ export async function fetchCitationsByGrant(region, grantIds, reportStartDate) {
'citations',
'region',
String(region),
- `?grantIds=${grantIds.join('&')}&reportStartDate=${formattedDate}`,
+ `?grantIds=${grantIds.join('&grantIds=')}&reportStartDate=${formattedDate}`,
);
const citations = await get(url);
return citations.json();
From 2ca6a7d80b1fc23e3557aa22b52c67db3dfe5cc6 Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Mon, 9 Dec 2024 11:27:46 -0500
Subject: [PATCH 086/198] Hide monitoring goals behind a feature flag
---
src/policies/user.js | 2 +-
src/routes/recipient/handlers.js | 7 ++++++-
src/services/recipient.js | 10 ++++++++++
3 files changed, 17 insertions(+), 2 deletions(-)
diff --git a/src/policies/user.js b/src/policies/user.js
index 708871bcb1..c99fb56f0f 100644
--- a/src/policies/user.js
+++ b/src/policies/user.js
@@ -33,7 +33,7 @@ export default class Users {
* @returns {bool} whether the user can view the feature flag
*/
canSeeBehindFeatureFlag(flag) {
- return this.isAdmin() || this.user.flags.find((f) => f === flag);
+ return this.isAdmin() || !!(this.user.flags.find((f) => f === flag));
}
getAllAccessibleRegions() {
diff --git a/src/routes/recipient/handlers.js b/src/routes/recipient/handlers.js
index d18419b074..71e08e1dde 100644
--- a/src/routes/recipient/handlers.js
+++ b/src/routes/recipient/handlers.js
@@ -123,11 +123,16 @@ export async function getGoalsByRecipient(req, res) {
const { recipientId, regionId } = req.params;
+ const policy = new Users(await userById(await currentUserId(req, res)));
+
// Get goals for recipient.
const recipientGoals = await getGoalsByActivityRecipient(
recipientId,
regionId,
- req.query,
+ {
+ ...req.query,
+ excludeMonitoringGoals: !(policy.canSeeBehindFeatureFlag('monitoring_integration')),
+ },
);
res.json(recipientGoals);
} catch (error) {
diff --git a/src/services/recipient.js b/src/services/recipient.js
index 937dc9cf7b..a242603945 100644
--- a/src/services/recipient.js
+++ b/src/services/recipient.js
@@ -540,6 +540,7 @@ export async function getGoalsByActivityRecipient(
offset = 0,
limit = GOALS_PER_PAGE,
goalIds = [],
+ excludeMonitoringGoals = true,
...filters
},
) {
@@ -587,6 +588,15 @@ export async function getGoalsByActivityRecipient(
};
}
+ if (excludeMonitoringGoals) {
+ goalWhere = {
+ ...goalWhere,
+ createdVia: {
+ [Op.not]: 'monitoring',
+ },
+ };
+ }
+
// goal IDS can be a string or an array of strings
// or undefined
// we also want at least one value here
From f7e37a37e808892ddd6e418cd231f4927ef41c0c Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Mon, 9 Dec 2024 11:30:54 -0500
Subject: [PATCH 087/198] Fix test
---
.../GoalCards/components/GoalStatusDropdown.js | 2 +-
src/tools/createMonitoringGoals.test.js | 12 ++++++------
2 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/frontend/src/components/GoalCards/components/GoalStatusDropdown.js b/frontend/src/components/GoalCards/components/GoalStatusDropdown.js
index 8e2bd69645..235ca5b5d8 100644
--- a/frontend/src/components/GoalCards/components/GoalStatusDropdown.js
+++ b/frontend/src/components/GoalCards/components/GoalStatusDropdown.js
@@ -17,7 +17,7 @@ export default function GoalStatusDropdown({
}) {
const { user } = useContext(UserContext);
const key = status || 'Needs Status';
- const { icon, display } = STATUSES[key];
+ const { icon, display } = STATUSES[key] || STATUSES['Needs Status'];
const isReadOnly = useMemo(() => ((
status === 'Draft'
diff --git a/src/tools/createMonitoringGoals.test.js b/src/tools/createMonitoringGoals.test.js
index 5d1ddac60c..2d44eece5c 100644
--- a/src/tools/createMonitoringGoals.test.js
+++ b/src/tools/createMonitoringGoals.test.js
@@ -1635,7 +1635,7 @@ describe('createMonitoringGoals', () => {
name: goalTemplateName,
grantId: grantBeingMonitoredSplit10A.id,
goalTemplateId: goalTemplate.id,
- status: 'Not started',
+ status: 'Not Started',
});
// Create a existing monitoring goal for Case 11 on Grant A and B.
@@ -1644,7 +1644,7 @@ describe('createMonitoringGoals', () => {
name: goalTemplateName,
grantId: grantBeingMerged11A.id,
goalTemplateId: goalTemplate.id,
- status: 'Not started',
+ status: 'Not Started',
});
await Goal.create({
@@ -1652,7 +1652,7 @@ describe('createMonitoringGoals', () => {
name: goalTemplateName,
grantId: grantBeingMerged11B.id,
goalTemplateId: goalTemplate.id,
- status: 'Not started',
+ status: 'Not Started',
});
// Create a monitoring goal for grantReopenMonitoringGoalNumberReviewId12 in case 12 thats closed and should be set to Not started.
@@ -1695,7 +1695,7 @@ describe('createMonitoringGoals', () => {
// Assert that the goal that was created was the monitoring goal and is using the correct template.
expect(grant1Goals[0].goalTemplateId).toBe(goalTemplate.id);
expect(grant1Goals[0].name).toBe(goalTemplateName);
- expect(grant1Goals[0].status).toBe('Not started');
+ expect(grant1Goals[0].status).toBe('Not Started');
// CASE 2: Does not create a monitoring goal for a grant that already has one.
const grant2Goals = await Goal.findAll({ where: { grantId: grantThatAlreadyHasMonitoringGoal2.id } });
@@ -1733,7 +1733,7 @@ describe('createMonitoringGoals', () => {
expect(grant9Goals.length).toBe(1);
expect(grant9Goals[0].goalTemplateId).toBe(goalTemplate.id);
expect(grant9Goals[0].name).toBe(goalTemplateName);
- expect(grant9Goals[0].status).toBe('Not started');
+ expect(grant9Goals[0].status).toBe('Not Started');
// CASE 10: Creates a monitoring goal ONLY for the grant that initially had the monitoring goal and does NOT create one for the split grant..
const grant10AGoals = await Goal.findAll({ where: { grantId: grantBeingMonitoredSplit10A.id } });
@@ -1764,7 +1764,7 @@ describe('createMonitoringGoals', () => {
const grant12Goals = await Goal.findAll({ where: { grantId: grantReopenMonitoringGoal12.id } });
expect(grant12Goals.length).toBe(1);
expect(grant12Goals[0].goalTemplateId).toBe(goalTemplate.id);
- expect(grant12Goals[0].status).toBe('Not started');
+ expect(grant12Goals[0].status).toBe('Not Started');
};
it('creates monitoring goals for grants that need them', async () => {
From f13ef3da9455eb27b4d06de5dcfeac6e5ef88c9c Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Mon, 9 Dec 2024 11:32:08 -0500
Subject: [PATCH 088/198] Revert change
---
.../src/components/GoalCards/components/GoalStatusDropdown.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/src/components/GoalCards/components/GoalStatusDropdown.js b/frontend/src/components/GoalCards/components/GoalStatusDropdown.js
index 235ca5b5d8..8e2bd69645 100644
--- a/frontend/src/components/GoalCards/components/GoalStatusDropdown.js
+++ b/frontend/src/components/GoalCards/components/GoalStatusDropdown.js
@@ -17,7 +17,7 @@ export default function GoalStatusDropdown({
}) {
const { user } = useContext(UserContext);
const key = status || 'Needs Status';
- const { icon, display } = STATUSES[key] || STATUSES['Needs Status'];
+ const { icon, display } = STATUSES[key];
const isReadOnly = useMemo(() => ((
status === 'Draft'
From 3fad9404c1ab56f3009af9caa37f0c80f04e83e9 Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Mon, 9 Dec 2024 11:56:36 -0500
Subject: [PATCH 089/198] actually fix script
---
src/tools/createMonitoringGoals.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/tools/createMonitoringGoals.js b/src/tools/createMonitoringGoals.js
index 0d53df0122..c5e8208ff2 100644
--- a/src/tools/createMonitoringGoals.js
+++ b/src/tools/createMonitoringGoals.js
@@ -142,7 +142,7 @@ const createMonitoringGoals = async () => {
GROUP BY 1
)
UPDATE "Goals"
- SET "status" = 'Not started',
+ SET "status" = 'Not Started',
"updatedAt" = NOW()
FROM grants_needing_goal_reopend
WHERE "Goals".id = grants_needing_goal_reopend."goalId";
From 3586184be5feb7c32e0d5b19cc2582a686148dd0 Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Mon, 9 Dec 2024 12:26:51 -0500
Subject: [PATCH 090/198] Group by text and citation
---
src/services/citations.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/services/citations.ts b/src/services/citations.ts
index 4b64528883..c9d182b44f 100644
--- a/src/services/citations.ts
+++ b/src/services/citations.ts
@@ -11,6 +11,7 @@ export async function textByCitation(
where: {
citation: citationIds,
},
+ group: ['text', 'citation'],
});
}
From 4b392487e4ab39d0a85d08458c4dd8c3e906447d Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Mon, 9 Dec 2024 12:32:22 -0500
Subject: [PATCH 091/198] prevent changing source for monitoring template
---
frontend/src/pages/ActivityReport/Pages/components/GoalForm.js | 1 +
1 file changed, 1 insertion(+)
diff --git a/frontend/src/pages/ActivityReport/Pages/components/GoalForm.js b/frontend/src/pages/ActivityReport/Pages/components/GoalForm.js
index 1961112c7c..1ae7119414 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/GoalForm.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/GoalForm.js
@@ -186,6 +186,7 @@ export default function GoalForm({
permissions={isCurated ? [
isSourceEditable,
!goal.onApprovedAR || !goal.source,
+ citationOptions.length,
] : [!goal.onApprovedAR || !goal.source]}
label="Goal source"
value={goalSource}
From 2d62eabb15a0ede2563764b88b1be40df356018a Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Mon, 9 Dec 2024 13:20:51 -0500
Subject: [PATCH 092/198] Update test
---
.../getGoalsByActivityRecipient.test.js | 28 +++++++++----------
1 file changed, 13 insertions(+), 15 deletions(-)
diff --git a/src/goalServices/getGoalsByActivityRecipient.test.js b/src/goalServices/getGoalsByActivityRecipient.test.js
index 360d82f1a1..d55e2db552 100644
--- a/src/goalServices/getGoalsByActivityRecipient.test.js
+++ b/src/goalServices/getGoalsByActivityRecipient.test.js
@@ -14,8 +14,6 @@ import {
ActivityReportObjective,
Objective,
Topic,
- EventReportPilot,
- SessionReportPilot,
ActivityReportObjectiveTopic,
} from '../models';
import { getGoalsByActivityRecipient } from '../services/recipient';
@@ -244,7 +242,6 @@ describe('Goals by Recipient Test', () => {
let objectiveIds = [];
let goalIds = [];
let curatedGoalTemplate;
- let event;
beforeAll(async () => {
// Create User.
@@ -332,6 +329,7 @@ describe('Goals by Recipient Test', () => {
grantId: 300,
createdAt: '2021-01-10T19:16:15.842Z',
onApprovedAR: true,
+ createdVia: 'activityReport',
}),
// goal 2 (AR1)
Goal.create({
@@ -342,6 +340,7 @@ describe('Goals by Recipient Test', () => {
grantId: 300,
createdAt: '2021-02-15T19:16:15.842Z',
onApprovedAR: true,
+ createdVia: 'activityReport',
}),
// goal 3 (AR1)
Goal.create({
@@ -352,6 +351,7 @@ describe('Goals by Recipient Test', () => {
grantId: 300,
createdAt: '2021-03-03T19:16:15.842Z',
onApprovedAR: true,
+ createdVia: 'activityReport',
}),
// goal 4 (AR2)
Goal.create({
@@ -362,6 +362,7 @@ describe('Goals by Recipient Test', () => {
grantId: 301,
createdAt: '2021-04-02T19:16:15.842Z',
onApprovedAR: true,
+ createdVia: 'activityReport',
}),
// goal 5 (AR3 Exclude)
Goal.create({
@@ -372,6 +373,7 @@ describe('Goals by Recipient Test', () => {
grantId: 302,
createdAt: '2021-05-02T19:16:15.842Z',
onApprovedAR: true,
+ createdVia: 'activityReport',
}),
// 6
Goal.create({
@@ -382,6 +384,7 @@ describe('Goals by Recipient Test', () => {
grantId: 300,
createdAt: '2021-01-10T19:16:15.842Z',
onApprovedAR: true,
+ createdVia: 'rtr',
}),
// 7
Goal.create({
@@ -393,6 +396,7 @@ describe('Goals by Recipient Test', () => {
createdAt: '2021-01-10T19:16:15.842Z',
onApprovedAR: true,
onAR: true,
+ createdVia: 'rtr',
}),
// 8
Goal.create({
@@ -403,6 +407,7 @@ describe('Goals by Recipient Test', () => {
grantId: 300,
createdAt: '2021-01-10T19:16:15.842Z',
onApprovedAR: false,
+ createdVia: 'activityReport',
}),
// 9
@@ -414,6 +419,7 @@ describe('Goals by Recipient Test', () => {
grantId: grant3.id,
createdAt: '2021-01-10T19:16:15.842Z',
onApprovedAR: true,
+ createdVia: 'activityReport',
}),
// 10
@@ -425,6 +431,7 @@ describe('Goals by Recipient Test', () => {
grantId: grant4.id,
createdAt: '2021-01-10T19:16:15.842Z',
onApprovedAR: true,
+ createdVia: 'activityReport',
}),
// 11
@@ -436,6 +443,7 @@ describe('Goals by Recipient Test', () => {
grantId: grant4.id,
createdAt: '2021-02-10T19:16:15.842Z',
onApprovedAR: true,
+ createdVia: 'activityReport',
}),
// 12
@@ -447,6 +455,7 @@ describe('Goals by Recipient Test', () => {
grantId: grant4.id,
createdAt: '2021-03-10T19:16:15.842Z',
onApprovedAR: false,
+ createdVia: 'imported',
}),
// 13, goal from template
@@ -459,6 +468,7 @@ describe('Goals by Recipient Test', () => {
createdAt: '2021-03-10T19:16:15.842Z',
onApprovedAR: false,
goalTemplateId: curatedGoalTemplate.id,
+ createdVia: 'rtr',
}),
],
);
@@ -640,15 +650,6 @@ describe('Goals by Recipient Test', () => {
}),
],
);
-
- event = await EventReportPilot.create({
- ownerId: 1,
- regionId: 1,
- collaboratorIds: [1],
- data: {},
- imported: {},
- pocIds: [],
- });
});
afterAll(async () => {
@@ -656,9 +657,6 @@ describe('Goals by Recipient Test', () => {
const reportsToDelete = await ActivityReport.findAll({ where: { userId: mockGoalUser.id } });
const reportIdsToDelete = reportsToDelete.map((report) => report.id);
- await SessionReportPilot.destroy({ where: { eventId: event.id } });
- await EventReportPilot.destroy({ where: { ownerId: 1 } });
-
// Delete AR Objectives.
await ActivityReportObjective.destroy({
where: {
From f4d8a850470078ff9ef3764eeace9b7d9f5cb5d2 Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Mon, 9 Dec 2024 13:47:57 -0500
Subject: [PATCH 093/198] Update more tests
---
src/routes/recipient/handlers.test.js | 1 +
src/services/recipient.test.js | 13 ++++++++++++-
2 files changed, 13 insertions(+), 1 deletion(-)
diff --git a/src/routes/recipient/handlers.test.js b/src/routes/recipient/handlers.test.js
index f36f2d919e..50f3d17d06 100644
--- a/src/routes/recipient/handlers.test.js
+++ b/src/routes/recipient/handlers.test.js
@@ -224,6 +224,7 @@ describe('getGoalsByActivityRecipient', () => {
userId: 1000,
},
};
+ jest.spyOn(Users.prototype, 'canSeeBehindFeatureFlag').mockReturnValueOnce(true);
recipientById.mockResolvedValue(recipientWhere);
getUserReadRegions.mockResolvedValue([1]);
getGoalsByActivityRecipient.mockResolvedValue(recipientWhere);
diff --git a/src/services/recipient.test.js b/src/services/recipient.test.js
index 0fd77766ce..9e17a95667 100644
--- a/src/services/recipient.test.js
+++ b/src/services/recipient.test.js
@@ -786,6 +786,7 @@ describe('Recipient DB service', () => {
grantId: grant.id,
onApprovedAR: true,
source: null,
+ createdVia: 'rtr',
});
const goal2 = await Goal.create({
@@ -794,6 +795,7 @@ describe('Recipient DB service', () => {
grantId: grant.id,
onApprovedAR: true,
source: null,
+ createdVia: 'rtr',
});
const goal3 = await Goal.create({
@@ -802,6 +804,7 @@ describe('Recipient DB service', () => {
grantId: grant.id,
onApprovedAR: true,
source: null,
+ createdVia: 'rtr',
});
const goal4 = await Goal.create({
@@ -810,6 +813,7 @@ describe('Recipient DB service', () => {
grantId: grant.id,
onApprovedAR: true,
source: null,
+ createdVia: 'rtr',
});
const feiGoal = await Goal.create({
@@ -819,6 +823,7 @@ describe('Recipient DB service', () => {
onApprovedAR: true,
source: null,
goalTemplateId: feiRootCausePrompt.goalTemplateId,
+ createdVia: 'rtr',
});
goals = [goal1, goal2, goal3, goal4, feiGoal];
@@ -1024,6 +1029,7 @@ describe('Recipient DB service', () => {
grantId: grant.id,
onApprovedAR: true,
source: null,
+ createdVia: 'monitoring',
});
const goal2 = await Goal.create({
@@ -1032,6 +1038,7 @@ describe('Recipient DB service', () => {
grantId: grant.id,
onApprovedAR: true,
source: null,
+ createdVia: 'monitoring',
});
goals = [goal1, goal2];
@@ -1165,7 +1172,11 @@ describe('Recipient DB service', () => {
});
it('successfully maintains two goals without losing topics', async () => {
- const goalsForRecord = await getGoalsByActivityRecipient(recipient.id, grant.regionId, {});
+ const goalsForRecord = await getGoalsByActivityRecipient(
+ recipient.id,
+ grant.regionId,
+ { excludeMonitoringGoals: false },
+ );
// Assert counts.
expect(goalsForRecord.count).toBe(2);
From c8db75fcc89acfea20f9277ad81f0028804226a1 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Mon, 9 Dec 2024 14:11:46 -0500
Subject: [PATCH 094/198] fix fe test
---
frontend/src/fetchers/__tests__/citations.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/src/fetchers/__tests__/citations.js b/frontend/src/fetchers/__tests__/citations.js
index f6d5e60b61..39794190be 100644
--- a/frontend/src/fetchers/__tests__/citations.js
+++ b/frontend/src/fetchers/__tests__/citations.js
@@ -7,7 +7,7 @@ describe('Citations fetcher', () => {
beforeEach(() => fetchMock.reset());
it('fetches citations', async () => {
- fetchMock.get('/api/citations/region/1?grantIds=1&2&reportStartDate=2024-12-03', []);
+ fetchMock.get('/api/citations/region/1?grantIds=1&grantIds=2&reportStartDate=2024-12-03', []);
await fetchCitationsByGrant(1, [1, 2], '2024-12-03');
expect(fetchMock.called()).toBeTruthy();
});
From ddc8485d6df23bb6948be3096444c54f123399a0 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Mon, 9 Dec 2024 17:39:42 -0500
Subject: [PATCH 095/198] fix FE issue that would dupe citations and save aro
citations json per grant
---
.../Pages/components/GoalPicker.js | 2 +-
src/services/reportCache.js | 35 +++++++++++-
src/services/reportCache.test.js | 56 +++++++++++++++++++
3 files changed, 89 insertions(+), 4 deletions(-)
diff --git a/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js b/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js
index 8d37f2eca0..d9186bd6a2 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js
@@ -137,7 +137,7 @@ const GoalPicker = ({
}
const findingKey = `${currentGrant.acro} - ${currentGrant.citation} - ${currentGrant.findingType}`;
- if (!acc[findingType].options.find((option) => option.label === findingKey)) {
+ if (!acc[findingType].options.find((option) => option.name === findingKey)) {
acc[findingType].options.push({
name: findingKey,
id: current.standardId,
diff --git a/src/services/reportCache.js b/src/services/reportCache.js
index cfab4881d6..49a580c326 100644
--- a/src/services/reportCache.js
+++ b/src/services/reportCache.js
@@ -1,5 +1,4 @@
import {
- getResourcesForActivityReportObjectives,
processActivityReportObjectiveForResourcesById,
} from './resource';
@@ -158,11 +157,41 @@ export const cacheCitations = async (objectiveId, activityReportObjectiveId, cit
// Create citations to save.
if (citations && citations.length > 0) {
- newCitations = citations.map((citation) => (
+ // Get the grant id from the goal.
+ const goal = await Goal.findOne({
+ attributes: ['grantId'],
+ include: [
+ {
+ model: Objective,
+ as: 'objectives',
+ where: { id: objectiveId },
+ required: true,
+ },
+ ],
+ });
+
+ const grantForThisCitation = goal.grantId;
+
+ // Get all the citations for the grant.
+ const citationsToSave = citations.reduce((acc, citation) => {
+ const { monitoringReferences } = citation;
+ monitoringReferences.forEach((ref) => {
+ const { grantId } = ref;
+ if (grantId === grantForThisCitation) {
+ acc.push(citation);
+ }
+ });
+ return acc;
+ }, []);
+
+ newCitations = citationsToSave.map((citation) => (
{
activityReportObjectiveId,
citation: citation.citation,
- monitoringReferences: citation.monitoringReferences,
+ // Only save the monitoring references for the grant we are working with.
+ monitoringReferences: citation.monitoringReferences.filter(
+ (ref) => ref.grantId === grantForThisCitation,
+ ),
}));
// If we have citations to save, create them.
diff --git a/src/services/reportCache.test.js b/src/services/reportCache.test.js
index 0ccb3c743e..aec48837b3 100644
--- a/src/services/reportCache.test.js
+++ b/src/services/reportCache.test.js
@@ -130,6 +130,7 @@ describe('activityReportObjectiveCitation', () => {
beforeAll(async () => {
recipient = await createRecipient({});
+
grant = await createGrant({ recipientId: recipient.id });
activityReport = await createReport({
@@ -250,6 +251,61 @@ describe('activityReportObjectiveCitation', () => {
expect(deletedAroCitations).toHaveLength(0);
});
+
+ it('correctly saves aro citations per grant', async () => {
+ const multiGrantCitations = [
+ {
+ citation: 'Citation 1',
+ monitoringReferences: [{
+ grantId: grant.id,
+ findingId: 1,
+ reviewName: 'Review 1',
+ }],
+ },
+ {
+ citation: 'Citation 2',
+ monitoringReferences: [{
+ grantId: 2,
+ findingId: 1,
+ reviewName: 'Review 2',
+ }],
+ },
+ {
+ citation: 'Citation 3',
+ monitoringReferences: [
+ {
+ grantId: 3,
+ findingId: 1,
+ reviewName: 'Review 3',
+ },
+ {
+ grantId: grant.id,
+ findingId: 1,
+ reviewName: 'Review 4',
+ }],
+ },
+ ];
+
+ const result = await cacheCitations(objective.id, aro.id, multiGrantCitations);
+
+ // Retrieve all citations for the aro.
+ const aroCitations = await ActivityReportObjectiveCitation.findAll({
+ where: {
+ activityReportObjectiveId: aro.id,
+ },
+ });
+
+ expect(aroCitations).toHaveLength(2);
+
+ // Assert citations are saved correctly.
+ expect(aroCitations[0].citation).toEqual('Citation 1');
+ expect(aroCitations[0].monitoringReferences.length).toEqual(1);
+ expect(aroCitations[0].monitoringReferences[0].grantId).toBe(grant.id);
+
+ expect(aroCitations[1].citation).toEqual('Citation 3');
+ expect(aroCitations[1].monitoringReferences.length).toEqual(1);
+ expect(aroCitations[1].monitoringReferences[0].grantId).toBe(grant.id);
+ });
});
describe('cacheGoalMetadata', () => {
From 62bd3c59c4aea6b47dbdd39b796a747727d35146 Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Tue, 10 Dec 2024 11:21:29 -0500
Subject: [PATCH 096/198] Update query to get ARO Citations
---
.../Monitoring/components/CitationCard.js | 2 +-
.../Monitoring/components/CitationCards.js | 5 +-
.../pages/Monitoring/components/ReviewCard.js | 5 +-
.../Monitoring/components/ReviewObjective.js | 10 +-
.../components/ReviewWithinCitation.js | 2 +-
.../pages/Monitoring/testHelpers/mockData.js | 16 +-
src/models/activityReportObjectiveCitation.js | 24 ++
src/services/monitoring.ts | 159 +++++++++++++-
.../types/activityReportObjectiveCitations.ts | 206 ++++++++++++++++++
src/services/types/monitoring.ts | 5 +-
10 files changed, 405 insertions(+), 29 deletions(-)
create mode 100644 src/services/types/activityReportObjectiveCitations.ts
diff --git a/frontend/src/pages/RecipientRecord/pages/Monitoring/components/CitationCard.js b/frontend/src/pages/RecipientRecord/pages/Monitoring/components/CitationCard.js
index 89c8562250..29fdd6f609 100644
--- a/frontend/src/pages/RecipientRecord/pages/Monitoring/components/CitationCard.js
+++ b/frontend/src/pages/RecipientRecord/pages/Monitoring/components/CitationCard.js
@@ -83,7 +83,7 @@ CitationCard.propTypes = {
})).isRequired,
objectives: PropTypes.arrayOf(PropTypes.shape({
title: PropTypes.string.isRequired,
- activityReportIds: PropTypes.arrayOf(PropTypes.string).isRequired,
+ activityReports: PropTypes.arrayOf(PropTypes.string).isRequired,
endDate: PropTypes.string.isRequired,
topics: PropTypes.arrayOf(PropTypes.string).isRequired,
status: PropTypes.string.isRequired,
diff --git a/frontend/src/pages/RecipientRecord/pages/Monitoring/components/CitationCards.js b/frontend/src/pages/RecipientRecord/pages/Monitoring/components/CitationCards.js
index b037611c3e..94e980ec69 100644
--- a/frontend/src/pages/RecipientRecord/pages/Monitoring/components/CitationCards.js
+++ b/frontend/src/pages/RecipientRecord/pages/Monitoring/components/CitationCards.js
@@ -29,7 +29,10 @@ CitationCards.propTypes = {
})).isRequired,
objectives: PropTypes.arrayOf(PropTypes.shape({
title: PropTypes.string.isRequired,
- activityReportIds: PropTypes.arrayOf(PropTypes.string).isRequired,
+ activityReports: PropTypes.arrayOf(PropTypes.shape({
+ id: PropTypes.number.isRequired,
+ displayId: PropTypes.string.isRequired,
+ })).isRequired,
endDate: PropTypes.string.isRequired,
topics: PropTypes.arrayOf(PropTypes.string).isRequired,
status: PropTypes.string.isRequired,
diff --git a/frontend/src/pages/RecipientRecord/pages/Monitoring/components/ReviewCard.js b/frontend/src/pages/RecipientRecord/pages/Monitoring/components/ReviewCard.js
index c8f09bdb59..0b93af2711 100644
--- a/frontend/src/pages/RecipientRecord/pages/Monitoring/components/ReviewCard.js
+++ b/frontend/src/pages/RecipientRecord/pages/Monitoring/components/ReviewCard.js
@@ -90,7 +90,10 @@ ReviewCard.propTypes = {
category: PropTypes.string.isRequired,
correctionDeadline: PropTypes.string.isRequired,
objectives: PropTypes.arrayOf(PropTypes.shape({
- activityReportIds: PropTypes.arrayOf(PropTypes.string).isRequired,
+ activityReports: PropTypes.arrayOf(PropTypes.shape({
+ id: PropTypes.number.isRequired,
+ displayId: PropTypes.string.isRequired,
+ })).isRequired,
endDate: PropTypes.string.isRequired,
status: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
diff --git a/frontend/src/pages/RecipientRecord/pages/Monitoring/components/ReviewObjective.js b/frontend/src/pages/RecipientRecord/pages/Monitoring/components/ReviewObjective.js
index a26c7dcbaf..85dec82a5f 100644
--- a/frontend/src/pages/RecipientRecord/pages/Monitoring/components/ReviewObjective.js
+++ b/frontend/src/pages/RecipientRecord/pages/Monitoring/components/ReviewObjective.js
@@ -14,10 +14,10 @@ export default function ReviewObjective({ objective, regionId }) {
- {objective.activityReportIds.map((reportId) => (
-
-
- {reportId}
+ {objective.activityReports.map(({ id, displayId }) => (
+
+
+ {displayId}
))}
@@ -45,7 +45,7 @@ export default function ReviewObjective({ objective, regionId }) {
ReviewObjective.propTypes = {
objective: PropTypes.shape({
title: PropTypes.string.isRequired,
- activityReportIds: PropTypes.arrayOf(PropTypes.string).isRequired,
+ activityReports: PropTypes.arrayOf(PropTypes.string).isRequired,
endDate: PropTypes.string.isRequired,
topics: PropTypes.arrayOf(PropTypes.string).isRequired,
status: PropTypes.string.isRequired,
diff --git a/frontend/src/pages/RecipientRecord/pages/Monitoring/components/ReviewWithinCitation.js b/frontend/src/pages/RecipientRecord/pages/Monitoring/components/ReviewWithinCitation.js
index 58d4e2482b..2205e0f561 100644
--- a/frontend/src/pages/RecipientRecord/pages/Monitoring/components/ReviewWithinCitation.js
+++ b/frontend/src/pages/RecipientRecord/pages/Monitoring/components/ReviewWithinCitation.js
@@ -54,7 +54,7 @@ ReviewWithinCitation.propTypes = {
})).isRequired,
objectives: PropTypes.arrayOf(PropTypes.shape({
title: PropTypes.string.isRequired,
- activityReportIds: PropTypes.arrayOf(PropTypes.string).isRequired,
+ activityReports: PropTypes.arrayOf(PropTypes.string).isRequired,
endDate: PropTypes.string.isRequired,
topics: PropTypes.arrayOf(PropTypes.string).isRequired,
status: PropTypes.string.isRequired,
diff --git a/frontend/src/pages/RecipientRecord/pages/Monitoring/testHelpers/mockData.js b/frontend/src/pages/RecipientRecord/pages/Monitoring/testHelpers/mockData.js
index e9eee63a4e..3513357772 100644
--- a/frontend/src/pages/RecipientRecord/pages/Monitoring/testHelpers/mockData.js
+++ b/frontend/src/pages/RecipientRecord/pages/Monitoring/testHelpers/mockData.js
@@ -63,7 +63,7 @@ const reviewData = [
objectives: [
{
title: 'The TTA Specialist will assist the recipient with developing a QIP and/or corrective action plan to address the finding with correction strategies, timeframes, and evidence of the correction.',
- activityReportIds: ['14AR29888'],
+ activityReports: [{ displayId: '14AR12345', id: 12345 }],
endDate: '07/12/2024',
topics: ['Communication', 'Quality Improvement Plan/QIP', 'Safety Practices', 'Transportation'],
status: 'In Progress',
@@ -102,7 +102,7 @@ const reviewData = [
objectives: [
{
title: 'The TTA Specialist will support the recipient by developing a plan to ensure all staff implement health and safety practices to always keep children safe',
- activityReportIds: ['14AR12345'],
+ activityReports: [{ displayId: '14AR12345', id: 12345 }],
endDate: '06/24/2024',
topics: ['Human Resources', 'Ongoing Monitoring Management System', 'Safety Practices', 'Training and Profressional Development'],
status: 'Complete',
@@ -118,7 +118,7 @@ const reviewData = [
objectives: [
{
title: 'The TTA Specialist will support the recipient by developing a plan to ensure all staff implement health and safety practices to always keep children safe',
- activityReportIds: ['14AR12345'],
+ activityReports: [{ displayId: '14AR12345', id: 12345 }],
endDate: '06/24/2024',
topics: ['Human Resources', 'Ongoing Monitoring Management System', 'Safety Practices', 'Training and Profressional Development'],
status: 'Complete',
@@ -134,7 +134,7 @@ const reviewData = [
objectives: [
{
title: 'The TTA Specialist will support the recipient by developing a plan to ensure all staff implement health and safety practices to always keep children safe',
- activityReportIds: ['14AR12345'],
+ activityReports: [{ displayId: '14AR12345', id: 12345 }],
endDate: '06/24/2024',
topics: ['Human Resources', 'Ongoing Monitoring Management System', 'Safety Practices', 'Training and Profressional Development'],
status: 'Complete',
@@ -185,7 +185,7 @@ const citationData = [
objectives: [
{
title: 'The TTA Specialist will support the recipient by developing a plan to ensure all staff implement health and safety practices to always keep children safe',
- activityReportIds: ['14AR12345'],
+ activityReports: [{ displayId: '14AR12345', id: 12345 }],
endDate: '06/24/2024',
topics: ['Human Resources', 'Ongoing Monitoring Management System', 'Safety Practices', 'Training and Profressional Development'],
status: 'Complete',
@@ -233,7 +233,7 @@ const citationData = [
objectives: [
{
title: 'The TTA Specialist will support the recipient by developing a plan to ensure all staff implement health and safety practices to always keep children safe',
- activityReportIds: ['14AR12345'],
+ activityReports: [{ displayId: '14AR12345', id: 12345 }],
endDate: '06/24/2024',
topics: ['Human Resources', 'Ongoing Monitoring Management System', 'Safety Practices', 'Training and Profressional Development'],
status: 'Complete',
@@ -265,7 +265,7 @@ const citationData = [
objectives: [
{
title: 'The TTA Specialist will assist the recipient with developing a QIP and/or corrective action plan to address the finding with correction strategies, timeframes, and evidence of the correction.',
- activityReportIds: ['14AR29888'],
+ activityReports: [{ displayId: '14AR12345', id: 12345 }],
endDate: '07/12/2024',
topics: ['Communication', 'Quality Improvement Plan/QIP', 'Safety Practices', 'Transportation'],
status: 'In Progress',
@@ -310,7 +310,7 @@ const citationData = [
objectives: [
{
title: 'The TTA Specialist will support the recipient by developing a plan to ensure all staff implement health and safety practices to always keep children safe',
- activityReportIds: ['14AR12345'],
+ activityReports: [{ displayId: '14AR12345', id: 12345 }],
endDate: '06/24/2024',
topics: ['Human Resources', 'Ongoing Monitoring Management System', 'Safety Practices', 'Training and Profressional Development'],
status: 'Complete',
diff --git a/src/models/activityReportObjectiveCitation.js b/src/models/activityReportObjectiveCitation.js
index c948593611..023bca5e66 100644
--- a/src/models/activityReportObjectiveCitation.js
+++ b/src/models/activityReportObjectiveCitation.js
@@ -51,6 +51,30 @@ export default (sequelize, DataTypes) => {
allowNull: false,
defaultValue: DataTypes.NOW,
},
+ findingId: {
+ type: DataTypes.VIRTUAL,
+ get() {
+ const [reference] = this.monitoringReferences;
+ if (!reference) return null;
+ return reference.findingId;
+ },
+ },
+ grantNumber: {
+ type: DataTypes.VIRTUAL,
+ get() {
+ const [reference] = this.monitoringReferences;
+ if (!reference) return null;
+ return reference.grantNumber;
+ },
+ },
+ reviewName: {
+ type: DataTypes.VIRTUAL,
+ get() {
+ const [reference] = this.monitoringReferences;
+ if (!reference) return null;
+ return reference.reviewName;
+ },
+ },
}, {
sequelize,
modelName: 'ActivityReportObjectiveCitation',
diff --git a/src/services/monitoring.ts b/src/services/monitoring.ts
index 1c49119d0e..9357c7f6b5 100644
--- a/src/services/monitoring.ts
+++ b/src/services/monitoring.ts
@@ -12,6 +12,10 @@ import {
MonitoringStandard as MonitoringStandardType,
MonitoringReview as MonitoringReviewType,
} from './types/monitoring';
+import {
+ ActivityReportObjectiveCitation as ActivityReportObjectiveCitationType,
+ ActivityReportObjectiveCitationResponse,
+} from './types/activityReportObjectiveCitations';
const {
Grant,
@@ -30,8 +34,21 @@ const {
MonitoringFindingStandard,
MonitoringStandardLink,
MonitoringStandard,
+ ActivityReportObjectiveCitation,
+ ActivityReportObjective,
+ ActivityReportObjectiveTopic,
+ Topic,
+ ActivityReport,
+ Objective,
+ Goal,
+ GoalCollaborator,
+ CollaboratorType,
+ User,
} = db;
+const MIN_DELIVERY_DATE = '2022-01-01';
+const REVIEW_STATUS_COMPLETE = 'Complete';
+
export async function grantNumbersByRecipientAndRegion(recipientId: number, regionId: number) {
const grants = await Grant.findAll({
attributes: ['number'],
@@ -44,14 +61,125 @@ export async function grantNumbersByRecipientAndRegion(recipientId: number, regi
return grants.map((grant) => grant.number);
}
-const MIN_DELIVERY_DATE = '2023-01-01';
-const REVIEW_STATUS_COMPLETE = 'Complete';
+export async function aroCitationsByGrantNumbers(grantNumbers: string[]): Promise {
+ const citations = await ActivityReportObjectiveCitation.findAll({
+ attributes: {
+ include: [
+ 'findingId',
+ 'grantNumber',
+ ],
+ },
+ where: {
+ [Op.or]: grantNumbers.map((grantNumber) => ({
+ monitoringReferences: {
+ [Op.contains]: [{ grantNumber }],
+ },
+ })),
+ },
+ include: [
+ {
+ model: ActivityReportObjective,
+ as: 'activityReportObjective',
+ attributes: [
+ 'activityReportId',
+ 'objectiveId',
+ 'title',
+ 'status',
+ ],
+ required: true,
+ include: [
+ {
+ model: ActivityReportObjectiveTopic,
+ as: 'activityReportObjectiveTopics',
+ attributes: [
+ 'activityReportObjectiveId',
+ 'topicId',
+ ],
+ include: [
+ {
+ attributes: ['name'],
+ model: Topic,
+ as: 'topic',
+ },
+ ],
+ },
+ {
+ model: ActivityReport,
+ as: 'activityReport',
+ attributes: [
+ 'displayId',
+ 'endDate',
+ 'calculatedStatus',
+ ],
+ required: true,
+ },
+ {
+ model: Objective,
+ as: 'objective',
+ attributes: ['id', 'goalId'],
+ include: [
+ {
+ model: Goal,
+ as: 'goal',
+ attributes: ['id'],
+ include: [
+ {
+ model: GoalCollaborator,
+ as: 'goalCollaborators',
+ include: [
+ {
+ model: CollaboratorType,
+ as: 'collaboratorType',
+ where: {
+ name: 'Creator',
+ },
+ attributes: ['name'],
+ },
+ {
+ model: User,
+ as: 'user',
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ }) as ActivityReportObjectiveCitationType[];
+
+ return citations.map((citation) => {
+ const { activityReportObjective } = citation;
+ const { activityReport } = activityReportObjective;
+ const { activityReportObjectiveTopics } = activityReportObjective;
+
+ return {
+ findingId: citation.findingId,
+ grantNumber: citation.grantNumber,
+ reviewName: citation.reviewName,
+ title: activityReportObjective.title,
+ activityReports: [
+ {
+ id: activityReport.id,
+ displayId: activityReport.displayId,
+ },
+ ],
+ endDate: moment(activityReport.endDate).format('MM/DD/YYYY'),
+ topics: activityReportObjectiveTopics.map((topic) => topic.topic.name),
+ status: activityReportObjective.status,
+ };
+ });
+}
export async function ttaByReviews(
recipientId: number,
regionId: number,
): Promise {
const grantNumbers = await grantNumbersByRecipientAndRegion(recipientId, regionId) as string[];
+ const citationsOnActivityReports = await aroCitationsByGrantNumbers(grantNumbers);
+
const reviews = await MonitoringReview.findAll({
where: {
reportDeliveryDate: {
@@ -135,9 +263,9 @@ export async function ttaByReviews(
],
}) as MonitoringReviewType[];
- const response = reviews.map((review) => {
+ return reviews.map((review) => {
const { monitoringReviewGrantees, monitoringFindingHistories } = review.monitoringReviewLink;
-
+ let lastTTADate = null;
const findings = [];
monitoringFindingHistories.forEach((history) => {
@@ -153,14 +281,23 @@ export async function ttaByReviews(
}
history.monitoringFindingLink.monitoringFindings.forEach((finding) => {
+ const { findingId } = finding;
const status = finding.statusLink.monitoringFindingStatuses[0].name;
+ const objectives = citationsOnActivityReports.filter((c) => c.findingId === findingId);
+
+ objectives.forEach(({ endDate }) => {
+ if (!lastTTADate || moment(endDate, 'MM/DD/YYYY').isAfter(lastTTADate)) {
+ lastTTADate = moment(endDate, 'MM/DD/YYYY');
+ }
+ });
+
findings.push({
citation,
status,
findingType: finding.findingType,
- correctionDeadline: moment(finding.correctionDeadLine).format('MM/DD/YYYY'),
+ correctionDeadline: finding.correctionDeadLine,
category: finding.source,
- objectives: [],
+ objectives,
});
});
});
@@ -168,7 +305,8 @@ export async function ttaByReviews(
return {
name: review.name,
id: review.id,
- lastTTADate: null,
+ // last tta date is stored as a moment object (or null)
+ lastTTADate: lastTTADate ? lastTTADate.format('MM/DD/YYYY') : null,
outcome: review.outcome,
reviewType: review.reviewType,
reviewReceived: moment(review.reportDeliveryDate).format('MM/DD/YYYY'),
@@ -177,8 +315,6 @@ export async function ttaByReviews(
findings,
};
});
-
- return response;
}
export async function ttaByCitations(
@@ -186,6 +322,7 @@ export async function ttaByCitations(
regionId: number,
): Promise {
const grantNumbers = await grantNumbersByRecipientAndRegion(recipientId, regionId) as string[];
+ const citationsOnActivityReports = await aroCitationsByGrantNumbers(grantNumbers);
const citations = await MonitoringStandard.findAll({
include: [
@@ -227,7 +364,6 @@ export async function ttaByCitations(
{
model: MonitoringFindingStatus,
as: 'monitoringFindingStatuses',
- required: true,
},
],
},
@@ -324,6 +460,7 @@ export async function ttaByCitations(
const { monitoringFindings: historicalFindings } = monitoringFindingLink;
const [reviewFinding] = historicalFindings;
const [findingStatus] = reviewFinding.statusLink.monitoringFindingStatuses;
+ const objectives = citationsOnActivityReports.filter((c) => c.findingId === reviewFinding.findingId);
monitoringReviews.forEach((review) => {
const { monitoringReviewGrantees } = monitoringReviewLink;
@@ -338,7 +475,7 @@ export async function ttaByCitations(
outcome: review.outcome,
findingStatus: findingStatus.name,
specialists: [],
- objectives: [],
+ objectives: objectives.filter((o) => o.reviewName === review.name),
});
});
});
diff --git a/src/services/types/activityReportObjectiveCitations.ts b/src/services/types/activityReportObjectiveCitations.ts
new file mode 100644
index 0000000000..ce2abb51ca
--- /dev/null
+++ b/src/services/types/activityReportObjectiveCitations.ts
@@ -0,0 +1,206 @@
+import { ITTAByReviewObjective } from './monitoring';
+
+export interface ActivityReportObjectiveCitationResponse extends ITTAByReviewObjective {
+ findingId: string;
+ grantNumber: string;
+ reviewName: string;
+}
+
+export interface ActivityReportObjectiveCitation {
+ findingId: string;
+ grantNumber: string;
+ reviewName: string;
+ id: number;
+ activityReportObjectiveId: number;
+ citation: string;
+ monitoringReferences: MonitoringReference[];
+ createdAt: Date;
+ updatedAt: Date;
+ activityReportObjective: ActivityReportObjective;
+}
+
+export interface ActivityReportObjective {
+ id: number;
+ activityReportId: number;
+ objectiveId: number;
+ arOrder: number;
+ closeSuspendReason: null;
+ closeSuspendContext: null;
+ title: string;
+ status: string;
+ ttaProvided: string;
+ supportType: string;
+ objectiveCreatedHere: boolean;
+ originalObjectiveId: null;
+ createdAt: Date;
+ updatedAt: Date;
+ activityReportObjectiveTopics: ActivityReportObjectiveTopic[];
+ activityReport: ActivityReport;
+ objective: Objective;
+}
+
+export interface ActivityReport {
+ displayId: string;
+ endDate: string;
+ startDate: string;
+ submittedDate: null;
+ lastSaved: string;
+ creatorNameWithRole: string;
+ sortedTopics: any[];
+ creatorName: string;
+ id: number;
+ legacyId: null;
+ userId: number;
+ lastUpdatedById: number;
+ ECLKCResourcesUsed: any[];
+ nonECLKCResourcesUsed: any[];
+ additionalNotes: null;
+ numberOfParticipants: number;
+ deliveryMethod: string;
+ version: number;
+ duration: string;
+ activityRecipientType: string;
+ requester: string;
+ targetPopulations: string[];
+ language: string[];
+ virtualDeliveryType: null;
+ reason: string[];
+ participants: string[];
+ topics: any[];
+ programTypes: null;
+ context: string;
+ pageState: { [key: string]: string };
+ regionId: number;
+ submissionStatus: string;
+ calculatedStatus: string;
+ ttaType: string[];
+ updatedAt: Date;
+ approvedAt: null;
+ imported: null;
+ creatorRole: string;
+ createdAt: Date;
+}
+
+export interface ActivityReportObjectiveTopic {
+ id: number;
+ activityReportObjectiveId: number;
+ topicId: number;
+ createdAt: Date;
+ updatedAt: Date;
+ topic: Topic;
+}
+
+export interface Topic {
+ name: string;
+}
+
+export interface Objective {
+ id: number;
+ otherEntityId: null;
+ goalId: number;
+ title: string;
+ status: string;
+ objectiveTemplateId: null;
+ onAR: boolean;
+ onApprovedAR: boolean;
+ createdVia: string;
+ firstNotStartedAt: Date;
+ lastNotStartedAt: Date;
+ firstInProgressAt: null;
+ lastInProgressAt: null;
+ firstSuspendedAt: null;
+ lastSuspendedAt: null;
+ firstCompleteAt: null;
+ lastCompleteAt: null;
+ rtrOrder: number;
+ closeSuspendReason: null;
+ closeSuspendContext: null;
+ mapsToParentObjectiveId: null;
+ createdAt: Date;
+ updatedAt: Date;
+ deletedAt: null;
+ goal: Goal;
+}
+
+export interface Goal {
+ endDate: string;
+ goalNumber: string;
+ isCurated: boolean;
+ isSourceEditable: boolean;
+ id: number;
+ name: string;
+ status: string;
+ timeframe: null;
+ isFromSmartsheetTtaPlan: boolean;
+ grantId: number;
+ goalTemplateId: number;
+ mapsToParentGoalId: null;
+ onAR: boolean;
+ onApprovedAR: boolean;
+ isRttapa: string;
+ createdVia: string;
+ rtrOrder: number;
+ source: string;
+ createdAt: Date;
+ updatedAt: Date;
+ deletedAt: null;
+ goalCollaborators: GoalCollaborator[];
+}
+
+export interface CollaboratorType {
+ id: number;
+ name: string;
+ createdAt: Date;
+ updatedAt: Date;
+ deletedAt: null;
+}
+
+export interface GoalCollaborator {
+ id: number;
+ goalId: number;
+ userId: number;
+ collaboratorTypeId: number;
+ linkBack: LinkBack;
+ createdAt: Date;
+ updatedAt: Date;
+ deletedAt: null;
+ user: User;
+ collaboratorTypes: CollaboratorType[];
+}
+
+export interface LinkBack {
+ activityReportIds: number[];
+}
+
+export interface User {
+ fullName: string;
+ nameWithNationalCenters: string;
+ id: number;
+ homeRegionId: number;
+ hsesUserId: string;
+ hsesUsername: string;
+ hsesAuthorities: string[];
+ name: string;
+ phoneNumber: null;
+ email: string;
+ flags: string[];
+ lastLogin: Date;
+ createdAt: Date;
+ updatedAt: Date;
+}
+
+export interface MonitoringReference {
+ acro: string;
+ name: string;
+ grantId: number;
+ citation: string;
+ severity: number;
+ findingId: string;
+ reviewName: string;
+ standardId: number;
+ findingType: string;
+ grantNumber: string;
+ findingSource: string;
+ reportDeliveryDate: Date;
+ monitoringFindingStatusName: string;
+}
diff --git a/src/services/types/monitoring.ts b/src/services/types/monitoring.ts
index 81f96b70f7..2a50198178 100644
--- a/src/services/types/monitoring.ts
+++ b/src/services/types/monitoring.ts
@@ -29,7 +29,10 @@ interface IMonitoringResponse {
interface ITTAByReviewObjective {
title: string;
- activityReportIds: string[];
+ activityReports: {
+ id: number;
+ displayId: string;
+ }[];
endDate: string;
topics: string[];
status: string;
From a618a6d51d49597cb53ce0f42c6772e4af0ff760 Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Tue, 10 Dec 2024 11:24:36 -0500
Subject: [PATCH 097/198] Remove "any" type
---
src/services/types/activityReportObjectiveCitations.ts | 4 ----
1 file changed, 4 deletions(-)
diff --git a/src/services/types/activityReportObjectiveCitations.ts b/src/services/types/activityReportObjectiveCitations.ts
index ce2abb51ca..457623d465 100644
--- a/src/services/types/activityReportObjectiveCitations.ts
+++ b/src/services/types/activityReportObjectiveCitations.ts
@@ -46,14 +46,11 @@ export interface ActivityReport {
submittedDate: null;
lastSaved: string;
creatorNameWithRole: string;
- sortedTopics: any[];
creatorName: string;
id: number;
legacyId: null;
userId: number;
lastUpdatedById: number;
- ECLKCResourcesUsed: any[];
- nonECLKCResourcesUsed: any[];
additionalNotes: null;
numberOfParticipants: number;
deliveryMethod: string;
@@ -66,7 +63,6 @@ export interface ActivityReport {
virtualDeliveryType: null;
reason: string[];
participants: string[];
- topics: any[];
programTypes: null;
context: string;
pageState: { [key: string]: string };
From 112f4e82497073bd26bc263e5622452c36d6b76a Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Tue, 10 Dec 2024 13:05:57 -0500
Subject: [PATCH 098/198] WIP
---
src/models/activityReportObjectiveCitation.js | 12 +--
src/services/monitoring.ts | 74 ++++++-------------
.../types/activityReportObjectiveCitations.ts | 8 +-
src/services/types/monitoring.ts | 2 +-
4 files changed, 32 insertions(+), 64 deletions(-)
diff --git a/src/models/activityReportObjectiveCitation.js b/src/models/activityReportObjectiveCitation.js
index 023bca5e66..ada2a32a11 100644
--- a/src/models/activityReportObjectiveCitation.js
+++ b/src/models/activityReportObjectiveCitation.js
@@ -51,12 +51,10 @@ export default (sequelize, DataTypes) => {
allowNull: false,
defaultValue: DataTypes.NOW,
},
- findingId: {
+ findingIds: {
type: DataTypes.VIRTUAL,
get() {
- const [reference] = this.monitoringReferences;
- if (!reference) return null;
- return reference.findingId;
+ return this.monitoringReferences.map((reference) => reference.findingId);
},
},
grantNumber: {
@@ -67,12 +65,10 @@ export default (sequelize, DataTypes) => {
return reference.grantNumber;
},
},
- reviewName: {
+ reviewNames: {
type: DataTypes.VIRTUAL,
get() {
- const [reference] = this.monitoringReferences;
- if (!reference) return null;
- return reference.reviewName;
+ return this.monitoringReferences.map((reference) => reference.reviewName);
},
},
}, {
diff --git a/src/services/monitoring.ts b/src/services/monitoring.ts
index 9357c7f6b5..55e7b1ed43 100644
--- a/src/services/monitoring.ts
+++ b/src/services/monitoring.ts
@@ -28,9 +28,11 @@ const {
MonitoringClassSummary,
MonitoringFindingLink,
MonitoringFindingHistory,
+ MonitoringFindingHistoryStatusLink,
MonitoringFinding,
MonitoringFindingStatusLink,
MonitoringFindingStatus,
+ MonitoringFindingHistoryStatus,
MonitoringFindingStandard,
MonitoringStandardLink,
MonitoringStandard,
@@ -63,12 +65,6 @@ export async function grantNumbersByRecipientAndRegion(recipientId: number, regi
export async function aroCitationsByGrantNumbers(grantNumbers: string[]): Promise {
const citations = await ActivityReportObjectiveCitation.findAll({
- attributes: {
- include: [
- 'findingId',
- 'grantNumber',
- ],
- },
where: {
[Op.or]: grantNumbers.map((grantNumber) => ({
monitoringReferences: {
@@ -156,9 +152,9 @@ export async function aroCitationsByGrantNumbers(grantNumbers: string[]): Promis
const { activityReportObjectiveTopics } = activityReportObjective;
return {
- findingId: citation.findingId,
+ findingIds: citation.findingIds,
grantNumber: citation.grantNumber,
- reviewName: citation.reviewName,
+ reviewNames: citation.reviewNames,
title: activityReportObjective.title,
activityReports: [
{
@@ -283,7 +279,7 @@ export async function ttaByReviews(
history.monitoringFindingLink.monitoringFindings.forEach((finding) => {
const { findingId } = finding;
const status = finding.statusLink.monitoringFindingStatuses[0].name;
- const objectives = citationsOnActivityReports.filter((c) => c.findingId === findingId);
+ const objectives = citationsOnActivityReports.filter((c) => c.findingIds.includes(findingId));
objectives.forEach(({ endDate }) => {
if (!lastTTADate || moment(endDate, 'MM/DD/YYYY').isAfter(lastTTADate)) {
@@ -347,27 +343,12 @@ export async function ttaByCitations(
required: true,
include: [
{
- model: MonitoringFindingLink,
- as: 'monitoringFindingLink',
- required: true,
+ model: MonitoringFindingHistoryStatusLink,
+ as: 'monitoringFindingStatusLink',
include: [
{
- model: MonitoringFinding,
- as: 'monitoringFindings',
- required: true,
- include: [
- {
- model: MonitoringFindingStatusLink,
- as: 'statusLink',
- required: true,
- include: [
- {
- model: MonitoringFindingStatus,
- as: 'monitoringFindingStatuses',
- },
- ],
- },
- ],
+ model: MonitoringFindingHistoryStatus,
+ as: 'monitoringFindingHistoryStatuses',
},
],
},
@@ -389,6 +370,7 @@ export async function ttaByCitations(
{
model: MonitoringReviewStatusLink,
as: 'statusLink',
+ required: true,
include: [
{
model: MonitoringReviewStatus,
@@ -414,25 +396,6 @@ export async function ttaByCitations(
},
],
},
- {
- model: MonitoringFinding,
- as: 'monitoringFindings',
- required: true,
- include: [
- {
- model: MonitoringFindingStatusLink,
- as: 'statusLink',
- required: true,
- include: [
- {
- model: MonitoringFindingStatus,
- as: 'monitoringFindingStatuses',
- required: true,
- },
- ],
- },
- ],
- },
],
},
],
@@ -442,11 +405,15 @@ export async function ttaByCitations(
],
}) as MonitoringStandardType[];
+ return citations;
+
return citations.map((citation) => {
const [findingStandard] = citation.standardLink.monitoringFindingStandards;
const { findingLink } = findingStandard;
const { monitoringFindings } = findingLink;
+ let lastTTADate = null;
+
const grants = [];
const reviews = [];
@@ -454,13 +421,18 @@ export async function ttaByCitations(
const [status] = finding.statusLink.monitoringFindingStatuses;
findingLink.monitoringFindingHistories.forEach((history) => {
- const { monitoringReviewLink, monitoringFindingLink } = history;
+ const { monitoringReviewLink } = history;
const { monitoringReviews } = monitoringReviewLink;
const { monitoringFindings: historicalFindings } = monitoringFindingLink;
const [reviewFinding] = historicalFindings;
const [findingStatus] = reviewFinding.statusLink.monitoringFindingStatuses;
- const objectives = citationsOnActivityReports.filter((c) => c.findingId === reviewFinding.findingId);
+ const objectives = citationsOnActivityReports.filter((c) => c.findingIds.includes(finding.findingId));
+ objectives.forEach(({ endDate }) => {
+ if (!lastTTADate || moment(endDate, 'MM/DD/YYYY').isAfter(lastTTADate)) {
+ lastTTADate = moment(endDate, 'MM/DD/YYYY');
+ }
+ });
monitoringReviews.forEach((review) => {
const { monitoringReviewGrantees } = monitoringReviewLink;
@@ -475,7 +447,7 @@ export async function ttaByCitations(
outcome: review.outcome,
findingStatus: findingStatus.name,
specialists: [],
- objectives: objectives.filter((o) => o.reviewName === review.name),
+ objectives: objectives.filter((o) => o.reviewNames.includes(review.name)),
});
});
});
@@ -486,7 +458,7 @@ export async function ttaByCitations(
findingType: finding.findingType,
category: finding.source,
grantNumbers: uniq(grants.flat()),
- lastTTADate: null,
+ lastTTADate: lastTTADate ? lastTTADate.format('MM/DD/YYYY') : null,
reviews,
};
});
diff --git a/src/services/types/activityReportObjectiveCitations.ts b/src/services/types/activityReportObjectiveCitations.ts
index 457623d465..7e7acb5332 100644
--- a/src/services/types/activityReportObjectiveCitations.ts
+++ b/src/services/types/activityReportObjectiveCitations.ts
@@ -1,15 +1,15 @@
import { ITTAByReviewObjective } from './monitoring';
export interface ActivityReportObjectiveCitationResponse extends ITTAByReviewObjective {
- findingId: string;
+ findingIds: string[];
+ reviewNames: string[];
grantNumber: string;
- reviewName: string;
}
export interface ActivityReportObjectiveCitation {
- findingId: string;
+ findingIds: string[];
grantNumber: string;
- reviewName: string;
+ reviewNames: string[];
id: number;
activityReportObjectiveId: number;
citation: string;
diff --git a/src/services/types/monitoring.ts b/src/services/types/monitoring.ts
index 2a50198178..b86d34ea37 100644
--- a/src/services/types/monitoring.ts
+++ b/src/services/types/monitoring.ts
@@ -154,7 +154,7 @@ export interface MonitoringFindingHistory {
updatedAt: Date;
deletedAt: null;
monitoringReviewLink: MonitoringReviewLink;
- monitoringFindingLink: MonitoringFindingLink;
+ // monitoringFindingLink: MonitoringFindingLink;
}
export interface MonitoringReviewLink {
From 17da1471dc45c622b1600d7fe31f49de549ebd6b Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Tue, 10 Dec 2024 14:45:38 -0500
Subject: [PATCH 099/198] monitoring goal card
---
.../src/components/GoalCards/EnteredBy.js | 35 ++++++++++
frontend/src/components/GoalCards/GoalCard.js | 68 ++++++++++++-------
.../src/components/GoalCards/GoalCard.scss | 1 -
.../components/GoalCards/ObjectiveCard.css | 2 +-
.../src/components/GoalCards/ObjectiveCard.js | 13 ++++
.../GoalCards/__tests__/GoalCard.js | 22 ++++++
.../GoalCards/__tests__/ObjectiveCard.js | 59 +++++++++++++++-
.../src/pages/ActivityReport/constants.js | 1 +
src/services/recipient.js | 24 ++++++-
src/tools/createMonitoringGoals.js | 2 +-
10 files changed, 197 insertions(+), 30 deletions(-)
create mode 100644 frontend/src/components/GoalCards/EnteredBy.js
diff --git a/frontend/src/components/GoalCards/EnteredBy.js b/frontend/src/components/GoalCards/EnteredBy.js
new file mode 100644
index 0000000000..420191688a
--- /dev/null
+++ b/frontend/src/components/GoalCards/EnteredBy.js
@@ -0,0 +1,35 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Tooltip from '../Tooltip';
+
+const EnteredBy = ({
+ goalNumber, creatorRole, creatorName, moreThanOne,
+}) => (
+ <>
+
+ {moreThanOne && (
+ <>
+ {goalNumber}
+ {' '}
+ >
+ )}
+
+
+ >
+);
+
+EnteredBy.propTypes = {
+ goalNumber: PropTypes.string.isRequired,
+ creatorRole: PropTypes.string.isRequired,
+ creatorName: PropTypes.string.isRequired,
+ moreThanOne: PropTypes.bool.isRequired,
+};
+
+export default EnteredBy;
diff --git a/frontend/src/components/GoalCards/GoalCard.js b/frontend/src/components/GoalCards/GoalCard.js
index d8a07620e2..2347af22f3 100644
--- a/frontend/src/components/GoalCards/GoalCard.js
+++ b/frontend/src/components/GoalCards/GoalCard.js
@@ -20,7 +20,6 @@ import ExpanderButton from '../ExpanderButton';
import './GoalCard.scss';
import { goalPropTypes } from './constants';
import colors from '../../colors';
-import Tooltip from '../Tooltip';
import isAdmin, { hasApproveActivityReportInRegion, canEditOrCreateGoals } from '../../permissions';
import UserContext from '../../UserContext';
import { deleteGoal } from '../../fetchers/goals';
@@ -28,6 +27,7 @@ import AppLoadingContext from '../../AppLoadingContext';
import GoalStatusChangeAlert from './components/GoalStatusChangeAlert';
import useObjectiveStatusMonitor from '../../hooks/useObjectiveStatusMonitor';
import DataCard from '../DataCard';
+import EnteredBy from './EnteredBy';
export const ObjectiveSwitch = ({
objective,
@@ -35,6 +35,7 @@ export const ObjectiveSwitch = ({
regionId,
goalStatus,
dispatchStatusChange,
+ isMonitoringGoal,
}) => (
);
@@ -54,6 +56,7 @@ ObjectiveSwitch.propTypes = {
regionId: PropTypes.number.isRequired,
goalStatus: PropTypes.string.isRequired,
dispatchStatusChange: PropTypes.func.isRequired,
+ isMonitoringGoal: PropTypes.bool.isRequired,
};
export default function GoalCard({
@@ -86,6 +89,14 @@ export default function GoalCard({
isReopenedGoal,
} = goal;
+ // Check for monitoring goal.
+ const reasonsToMonitor = [...reasons];
+ let isMonitoringGoal = false;
+ if (goal.createdVia === 'monitoring') {
+ reasonsToMonitor.push('Monitoring Goal');
+ isMonitoringGoal = true;
+ }
+
const { user } = useContext(UserContext);
const { setIsAppLoading } = useContext(AppLoadingContext);
const [invalidStatusChangeAttempted, setInvalidStatusChangeAttempted] = useState();
@@ -206,6 +217,33 @@ export default function GoalCard({
return responses.map((r) => r).join(', ');
};
+ // console.log('goalText', goalText);
+
+ const renderEnteredBy = () => {
+ if (isMonitoringGoal) {
+ const goalNumber = `G-${id}`;
+ return (
+
+ );
+ }
+ return collaborators.map((c) => {
+ if (!c.goalCreatorName) return null;
+ return (
+ 1}
+ />
+ );
+ });
+ };
+
return (
@@ -298,28 +336,9 @@ export default function GoalCard({
Entered by
- {collaborators.map((c) => {
- if (!c.goalCreatorName) return null;
-
- return (
-
- {collaborators.length > 1 && (
- <>
- {c.goalNumber}
- {' '}
- >
- )}
-
-
- );
- })}
+ {
+ renderEnteredBy()
+ }
@@ -340,6 +359,7 @@ export default function GoalCard({
goalStatus={goalStatus}
regionId={parseInt(regionId, DECIMAL_BASE)}
dispatchStatusChange={dispatchStatusChange}
+ isMonitoringGoal={isMonitoringGoal}
/>
))}
diff --git a/frontend/src/components/GoalCards/GoalCard.scss b/frontend/src/components/GoalCards/GoalCard.scss
index 5739938427..3b8359948d 100644
--- a/frontend/src/components/GoalCards/GoalCard.scss
+++ b/frontend/src/components/GoalCards/GoalCard.scss
@@ -58,7 +58,6 @@ $max-width-date-column: 50%;
@media(min-width: 1475px) {
.ttahub-goal-card__goal-column__goal-source p {
- white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
max-width: 220px;
diff --git a/frontend/src/components/GoalCards/ObjectiveCard.css b/frontend/src/components/GoalCards/ObjectiveCard.css
index 6c49707d91..d0450939ab 100644
--- a/frontend/src/components/GoalCards/ObjectiveCard.css
+++ b/frontend/src/components/GoalCards/ObjectiveCard.css
@@ -5,5 +5,5 @@
it was wrapping at minw-15)
**/
.ttahub-goal-card__objective-list li span:first-child {
- min-width: 130px;
+ min-width: 140px;
}
\ No newline at end of file
diff --git a/frontend/src/components/GoalCards/ObjectiveCard.js b/frontend/src/components/GoalCards/ObjectiveCard.js
index 6d715455f9..c9fe207774 100644
--- a/frontend/src/components/GoalCards/ObjectiveCard.js
+++ b/frontend/src/components/GoalCards/ObjectiveCard.js
@@ -20,6 +20,7 @@ function ObjectiveCard({
regionId,
dispatchStatusChange,
forceReadOnly,
+ isMonitoringGoal,
}) {
const {
title,
@@ -31,6 +32,7 @@ function ObjectiveCard({
activityReports,
supportType,
ids,
+ citations,
} = objective;
const modalRef = useRef(null);
const [localStatus, setLocalStatus] = useState(status || 'Not Started');
@@ -106,6 +108,14 @@ function ObjectiveCard({
Objective
{title}
+ {
+ isMonitoringGoal && (
+
+ Citations addressed
+ {citations.join(', ')}
+
+ )
+ }
Activity reports
@@ -205,6 +215,7 @@ export const objectivePropTypes = PropTypes.shape({
id: PropTypes.number,
name: PropTypes.string,
})),
+ citations: PropTypes.arrayOf(PropTypes.string),
supportType: PropTypes.string,
ids: PropTypes.arrayOf(PropTypes.number).isRequired,
});
@@ -221,6 +232,7 @@ objectivePropTypes.defaultProps = {
ObjectiveCard.propTypes = {
objective: objectivePropTypes.isRequired,
objectivesExpanded: PropTypes.bool.isRequired,
+ isMonitoringGoal: PropTypes.bool,
goalStatus: PropTypes.string,
regionId: PropTypes.number.isRequired,
dispatchStatusChange: PropTypes.func,
@@ -231,6 +243,7 @@ ObjectiveCard.defaultProps = {
dispatchStatusChange: () => {},
goalStatus: null,
forceReadOnly: false,
+ isMonitoringGoal: false,
};
export default ObjectiveCard;
diff --git a/frontend/src/components/GoalCards/__tests__/GoalCard.js b/frontend/src/components/GoalCards/__tests__/GoalCard.js
index 2c80149871..9867d2b183 100644
--- a/frontend/src/components/GoalCards/__tests__/GoalCard.js
+++ b/frontend/src/components/GoalCards/__tests__/GoalCard.js
@@ -42,6 +42,7 @@ describe('GoalCard', () => {
activityReports: [],
grantNumbers: ['G-1'],
topics: [],
+ citations: [],
},
{
id: 2,
@@ -55,6 +56,7 @@ describe('GoalCard', () => {
activityReports: [],
grantNumbers: ['G-1'],
topics: [],
+ citations: [],
},
],
previousStatus: null,
@@ -127,6 +129,20 @@ describe('GoalCard', () => {
expect(objectives.length).toBe(2);
});
+ it('shows the monitoring flag when the goal creatdVia is monitoring', () => {
+ renderGoalCard({ }, { ...goal, createdVia: 'monitoring' });
+ const monitoringToolTip = screen.getByRole('button', {
+ name: /reason for flag on goal g-1 is monitoring\. click button to visually reveal this information\./i,
+ });
+ expect(monitoringToolTip).toBeInTheDocument();
+ });
+
+ it('shows entered by as OHS when the goal createdVia is monitoring', () => {
+ renderGoalCard({ }, { ...goal, createdVia: 'monitoring' });
+ expect(screen.getByText(/entered by/i)).toBeInTheDocument();
+ expect(screen.getByText(/OHS/i)).toBeInTheDocument();
+ });
+
it('shows goal source', () => {
renderGoalCard();
@@ -396,6 +412,7 @@ describe('GoalCard', () => {
activityReports: [],
grantNumbers: ['G-1'],
topics: [],
+ citations: [],
},
{
id: 2,
@@ -408,6 +425,7 @@ describe('GoalCard', () => {
activityReports: [],
grantNumbers: ['G-2'],
topics: [],
+ citations: [],
},
],
};
@@ -445,6 +463,7 @@ describe('GoalCard', () => {
activityReports: [],
grantNumbers: ['G-1'],
topics: [],
+ citations: [],
ids: [1],
},
{
@@ -459,6 +478,7 @@ describe('GoalCard', () => {
activityReports: [],
grantNumbers: ['G-2'],
topics: [],
+ citations: [],
},
],
};
@@ -495,6 +515,7 @@ describe('GoalCard', () => {
activityReports: [],
grantNumbers: ['G-1'],
topics: [],
+ citations: [],
ids: [1],
},
{
@@ -509,6 +530,7 @@ describe('GoalCard', () => {
activityReports: [],
grantNumbers: ['G-2'],
topics: [],
+ citations: [],
},
],
};
diff --git a/frontend/src/components/GoalCards/__tests__/ObjectiveCard.js b/frontend/src/components/GoalCards/__tests__/ObjectiveCard.js
index d83ad64af8..29c259fbe9 100644
--- a/frontend/src/components/GoalCards/__tests__/ObjectiveCard.js
+++ b/frontend/src/components/GoalCards/__tests__/ObjectiveCard.js
@@ -13,7 +13,11 @@ import UserContext from '../../../UserContext';
describe('ObjectiveCard', () => {
const history = createMemoryHistory();
- const renderObjectiveCard = (objective, dispatchStatusChange = jest.fn()) => {
+ const renderObjectiveCard = (
+ objective,
+ dispatchStatusChange = jest.fn(),
+ isMonitoringGoal = false,
+ ) => {
render(
{
goalStatus="In Progress"
objectivesExpanded
dispatchStatusChange={dispatchStatusChange}
+ isMonitoringGoal={isMonitoringGoal}
/>
,
@@ -49,6 +54,7 @@ describe('ObjectiveCard', () => {
status: 'In Progress',
grantNumbers: ['grant1', 'grant2'],
topics: [],
+ citations: [],
supportTypes: ['Planning'],
activityReports: [
{
@@ -79,6 +85,7 @@ describe('ObjectiveCard', () => {
status: 'In Progress',
grantNumbers: ['grant1', 'grant2'],
topics: [],
+ citations: [],
supportTypes: ['Planning'],
activityReports: [],
};
@@ -106,6 +113,55 @@ describe('ObjectiveCard', () => {
});
});
+ it('shows citations addressed field when the prop isMonitoringGoal is true', async () => {
+ const objective = {
+ id: 123,
+ ids: [123],
+ title: 'This is an objective',
+ endDate: '2020-01-01',
+ reasons: ['reason1', 'reason2'],
+ status: 'In Progress',
+ grantNumbers: ['grant1', 'grant2'],
+ topics: [],
+ supportTypes: ['Planning'],
+ activityReports: [],
+ citations: ['citation1', 'citation2'],
+ };
+
+ renderObjectiveCard(objective, jest.fn(), true);
+
+ expect(screen.getByText('This is an objective')).toBeInTheDocument();
+ expect(screen.getByText('2020-01-01')).toBeInTheDocument();
+ expect(screen.getByText('reason1')).toBeInTheDocument();
+ expect(screen.getByText('reason2')).toBeInTheDocument();
+ expect(screen.getByText('Citations addressed')).toBeInTheDocument();
+ expect(screen.getByText('citation1, citation2')).toBeInTheDocument();
+ });
+
+ it('hides citations addressed field when the prop isMonitoringGoal is false', async () => {
+ const objective = {
+ id: 123,
+ ids: [123],
+ title: 'This is an objective',
+ endDate: '2020-01-01',
+ reasons: ['reason1', 'reason2'],
+ status: 'In Progress',
+ grantNumbers: ['grant1', 'grant2'],
+ topics: [],
+ supportTypes: ['Planning'],
+ activityReports: [],
+ citations: [],
+ };
+
+ renderObjectiveCard(objective, jest.fn(), false);
+ expect(screen.getByText('This is an objective')).toBeInTheDocument();
+ expect(screen.getByText('2020-01-01')).toBeInTheDocument();
+ expect(screen.getByText('reason1')).toBeInTheDocument();
+ expect(screen.getByText('reason2')).toBeInTheDocument();
+ expect(screen.queryAllByText('Citations addressed').length).toBe(0);
+ expect(screen.queryAllByText('citation1, citation2').length).toBe(0);
+ });
+
it('suspends an objective', async () => {
const objective = {
id: 123,
@@ -116,6 +172,7 @@ describe('ObjectiveCard', () => {
status: 'In Progress',
grantNumbers: ['grant1', 'grant2'],
topics: [],
+ citations: [],
supportTypes: ['Planning'],
activityReports: [],
};
diff --git a/frontend/src/pages/ActivityReport/constants.js b/frontend/src/pages/ActivityReport/constants.js
index f2fa1fec34..20fe3dc6bd 100644
--- a/frontend/src/pages/ActivityReport/constants.js
+++ b/frontend/src/pages/ActivityReport/constants.js
@@ -2,6 +2,7 @@ export const reasonsToMonitor = [
'Monitoring | Deficiency',
'Monitoring | Noncompliance',
'Monitoring | Area of Concern',
+ 'Monitoring Goal',
];
// Note that if this topic list is changed, it needs also to be changed in
diff --git a/src/services/recipient.js b/src/services/recipient.js
index 937dc9cf7b..a466918c49 100644
--- a/src/services/recipient.js
+++ b/src/services/recipient.js
@@ -25,6 +25,7 @@ import {
ActivityReportApprover,
ActivityReportObjective,
GoalTemplateFieldPrompt,
+ ActivityReportObjectiveCitation,
} from '../models';
import orderRecipientsBy from '../lib/orderRecipientsBy';
import {
@@ -421,7 +422,9 @@ export function reduceObjectivesForRecipientRecord(
reportReasons: [...accumulated.reportReasons, ...currentReport.reason],
// eslint-disable-next-line max-len
endDate: new Date(currentReport.endDate) < new Date(accumulated.endDate) ? accumulated.endDate : currentReport.endDate,
- }), { reportTopics: [], reportReasons: [], endDate: '' });
+ }), {
+ reportTopics: [], reportReasons: [], endDate: '',
+ });
const objectiveTitle = objective.title.trim();
const objectiveStatus = objective.status;
@@ -431,6 +434,12 @@ export function reduceObjectivesForRecipientRecord(
objective.activityReportObjectives?.flatMap((aro) => aro.topics) || []
);
+ // get our citations.
+ const objectiveCitations = objective.activityReportObjectives?.flatMap(
+ (aro) => aro.activityReportObjectiveCitations,
+ ) || [];
+ const reportObjectiveCitations = objectiveCitations.map((c) => `${c.dataValues.monitoringReferences[0].findingType} - ${c.dataValues.citation} - ${c.dataValues.monitoringReferences[0].findingSource}`);
+
const existing = acc.objectives.find((o) => (
o.title === objectiveTitle
&& o.status === objectiveStatus
@@ -442,6 +451,7 @@ export function reduceObjectivesForRecipientRecord(
existing.reasons.sort();
existing.topics = [...existing.topics, ...reportTopics, ...objectiveTopics];
existing.topics.sort();
+ existing.citations = uniq([...existing.citations, ...reportObjectiveCitations]);
existing.grantNumbers = grantNumbers;
existing.ids = combineObjectiveIds(existing, objective);
@@ -463,6 +473,7 @@ export function reduceObjectivesForRecipientRecord(
reasons: uniq(reportReasons),
activityReports: objective.activityReports || [],
topics: [...reportTopics, ...objectiveTopics],
+ citations: uniq(reportObjectiveCitations),
ids: combineObjectiveIds({ ids: [] }, objective),
};
@@ -473,11 +484,13 @@ export function reduceObjectivesForRecipientRecord(
objectives: [...acc.objectives, formattedObjective],
reasons: [...acc.reasons, ...reportReasons],
topics: reduceTopicsOfDifferingType([...acc.topics, ...reportTopics, ...objectiveTopics]),
+ citations: uniq([...acc.citations, ...reportObjectiveCitations]),
};
}, {
objectives: [],
topics: [],
reasons: [],
+ citations: [],
});
const current = goal;
@@ -718,13 +731,20 @@ export async function getGoalsByActivityRecipient(
model: ActivityReportObjective,
as: 'activityReportObjectives',
attributes: ['id', 'objectiveId'],
- separate: true,
include: [
{
model: Topic,
as: 'topics',
attributes: ['name'],
},
+ {
+ model: ActivityReportObjectiveCitation,
+ as: 'activityReportObjectiveCitations',
+ attributes: [
+ 'citation',
+ 'monitoringReferences',
+ ],
+ },
],
},
{
diff --git a/src/tools/createMonitoringGoals.js b/src/tools/createMonitoringGoals.js
index 18bf07586b..0d53df0122 100644
--- a/src/tools/createMonitoringGoals.js
+++ b/src/tools/createMonitoringGoals.js
@@ -71,7 +71,7 @@ const createMonitoringGoals = async () => {
new_goals AS (
SELECT
gt."templateName" "name",
- 'Not started' "status",
+ 'Not Started' "status",
NULL "timeframe",
FALSE "isFromSmartsheetTtaPlan",
NOW() "createdAt",
From fac0986b576fbf48e2559504ff40c3c641ef004a Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Tue, 10 Dec 2024 15:24:06 -0500
Subject: [PATCH 100/198] fix tests
---
.../Pages/components/GoalPicker.js | 2 +-
src/goalServices/reduceGoals.ts | 4 ++--
src/services/reportCache.test.js | 18 +++++++++++++-----
src/tools/createMonitoringGoals.test.js | 4 ++--
4 files changed, 18 insertions(+), 10 deletions(-)
diff --git a/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js b/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js
index d9186bd6a2..8cfb4466ff 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js
@@ -136,7 +136,7 @@ const GoalPicker = ({
acc[findingType] = { label: findingType, options: [] };
}
- const findingKey = `${currentGrant.acro} - ${currentGrant.citation} - ${currentGrant.findingType}`;
+ const findingKey = `${currentGrant.acro} - ${currentGrant.citation} - ${currentGrant.findingSource}`;
if (!acc[findingType].options.find((option) => option.name === findingKey)) {
acc[findingType].options.push({
name: findingKey,
diff --git a/src/goalServices/reduceGoals.ts b/src/goalServices/reduceGoals.ts
index c6955540d8..6dd0e25716 100644
--- a/src/goalServices/reduceGoals.ts
+++ b/src/goalServices/reduceGoals.ts
@@ -187,7 +187,7 @@ export function reduceObjectivesForActivityReport(
(c) => ({
...c.dataValues,
id: c.monitoringReferences[0].standardId,
- name: `${c.monitoringReferences[0].acro} - ${c.citation} - ${c.monitoringReferences[0].findingType}`,
+ name: `${c.monitoringReferences[0].acro} - ${c.citation} - ${c.monitoringReferences[0].findingSource}`,
}),
))
: null;
@@ -278,7 +278,7 @@ export function reduceObjectivesForActivityReport(
{
...c.dataValues,
id: c.monitoringReferences[0].standardId,
- name: `${c.monitoringReferences[0].acro} - ${c.citation} - ${c.monitoringReferences[0].findingType}`,
+ name: `${c.monitoringReferences[0].acro} - ${c.citation} - ${c.monitoringReferences[0].findingSource}`,
}),
),
)
diff --git a/src/services/reportCache.test.js b/src/services/reportCache.test.js
index aec48837b3..78210c6b1b 100644
--- a/src/services/reportCache.test.js
+++ b/src/services/reportCache.test.js
@@ -141,7 +141,15 @@ describe('activityReportObjectiveCitation', () => {
],
});
- goal = await createGoal({ grantId: grant.id, status: GOAL_STATUS.IN_PROGRESS });
+ goal = await Goal.create({
+ name: faker.lorem.sentence(20),
+ status: GOAL_STATUS.NOT_STARTED,
+ endDate: null,
+ isFromSmartsheetTtaPlan: false,
+ onApprovedAR: false,
+ grantId: grant.id,
+ createdVia: 'monitoring',
+ });
objective = await Objective.create({
goalId: goal.id,
@@ -176,7 +184,7 @@ describe('activityReportObjectiveCitation', () => {
{
citation: 'Citation 1',
monitoringReferences: [{
- grantId: 1,
+ grantId: grant.id,
findingId: 1,
reviewName: 'Review 1',
}],
@@ -200,7 +208,7 @@ describe('activityReportObjectiveCitation', () => {
expect(createdAroCitations).toHaveLength(1);
expect(createdAroCitations[0].citation).toEqual('Citation 1');
expect(createdAroCitations[0].monitoringReferences).toEqual([{
- grantId: 1,
+ grantId: grant.id,
findingId: 1,
reviewName: 'Review 1',
}]);
@@ -211,7 +219,7 @@ describe('activityReportObjectiveCitation', () => {
id: citation1Id,
citation: 'Citation 1 Updated',
monitoringReferences: [{
- grantId: 1,
+ grantId: grant.id,
findingId: 1,
reviewName: 'Review 1 Updated',
}],
@@ -232,7 +240,7 @@ describe('activityReportObjectiveCitation', () => {
expect(updatedAroCitations).toHaveLength(1);
expect(updatedAroCitations[0].citation).toEqual('Citation 1 Updated');
expect(updatedAroCitations[0].monitoringReferences).toEqual([{
- grantId: 1,
+ grantId: grant.id,
findingId: 1,
reviewName: 'Review 1 Updated',
}]);
diff --git a/src/tools/createMonitoringGoals.test.js b/src/tools/createMonitoringGoals.test.js
index 5d1ddac60c..580330b911 100644
--- a/src/tools/createMonitoringGoals.test.js
+++ b/src/tools/createMonitoringGoals.test.js
@@ -1695,7 +1695,7 @@ describe('createMonitoringGoals', () => {
// Assert that the goal that was created was the monitoring goal and is using the correct template.
expect(grant1Goals[0].goalTemplateId).toBe(goalTemplate.id);
expect(grant1Goals[0].name).toBe(goalTemplateName);
- expect(grant1Goals[0].status).toBe('Not started');
+ expect(grant1Goals[0].status).toBe('Not Started');
// CASE 2: Does not create a monitoring goal for a grant that already has one.
const grant2Goals = await Goal.findAll({ where: { grantId: grantThatAlreadyHasMonitoringGoal2.id } });
@@ -1733,7 +1733,7 @@ describe('createMonitoringGoals', () => {
expect(grant9Goals.length).toBe(1);
expect(grant9Goals[0].goalTemplateId).toBe(goalTemplate.id);
expect(grant9Goals[0].name).toBe(goalTemplateName);
- expect(grant9Goals[0].status).toBe('Not started');
+ expect(grant9Goals[0].status).toBe('Not Started');
// CASE 10: Creates a monitoring goal ONLY for the grant that initially had the monitoring goal and does NOT create one for the split grant..
const grant10AGoals = await Goal.findAll({ where: { grantId: grantBeingMonitoredSplit10A.id } });
From b892831c89f6d5790160bc57c31206fae3ae8a3d Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Tue, 10 Dec 2024 15:26:23 -0500
Subject: [PATCH 101/198] Fix BE test
---
src/services/reportCache.test.js | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/services/reportCache.test.js b/src/services/reportCache.test.js
index aec48837b3..9f3f5c1adf 100644
--- a/src/services/reportCache.test.js
+++ b/src/services/reportCache.test.js
@@ -176,7 +176,7 @@ describe('activityReportObjectiveCitation', () => {
{
citation: 'Citation 1',
monitoringReferences: [{
- grantId: 1,
+ grantId: grant.id,
findingId: 1,
reviewName: 'Review 1',
}],
@@ -200,7 +200,7 @@ describe('activityReportObjectiveCitation', () => {
expect(createdAroCitations).toHaveLength(1);
expect(createdAroCitations[0].citation).toEqual('Citation 1');
expect(createdAroCitations[0].monitoringReferences).toEqual([{
- grantId: 1,
+ grantId: grant.id,
findingId: 1,
reviewName: 'Review 1',
}]);
@@ -211,7 +211,7 @@ describe('activityReportObjectiveCitation', () => {
id: citation1Id,
citation: 'Citation 1 Updated',
monitoringReferences: [{
- grantId: 1,
+ grantId: grant.id,
findingId: 1,
reviewName: 'Review 1 Updated',
}],
@@ -232,7 +232,7 @@ describe('activityReportObjectiveCitation', () => {
expect(updatedAroCitations).toHaveLength(1);
expect(updatedAroCitations[0].citation).toEqual('Citation 1 Updated');
expect(updatedAroCitations[0].monitoringReferences).toEqual([{
- grantId: 1,
+ grantId: grant.id,
findingId: 1,
reviewName: 'Review 1 Updated',
}]);
From 0f590158973f69ab72a08082e5e6cb76d9394968 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Tue, 10 Dec 2024 15:50:30 -0500
Subject: [PATCH 102/198] switch finding type to source
---
.../src/pages/ActivityReport/Pages/components/GoalPicker.js | 2 +-
src/goalServices/reduceGoals.ts | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js b/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js
index d9186bd6a2..8cfb4466ff 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js
@@ -136,7 +136,7 @@ const GoalPicker = ({
acc[findingType] = { label: findingType, options: [] };
}
- const findingKey = `${currentGrant.acro} - ${currentGrant.citation} - ${currentGrant.findingType}`;
+ const findingKey = `${currentGrant.acro} - ${currentGrant.citation} - ${currentGrant.findingSource}`;
if (!acc[findingType].options.find((option) => option.name === findingKey)) {
acc[findingType].options.push({
name: findingKey,
diff --git a/src/goalServices/reduceGoals.ts b/src/goalServices/reduceGoals.ts
index c6955540d8..6dd0e25716 100644
--- a/src/goalServices/reduceGoals.ts
+++ b/src/goalServices/reduceGoals.ts
@@ -187,7 +187,7 @@ export function reduceObjectivesForActivityReport(
(c) => ({
...c.dataValues,
id: c.monitoringReferences[0].standardId,
- name: `${c.monitoringReferences[0].acro} - ${c.citation} - ${c.monitoringReferences[0].findingType}`,
+ name: `${c.monitoringReferences[0].acro} - ${c.citation} - ${c.monitoringReferences[0].findingSource}`,
}),
))
: null;
@@ -278,7 +278,7 @@ export function reduceObjectivesForActivityReport(
{
...c.dataValues,
id: c.monitoringReferences[0].standardId,
- name: `${c.monitoringReferences[0].acro} - ${c.citation} - ${c.monitoringReferences[0].findingType}`,
+ name: `${c.monitoringReferences[0].acro} - ${c.citation} - ${c.monitoringReferences[0].findingSource}`,
}),
),
)
From 6422cef71cb57373cdddd2a1243ad7741f8f3b14 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Tue, 10 Dec 2024 17:31:48 -0500
Subject: [PATCH 103/198] fixes per UX
---
frontend/src/components/Req.js | 2 +-
.../Pages/__tests__/goalsObjectives.js | 7 +++++
.../ActivityReport/Pages/goalsObjectives.js | 26 +++++++++----------
.../pages/ActivityReport/__tests__/index.js | 7 ++---
frontend/src/pages/ActivityReport/index.scss | 2 +-
src/services/citations.ts | 4 +--
6 files changed, 28 insertions(+), 20 deletions(-)
diff --git a/frontend/src/components/Req.js b/frontend/src/components/Req.js
index a3eacd7b42..2c30b4372f 100644
--- a/frontend/src/components/Req.js
+++ b/frontend/src/components/Req.js
@@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
export default function Req({ className, announce }) {
const aria = announce ? { 'aria-label': 'required' } : { 'aria-hidden': true };
return (
-
+
{' '}
*
diff --git a/frontend/src/pages/ActivityReport/Pages/__tests__/goalsObjectives.js b/frontend/src/pages/ActivityReport/Pages/__tests__/goalsObjectives.js
index 93fd18f625..9d8164e8e1 100644
--- a/frontend/src/pages/ActivityReport/Pages/__tests__/goalsObjectives.js
+++ b/frontend/src/pages/ActivityReport/Pages/__tests__/goalsObjectives.js
@@ -316,6 +316,13 @@ describe('goals objectives', () => {
// Expect to find an h3 with the text "Goal summary".
expect(await screen.findByText('Goal summary', { selector: '.margin-bottom-0.margin-top-4' })).toBeVisible();
});
+
+ it('shows the start date warning if the start date has the value of "Invalid date"', async () => {
+ renderGoals([1], 'recipient', [], false, false, jest.fn(), 'Invalid date');
+ expect(await screen.findByText('To add goals and objectives, indicate in the')).toBeVisible();
+ expect(screen.queryByText('who the activity was for')).toBeNull();
+ expect(await screen.findByText('the start date of the activity')).toBeVisible();
+ });
});
describe('when activity recipient type is other entity"', () => {
diff --git a/frontend/src/pages/ActivityReport/Pages/goalsObjectives.js b/frontend/src/pages/ActivityReport/Pages/goalsObjectives.js
index 48ddf6e494..acc265b3a3 100644
--- a/frontend/src/pages/ActivityReport/Pages/goalsObjectives.js
+++ b/frontend/src/pages/ActivityReport/Pages/goalsObjectives.js
@@ -315,24 +315,30 @@ const GoalsObjectives = ({
isOtherEntityReport && !isObjectivesFormClosed
);
+ const startDateHasValue = startDate && startDate !== 'Invalid date';
+ const alertIsDisplayed = (!isOtherEntityReport && !isRecipientReport)
+ || !startDateHasValue
+ || (isRecipientReport && !showGoals);
const determineReportTypeAlert = () => {
const messages = [];
// Check that the report type is set.
- if (!isOtherEntityReport && !isRecipientReport) {
- messages.push('who the activity was for');
+ if ((!isOtherEntityReport && !isRecipientReport)
+ || (isRecipientReport && !showGoals)) {
+ messages.push('Who the activity was for');
}
// Check the startDate is set.
- if (!startDate) {
- messages.push('the start date of the activity');
+ if (!startDateHasValue) {
+ messages.push('Start date of the activity');
}
if (messages.length > 0) {
return (
-
+
To add goals and objectives, indicate in the
{' '}
Activity Summary
+ :
{' '}
\n',
+ supportType: 'Planning',
+ isNew: false,
+ arOrder: 1,
+ objectiveCreatedHere: true,
+ topics: [],
+ resources: [],
+ files: [],
+ courses: [],
+ citations: [
+ {
+ id: 200205,
+ activityReportObjectiveId: 241644,
+ citation: '1302.12(k)',
+ monitoringReferences: [
+ {
+ acro: 'AOC',
+ name: 'AOC - 1302.12(k) - Monitoring ERSEA: Eligibility, Recruitment, Selection, Enrollment, and Attendance',
+ grantId: 11966,
+ citation: '1302.12(k)',
+ severity: 3,
+ findingId: '8D18F077-CD6F-4869-AB21-E76EB682433B',
+ reviewName: '230706F2',
+ standardId: 200205,
+ findingType: 'Area of Concern',
+ grantNumber: '01CH011566',
+ findingSource: 'Monitoring ERSEA: Eligibility, Recruitment, Selection, Enrollment, and Attendance',
+ reportDeliveryDate: '2023-06-26T04:00:00+00:00',
+ monitoringFindingStatusName: 'Active',
+ },
+ ],
+ name: 'AOC - 1302.12(k) - Monitoring ERSEA: Eligibility, Recruitment, Selection, Enrollment, and Attendance',
+ },
+ ],
+ },
+ ],
+ isSourceEditable: true,
+ goalNumber: 'G-90740',
+ promptsForReview: [],
+ isNew: false,
+ goalNumbers: [
+ 'G-90740',
+ 'G-90683',
+ 'G-90846',
+ ],
+ goalIds: [
+ 90740,
+ 90683,
+ 90846,
+ ],
+ grantIds: [
+ 11597,
+ 11074,
+ 11966,
+ ],
+ collaborators: [
+ {
+ goalNumber: 'G-90683',
+ },
+ ],
+ isReopenedGoal: false,
+ },
+ ],
+ }}
+ />);
+
+ expect(await screen.findByTestId('review-citation-label')).toHaveTextContent('R1 - GRANT2 - EHS');
+ expect(await screen.findByTestId('review-citation-listitem')).toHaveTextContent('AOC - 1302.12(k) - Monitoring ERSEA: Eligibility, Recruitment, Selection, Enrollment, and Attendance');
+ });
});
From bc32cf7a4e9f3d94eddc3cf0a9f832fb216de29b Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Thu, 12 Dec 2024 10:52:22 -0500
Subject: [PATCH 114/198] add test for recipient review section
---
.../__tests__/RecipientReviewSection.js | 148 +++++++++++++++++-
1 file changed, 142 insertions(+), 6 deletions(-)
diff --git a/frontend/src/pages/ActivityReport/Pages/components/__tests__/RecipientReviewSection.js b/frontend/src/pages/ActivityReport/Pages/components/__tests__/RecipientReviewSection.js
index 1c1f35f269..01f09ab10d 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/__tests__/RecipientReviewSection.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/__tests__/RecipientReviewSection.js
@@ -1,7 +1,6 @@
/* eslint-disable react/prop-types */
/* eslint-disable react/jsx-props-no-spreading */
import '@testing-library/jest-dom';
-
import {
render, screen,
} from '@testing-library/react';
@@ -107,12 +106,145 @@ const defaultGoalsAndObjectives = [{
},
],
},
+{
+ id: 90740,
+ name: '(Monitoring) The recipient will develop and implement a QIP/CAP to address monitoring findings.',
+ status: 'In Progress',
+ endDate: '',
+ isCurated: true,
+ grantId: 11597,
+ goalTemplateId: 24696,
+ onAR: true,
+ onApprovedAR: true,
+ rtrOrder: 1,
+ source: 'Federal monitoring issues, including CLASS and RANs',
+ regionId: 1,
+ recipientId: 1442,
+ standard: 'Monitoring',
+ prompts: [],
+ statusChanges: [
+ {
+ oldStatus: 'Not Started',
+ },
+ ],
+ activityReportGoals: [
+ {
+ endDate: null,
+ id: 155612,
+ activityReportId: 48418,
+ goalId: 90740,
+ isRttapa: null,
+ name: '(Monitoring) The recipient will develop and implement a QIP/CAP to address monitoring findings.',
+ status: 'In Progress',
+ timeframe: null,
+ closeSuspendReason: null,
+ closeSuspendContext: null,
+ source: 'Federal monitoring issues, including CLASS and RANs',
+ isActivelyEdited: false,
+ originalGoalId: null,
+ },
+ ],
+ objectives: [
+ {
+ id: 231994,
+ otherEntityId: null,
+ goalId: 90740,
+ title: 'test',
+ status: 'In Progress',
+ objectiveTemplateId: 565,
+ onAR: true,
+ onApprovedAR: true,
+ createdVia: 'activityReport',
+ rtrOrder: 1,
+ value: 231994,
+ ids: [
+ 231994,
+ 231995,
+ 231996,
+ ],
+ ttaProvided: 'tta
\n',
+ supportType: 'Planning',
+ isNew: false,
+ arOrder: 1,
+ objectiveCreatedHere: true,
+ topics: [],
+ resources: [],
+ files: [],
+ courses: [],
+ citations: [
+ {
+ id: 200205,
+ activityReportObjectiveId: 241644,
+ citation: '1302.12(k)',
+ monitoringReferences: [
+ {
+ acro: 'AOC',
+ name: 'AOC - 1302.12(k) - Monitoring ERSEA: Eligibility, Recruitment, Selection, Enrollment, and Attendance',
+ grantId: 11966,
+ citation: '1302.12(k)',
+ severity: 3,
+ findingId: '8D18F077-CD6F-4869-AB21-E76EB682433B',
+ reviewName: '230706F2',
+ standardId: 200205,
+ findingType: 'Area of Concern',
+ grantNumber: '01CH011566',
+ findingSource: 'Monitoring ERSEA: Eligibility, Recruitment, Selection, Enrollment, and Attendance',
+ reportDeliveryDate: '2023-06-26T04:00:00+00:00',
+ monitoringFindingStatusName: 'Active',
+ },
+ ],
+ name: 'AOC - 1302.12(k) - Monitoring ERSEA: Eligibility, Recruitment, Selection, Enrollment, and Attendance',
+ },
+ ],
+ },
+ ],
+ isSourceEditable: true,
+ goalNumber: 'G-90740',
+ promptsForReview: [],
+ isNew: false,
+ goalNumbers: [
+ 'G-90740',
+ 'G-90683',
+ 'G-90846',
+ ],
+ goalIds: [
+ 90740,
+ 90683,
+ 90846,
+ ],
+ grantIds: [
+ 11597,
+ 11074,
+ 11966,
+ ],
+ collaborators: [
+ {
+ goalNumber: 'G-90683',
+ },
+ ],
+ isReopenedGoal: false,
+},
];
const RenderRecipientReviewSection = ({ goalsAndObjectives }) => {
const history = createMemoryHistory();
const hookForm = useForm();
+ hookForm.getValues = () => ({
+ activityRecipients: [
+ {
+ id: 11074,
+ activityRecipientId: 11074,
+ name: 'R1 - GRANT1 - HS',
+ },
+ {
+ id: 11966,
+ activityRecipientId: 11966,
+ name: 'R1 - GRANT2 - EHS',
+ },
+ ],
+ });
+
hookForm.watch = () => ({
goalsAndObjectives,
calculatedStatus: 'Draft',
@@ -143,8 +275,8 @@ describe('RecipientReviewSection', () => {
RenderReviewSection(defaultGoalsAndObjectives);
// Make sure we have the correct number of goal and objective headers.
- expect(screen.queryAllByText(/Goal summary/i).length).toBe(2);
- expect(screen.queryAllByText(/Objective summary/i).length).toBe(3);
+ expect(screen.queryAllByText(/Goal summary/i).length).toBe(3);
+ expect(screen.queryAllByText(/Objective summary/i).length).toBe(4);
// Goal 1
expect(screen.getByText(/this is my 1st goal title/i)).toBeInTheDocument();
@@ -154,7 +286,7 @@ describe('RecipientReviewSection', () => {
// Goal 1 - Objective 1
expect(screen.getByText('Goal 1 - Objective 1')).toBeInTheDocument();
expect(screen.getByText('TTA Provided for Goal 1 - Objective 1')).toBeInTheDocument();
- expect(screen.getByText(/In Progress/)).toBeInTheDocument();
+ expect(screen.getAllByText(/In Progress/)).toHaveLength(2);
expect(screen.getByText(/test.txt/)).toBeInTheDocument();
expect(screen.getByText(/test.csv/)).toBeInTheDocument();
expect(screen.getByText(/https:\/\/www.govtest1.com/)).toBeInTheDocument();
@@ -184,8 +316,12 @@ describe('RecipientReviewSection', () => {
expect(screen.getByText(/Topic 2/)).toBeInTheDocument();
// Make sure we have the correct number of resources and files.
- expect(screen.queryAllByText(/Resource links/i).length).toBe(3);
- expect(screen.queryAllByText(/Resource attachments/i).length).toBe(3);
+ expect(screen.queryAllByText(/Resource links/i).length).toBe(4);
+ expect(screen.queryAllByText(/Resource attachments/i).length).toBe(4);
+
+ // citation display
+ expect(await screen.findByTestId('review-citation-label')).toHaveTextContent('R1 - GRANT2 - EHS');
+ expect(await screen.findByTestId('review-citation-listitem')).toHaveTextContent('AOC - 1302.12(k) - Monitoring ERSEA: Eligibility, Recruitment, Selection, Enrollment, and Attendance');
});
it('renders fei response correctly', async () => {
From 6297375c234472dc0aceabbdd8fe83a05512d8cc Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Thu, 12 Dec 2024 11:07:25 -0500
Subject: [PATCH 115/198] add test for component
---
.../Pages/components/RenderReviewCitations.js | 26 +++++-
.../__tests__/RenderReviewCitations.js | 93 +++++++++++++++++++
2 files changed, 115 insertions(+), 4 deletions(-)
create mode 100644 frontend/src/pages/ActivityReport/Pages/components/__tests__/RenderReviewCitations.js
diff --git a/frontend/src/pages/ActivityReport/Pages/components/RenderReviewCitations.js b/frontend/src/pages/ActivityReport/Pages/components/RenderReviewCitations.js
index cf9b7e9744..bb78832b59 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/RenderReviewCitations.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/RenderReviewCitations.js
@@ -2,8 +2,7 @@ import React from 'react';
import { uniqueId, uniq } from 'lodash';
import PropTypes from 'prop-types';
-// eslint-disable-next-line max-len
-export const formatCitations = (citations, activityRecipients) => citations.reduce((acc, citation) => {
+const formatCitations = (citations, activityRecipients) => citations.reduce((acc, citation) => {
const { monitoringReferences } = citation;
monitoringReferences.forEach((reference) => {
// eslint-disable-next-line max-len
@@ -35,8 +34,27 @@ export default function RenderReviewCitations({ citations, activityRecipients, c
}
RenderReviewCitations.propTypes = {
- // eslint-disable-next-line react/forbid-prop-types
- citations: PropTypes.object.isRequired, // we don't know the keys of this object
+ citations: PropTypes.arrayOf(
+ PropTypes.shape({
+ monitoringReferences: PropTypes.arrayOf(
+ PropTypes.shape({
+ acro: PropTypes.string,
+ name: PropTypes.string,
+ grantId: PropTypes.number,
+ citation: PropTypes.string,
+ severity: PropTypes.number,
+ findingId: PropTypes.string,
+ reviewName: PropTypes.string,
+ standardId: PropTypes.string,
+ findingType: PropTypes.string,
+ grantNumber: PropTypes.string,
+ findingSource: PropTypes.string,
+ reportDeliveryDate: PropTypes.string,
+ monitoringFindingStatusName: PropTypes.string,
+ }),
+ ),
+ }),
+ ).isRequired,
className: PropTypes.string,
activityRecipients: PropTypes.arrayOf(
PropTypes.shape({
diff --git a/frontend/src/pages/ActivityReport/Pages/components/__tests__/RenderReviewCitations.js b/frontend/src/pages/ActivityReport/Pages/components/__tests__/RenderReviewCitations.js
new file mode 100644
index 0000000000..7a6efd9401
--- /dev/null
+++ b/frontend/src/pages/ActivityReport/Pages/components/__tests__/RenderReviewCitations.js
@@ -0,0 +1,93 @@
+import '@testing-library/jest-dom';
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import RenderReviewCitations from '../RenderReviewCitations';
+
+describe('RenderReviewCitations', () => {
+ const activityRecipients = [
+ {
+ id: 11074,
+ activityRecipientId: 11074,
+ name: 'R1 - GRANT1 - HS',
+ },
+ {
+ id: 11966,
+ activityRecipientId: 11966,
+ name: 'R1 - GRANT2 - EHS',
+ },
+ ];
+
+ const citations = [
+ {
+ id: 200205,
+ activityReportObjectiveId: 241644,
+ citation: '1302.12(k)',
+ monitoringReferences: [
+ {
+ acro: 'AOC',
+ name: 'AOC - 1302.12(k) - Monitoring ERSEA: Eligibility, Recruitment, Selection, Enrollment, and Attendance',
+ grantId: 'bad-recipient-id',
+ citation: '1302.12(k)',
+ severity: 3,
+ findingId: '8D18F077-CD6F-4869-AB21-E76EB682433B',
+ reviewName: '230706F2',
+ standardId: 200205,
+ findingType: 'Area of Concern',
+ grantNumber: '01CH011566',
+ findingSource: 'Monitoring ERSEA: Eligibility, Recruitment, Selection, Enrollment, and Attendance',
+ reportDeliveryDate: '2023-06-26T04:00:00+00:00',
+ monitoringFindingStatusName: 'Active',
+ },
+ {
+ acro: 'AOC',
+ name: 'AOC - 1302.12(k) - Monitoring ERSEA: Eligibility, Recruitment, Selection, Enrollment, and Attendance',
+ grantId: 11966,
+ citation: '1302.12(k)',
+ severity: 3,
+ findingId: '8D18F077-CD6F-4869-AB21-E76EB682433B',
+ reviewName: '230706F2',
+ standardId: 200205,
+ findingType: 'Area of Concern',
+ grantNumber: '01CH011566',
+ findingSource: 'Monitoring ERSEA: Eligibility, Recruitment, Selection, Enrollment, and Attendance',
+ reportDeliveryDate: '2023-06-26T04:00:00+00:00',
+ monitoringFindingStatusName: 'Active',
+ },
+ {
+ acro: 'AOC',
+ name: 'AOC - 1302.12(k) - Monitoring ERSEA: Eligibility, Recruitment, Selection, Enrollment, and Attendance',
+ grantId: 11074,
+ citation: '1302.12(k)',
+ severity: 3,
+ findingId: '8D18F077-CD6F-4869-AB21-E76EB682433B',
+ reviewName: '230706F2',
+ standardId: 200205,
+ findingType: 'Area of Concern',
+ grantNumber: '01CH011566',
+ findingSource: 'Monitoring ERSEA: Eligibility, Recruitment, Selection, Enrollment, and Attendance',
+ reportDeliveryDate: '2023-06-26T04:00:00+00:00',
+ monitoringFindingStatusName: 'Active',
+ },
+ ],
+ name: 'AOC - 1302.12(k) - Monitoring ERSEA: Eligibility, Recruitment, Selection, Enrollment, and Attendance',
+ },
+ ];
+
+ it('renders the citations', async () => {
+ render( );
+
+ const recipient1 = screen.getByText('R1 - GRANT1 - HS');
+ expect(recipient1).toBeVisible();
+
+ const recipient2 = screen.getByText('R1 - GRANT2 - EHS');
+ expect(recipient2).toBeVisible();
+
+ const labels = await screen.findAllByTestId('review-citation-label');
+ expect(labels).toHaveLength(2);
+
+ const listItems = await screen.findAllByTestId('review-citation-listitem');
+ expect(listItems).toHaveLength(2);
+
+ expect(await screen.findAllByText('AOC - 1302.12(k) - Monitoring ERSEA: Eligibility, Recruitment, Selection, Enrollment, and Attendance')).toHaveLength(2);
+ });
+});
From cf55f230090b0ffe16706c0f1237a707948551b2 Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Thu, 12 Dec 2024 12:57:09 -0500
Subject: [PATCH 116/198] Update e2e test
---
tests/e2e/activity-report.spec.ts | 54 +++++++++++++------------------
1 file changed, 23 insertions(+), 31 deletions(-)
diff --git a/tests/e2e/activity-report.spec.ts b/tests/e2e/activity-report.spec.ts
index ff8c311d60..3c83e2ba99 100644
--- a/tests/e2e/activity-report.spec.ts
+++ b/tests/e2e/activity-report.spec.ts
@@ -374,7 +374,7 @@ test.describe('Activity Report', () => {
await expect(page.getByRole('heading', { name: `TTA activity report R0${regionNumber}-AR-${arNumber}` })).toBeVisible();
await expect(page.getByText(/date approved/i)).toBeVisible();
- const recipients = await page.locator('span:near(p:text("Recipient names"))').first().textContent();
+ const recipients = await page.locator('span:near(div:text("Recipient names"))').first().textContent();
const grants = getGrants(recipients || '');
// navigate to the Recipient TTA Records page
@@ -385,20 +385,12 @@ test.describe('Activity Report', () => {
await page.getByRole('link', { name: 'RTTAPA' }).click();
// check that previously created goals g1 and g2 are visible
// Assert there are two instances of 'g1' and 'g2' on the page
- await expect(page.getByText('g1', { exact: true }).first()).toBeTruthy();
- await expect(page.getByText('g1', { exact: true }).nth(1)).toBeTruthy();
+ expect(page.getByText('g1', { exact: true }).first()).toBeTruthy();
+ expect(page.getByText('g1', { exact: true }).nth(1)).toBeTruthy();
-
- await expect(page.getByText('g2', { exact: true }).first()).toBeTruthy();
- await expect(page.getByText('g2', { exact: true }).nth(1)).toBeTruthy();
-
- // look for the goals heading for the previously created goal, e.g. 'Goal G-6, G-5RTTAPA'
- const g1Goals = page.locator('h3:above(p:text("g1"))').first();
-
- // strip 'Goals' and 'RTTAPA' from g1GoalsTxt: e.g "Goal G-5, G-6RTTAPA" will become "G-5, G-6"
- // look for the goals heading for the previously created goal, e.g. 'Goal G-8, G-7RTTAPA'
- const g2Goals = page.locator('h3:above(p:text("g2"))').first();
+ expect(page.getByText('g2', { exact: true }).first()).toBeTruthy();
+ expect(page.getByText('g2', { exact: true }).nth(1)).toBeTruthy();
/* We have Two goals and Two Recipients this should result in 4 goals */
// Expand objectives for G1.
@@ -413,14 +405,14 @@ test.describe('Activity Report', () => {
await page.getByRole('button', { name: `View objectives for goal G-5` }).click();
- await expect(page.getByText('g1o1', { exact: true }).first()).toBeTruthy();
- await expect(page.getByText('g1o1', { exact: true }).nth(1)).toBeTruthy();
+ expect(page.getByText('g1o1', { exact: true }).first()).toBeTruthy();
+ expect(page.getByText('g1o1', { exact: true }).nth(1)).toBeTruthy();
// verify a link to the activity report is found in the objective section
- await expect(page.getByRole('link', { name: `R0${regionNumber}-AR-${arNumber}` }).first()).toBeTruthy();
- await expect(page.getByRole('link', { name: `R0${regionNumber}-AR-${arNumber}` }).nth(1)).toBeTruthy();
+ expect(page.getByRole('link', { name: `R0${regionNumber}-AR-${arNumber}` }).first()).toBeTruthy();
+ expect(page.getByRole('link', { name: `R0${regionNumber}-AR-${arNumber}` }).nth(1)).toBeTruthy();
// Access parent with '..'
- await expect(page.getByText('g1o1', { exact: true }).locator('..').locator('..').getByText('Grant numbers').nth(0)).toBeTruthy();
- await expect(page.getByText('g1o1', { exact: true }).locator('..').locator('..').getByText('Grant numbers').nth(1)).toBeTruthy();
+ expect(page.getByText('g1o1', { exact: true }).locator('..').locator('..').getByText('Grant numbers').nth(0)).toBeTruthy();
+ expect(page.getByText('g1o1', { exact: true }).locator('..').locator('..').getByText('Grant numbers').nth(1)).toBeTruthy();
// verify the grants are visible in the objective section
await Promise.all(
grants.map(async (grant) => expect(page.getByText('g1o1', { exact: true }).locator('..').locator('..').getByText(grant)).toBeTruthy()),
@@ -434,23 +426,23 @@ test.describe('Activity Report', () => {
expect(goalOneContentB).toContain('Behavioral / Mental Health / Trauma');
// verify the end date is visible in the objective section
- await expect(page.getByText('g1o1', { exact: true }).first().locator('..').locator('..').getByText('12/01/2050')).toBeTruthy();
- await expect(page.getByText('g1o1', { exact: true }).nth(1).locator('..').locator('..').getByText('12/01/2050')).toBeTruthy();
+ expect(page.getByText('g1o1', { exact: true }).first().locator('..').locator('..').getByText('12/01/2050')).toBeTruthy();
+ expect(page.getByText('g1o1', { exact: true }).nth(1).locator('..').locator('..').getByText('12/01/2050')).toBeTruthy();
// verify the correct status for the objective is visible
- await expect(page.getByText('g1o1', { exact: true }).first().locator('..').locator('..').getByText('Not started')).toBeTruthy();
- await expect(page.getByText('g1o1', { exact: true }).nth(1).locator('..').locator('..').getByText('Not started')).toBeTruthy();
+ expect(page.getByText('g1o1', { exact: true }).first().locator('..').locator('..').getByText('Not started')).toBeTruthy();
+ expect(page.getByText('g1o1', { exact: true }).nth(1).locator('..').locator('..').getByText('Not started')).toBeTruthy();
// Expand goals for G2.
await page.getByRole('button', { name: `View objectives for goal G-7` }).click();
await page.getByRole('button', { name: `View objectives for goal G-8` }).click();
- await expect(page.getByText('g2o1', { exact: true }).first()).toBeTruthy();
- await expect(page.getByText('g2o1', { exact: true }).nth(1)).toBeTruthy();
+ expect(page.getByText('g2o1', { exact: true }).first()).toBeTruthy();
+ expect(page.getByText('g2o1', { exact: true }).nth(1)).toBeTruthy();
// verify a link to the activity report is found in the objective section
- await expect(page.getByText('g2o1', { exact: true }).first().locator('..').locator('..').getByRole('link', { name: `R0${regionNumber}-AR-${arNumber}` })).toBeTruthy();
- await expect(page.getByText('g2o1', { exact: true }).nth(1).locator('..').locator('..').getByRole('link', { name: `R0${regionNumber}-AR-${arNumber}` })).toBeTruthy();
- await expect(page.getByText('g2o1', { exact: true }).locator('..').locator('..').getByText('Grant numbers').first()).toBeTruthy();
- await expect(page.getByText('g2o1', { exact: true }).locator('..').locator('..').getByText('Grant numbers').nth(1)).toBeTruthy();
+ expect(page.getByText('g2o1', { exact: true }).first().locator('..').locator('..').getByRole('link', { name: `R0${regionNumber}-AR-${arNumber}` })).toBeTruthy();
+ expect(page.getByText('g2o1', { exact: true }).nth(1).locator('..').locator('..').getByRole('link', { name: `R0${regionNumber}-AR-${arNumber}` })).toBeTruthy();
+ expect(page.getByText('g2o1', { exact: true }).locator('..').locator('..').getByText('Grant numbers').first()).toBeTruthy();
+ expect(page.getByText('g2o1', { exact: true }).locator('..').locator('..').getByText('Grant numbers').nth(1)).toBeTruthy();
// verify the grants are visible in the objective section
await Promise.all(
grants.map(async (grant) => expect(page.getByText('g2o1', { exact: true }).locator('..').locator('..').getByText(grant)).toBeTruthy()),
@@ -460,9 +452,9 @@ test.describe('Activity Report', () => {
const goalTwoContentB = await page.getByText('g2o1', {exact: true}).nth(1).locator('..').locator('..').textContent();
expect(goalTwoContentB).toContain('Change in Scope');
// verify the end date is visible in the objective section
- await expect(page.getByText('g2o1', { exact: true }).first().locator('..').locator('..').getByText('12/01/2050')).toBeTruthy();
+ expect(page.getByText('g2o1', { exact: true }).first().locator('..').locator('..').getByText('12/01/2050')).toBeTruthy();
// verify the correct status for the objective is visible
- await expect(page.getByText('g2o1', { exact: true }).nth(1).locator('..').locator('..').getByText('Not started')).toBeTruthy();
+ expect(page.getByText('g2o1', { exact: true }).nth(1).locator('..').locator('..').getByText('Not started')).toBeTruthy();
// check g1
await page.getByText('g1', { exact: true }).first().locator('..').locator('..').locator('..')
From 5e6d5f87859ae1b0bf89e39e621ea480711e3ac8 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Thu, 12 Dec 2024 14:38:56 -0500
Subject: [PATCH 117/198] add test
---
.../Pages/components/__tests__/GoalPicker.js | 418 +++++-------------
src/services/citations.ts | 4 +-
2 files changed, 124 insertions(+), 298 deletions(-)
diff --git a/frontend/src/pages/ActivityReport/Pages/components/__tests__/GoalPicker.js b/frontend/src/pages/ActivityReport/Pages/components/__tests__/GoalPicker.js
index beeca4ce53..48452c76a0 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/__tests__/GoalPicker.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/__tests__/GoalPicker.js
@@ -1,3 +1,4 @@
+/* eslint-disable react/prop-types */
/* eslint-disable react/jsx-props-no-spreading */
import '@testing-library/jest-dom';
import {
@@ -5,8 +6,11 @@ import {
screen,
act,
fireEvent,
+ waitFor,
} from '@testing-library/react';
import React from 'react';
+import { Router } from 'react-router';
+import { createMemoryHistory } from 'history';
import fetchMock from 'fetch-mock';
import { FormProvider, useForm } from 'react-hook-form';
import selectEvent from 'react-select-event';
@@ -16,6 +20,8 @@ import GoalPicker from '../GoalPicker';
import UserContext from '../../../../../UserContext';
import { mockRSSData } from '../../../../../testHelpers';
+const history = createMemoryHistory();
+
const defaultSelectedGoals = [
{
label: '123',
@@ -29,8 +35,13 @@ const defaultGoalForEditing = {
goalIds: [],
};
-// eslint-disable-next-line react/prop-types, object-curly-newline
-const GP = ({ availableGoals, selectedGoals, goalForEditing, goalTemplates }) => {
+const GP = ({
+ availableGoals,
+ selectedGoals,
+ goalForEditing,
+ goalTemplates,
+ additionalGrantRecipients = [],
+}) => {
const hookForm = useForm({
mode: 'onChange',
defaultValues: {
@@ -42,7 +53,7 @@ const GP = ({ availableGoals, selectedGoals, goalForEditing, goalTemplates }) =>
role: 'central office',
},
collaborators: [],
- activityRecipients: [{ activityRecipientId: 1 }],
+ activityRecipients: [...additionalGrantRecipients, { activityRecipientId: 1, name: 'Grant 1 Name' }],
},
});
@@ -59,14 +70,16 @@ const GP = ({ availableGoals, selectedGoals, goalForEditing, goalTemplates }) =>
},
}}
>
-
-
-
+
+
+
+
+
);
@@ -77,6 +90,7 @@ const renderGoalPicker = (
selectedGoals = defaultSelectedGoals,
goalForEditing = defaultGoalForEditing,
goalTemplates = [],
+ additionalGrantRecipients = [],
) => {
render(
,
);
};
@@ -97,78 +112,84 @@ describe('GoalPicker', () => {
afterEach(() => fetchMock.restore());
- it('you can select a goal', async () => {
- const availableGoals = [{
- label: 'Goal 1',
- value: 1,
- goalIds: [1],
- name: 'Goal 1',
- }];
-
- renderGoalPicker(availableGoals);
-
- const selector = await screen.findByLabelText(/Select recipient's goal*/i);
- const [availableGoal] = availableGoals;
+ it('correctly displays the monitoring warning if non monitoring recipients are selected', async () => {
+ fetchMock.get('/api/goal-templates/1/prompts?goalIds=1&goalIds=2', []);
+ fetchMock.get('/api/goal-templates/1/source?grantIds=2&grantIds=1', {
+ source: 'Federal monitoring issues, including CLASS and RANs',
+ });
- await selectEvent.select(selector, [availableGoal.label]);
+ // api/citations/region/1?grantIds=1&reportStartDate=2024-12-03
+ fetchMock.get('/api/citations/region/1?grantIds=1&reportStartDate=2024-12-03', [
+ {
+ citation: 'Not your citation',
+ grants: [
+ {
+ acro: 'DEF',
+ citation: 'test citation 1',
+ findingId: 1,
+ findingSource: 'source',
+ findingType: 'Not your citation type',
+ grantId: 2,
+ grantNumber: '123',
+ monitoringFindingStatusName: 'Active',
+ reportDeliveryDate: '2024-12-03',
+ reviewName: 'review name',
+ severity: 1,
+ },
+ ],
+ standardId: 1,
+ },
+ ]);
- const input = document.querySelector('[name="goalForEditing"');
- expect(input.value).toBe(availableGoal.value.toString());
- });
+ fetchMock.get('/api/citations/region/1?grantIds=1&grantIds=2&reportStartDate=2024-12-03', [
+ {
+ citation: 'Not your citation',
+ grants: [
+ {
+ acro: 'DEF',
+ citation: 'test citation 1',
+ findingId: 1,
+ findingSource: 'source',
+ findingType: 'Not your citation type',
+ grantId: 2,
+ grantNumber: '123',
+ monitoringFindingStatusName: 'Active',
+ reportDeliveryDate: '2024-12-03',
+ reviewName: 'review name',
+ severity: 1,
+ },
+ ],
+ standardId: 1,
+ },
+ ]);
- it('you can select a goal that has objectives, keeping the objectives', async () => {
const availableGoals = [{
label: 'Goal 1',
value: 1,
- goalIds: [1],
+ goalIds: [1, 2],
name: 'Goal 1',
+ objectives: [],
}];
-
- const goalForEditing = {
- objectives: [{
- topics: [],
- id: 1,
- title: 'Objective 1',
- resources: [],
- ttaProvided: '',
- objectiveCreatedHere: true,
- }],
- goalIds: [],
- };
-
- renderGoalPicker(
- availableGoals,
- defaultSelectedGoals,
- goalForEditing,
- );
-
- const selector = await screen.findByLabelText(/Select recipient's goal*/i);
- const [availableGoal] = availableGoals;
-
- await selectEvent.select(selector, [availableGoal.label]);
-
- expect(await screen.findByText('You have selected a different goal.')).toBeVisible();
-
- const button = await screen.findByRole('button', { name: /keep objective/i });
- userEvent.click(button);
-
- const input = document.querySelector('[name="goalForEditing"');
- expect(input.value).toBe(availableGoal.value.toString());
-
- const objective = await screen.findByText('Objective 1', { selector: 'textarea' });
- expect(objective).toBeVisible();
- expect(objective).toHaveAttribute('name', 'goalForEditing.objectives[0].title');
- });
-
- it('you can select a goal that has objectives, losing the objectives', async () => {
- const availableGoals = [{
- label: 'Goal 1',
+ const availableTemplates = [{
+ label: 'Monitoring Template Goal',
value: 1,
- goalIds: [1],
- name: 'Goal 1',
+ goalIds: [1, 2],
+ isCurated: true,
+ goalTemplateId: 1,
+ source: 'Federal monitoring issues, including CLASS and RANs',
+ standard: 'Monitoring',
+ objectives: [],
+ goals: [
+ {
+ grantId: 1,
+ },
+ {
+ grantId: 2,
+ },
+ ],
}];
-
const goalForEditing = {
+ standard: 'Monitoring',
objectives: [{
topics: [],
id: 1,
@@ -179,244 +200,49 @@ describe('GoalPicker', () => {
}],
goalIds: [],
};
-
- renderGoalPicker(
- availableGoals,
- defaultSelectedGoals,
- goalForEditing,
- );
-
- const selector = await screen.findByLabelText(/Select recipient's goal*/i);
- const [availableGoal] = availableGoals;
-
- await selectEvent.select(selector, [availableGoal.label]);
-
- expect(await screen.findByText('You have selected a different goal.')).toBeVisible();
-
- const button = await screen.findByRole('button', { name: /remove objective/i });
- userEvent.click(button);
-
- const input = document.querySelector('[name="goalForEditing"');
- expect(input.value).toBe(availableGoal.value.toString());
-
- const objective = document.querySelector('[name="goalForEditing.objectives[0].title"]');
- expect(objective).toBeNull();
- });
-
- it('you can select a goal with no selected goals', async () => {
- const availableGoals = [{
- label: 'Goal 1',
- value: 1,
- goalIds: [1],
- }];
-
- renderGoalPicker(availableGoals, null);
-
- const selector = await screen.findByLabelText(/Select recipient's goal*/i);
- const [availableGoal] = availableGoals;
-
- await selectEvent.select(selector, [availableGoal.label]);
-
- const input = document.querySelector('[name="goalForEditing"');
- expect(input.value).toBe(availableGoal.value.toString());
- });
-
- it('properly renders when there is no goal for editing selected', async () => {
- renderGoalPicker([], null);
- const selector = await screen.findByLabelText(/Select recipient's goal*/i);
-
+ act(() => {
+ renderGoalPicker(availableGoals, null, goalForEditing, availableTemplates, [{ activityRecipientId: 2, name: 'Grant 2 Name' }]);
+ });
+ let selector = screen.queryByLabelText(/Select recipient's goal*/i);
expect(selector).toBeVisible();
- });
- describe('with checkbox', () => {
- it('you can toggle the list of curated goals', async () => {
- fetchMock.get('/api/goal-templates/1/prompts?goalIds=1', []);
- fetchMock.get('/api/goal-templates/1/prompts', []);
- const availableGoals = [];
-
- renderGoalPicker(availableGoals, null, null, [
- {
- id: 1,
- goalTemplateId: 1,
- name: 'Goal Template 1',
- label: 'Goal Template 1',
- goals: [],
- isCurated: true,
- objectives: [],
- value: 1,
- source: '',
- },
- ]);
+ // Check box to use curated goals.
+ const checkbox = await screen.findByRole('checkbox', { name: /use ohs standard goal/i });
+ await act(async () => {
+ // use selectEvent to check the checkbox.
+ await userEvent.click(checkbox);
+ await waitFor(async () => {
+ // wait for check box to be checked.
+ expect(checkbox).toBeChecked();
+ });
+ });
- let selector = screen.queryByLabelText(/Select recipient's goal*/i);
- expect(selector).toBeVisible();
+ selector = await screen.findByLabelText(/Select recipient's goal*/i);
- const checkbox = await screen.findByLabelText(/use ohs standard goal/i);
- act(() => {
- userEvent.click(checkbox);
- });
+ await act(async () => {
+ await selectEvent.select(selector, ['Monitoring Template Goal']);
+ });
- selector = screen.queryByLabelText(/Select recipient's goal*/i);
+ // Select first template goal.
- fireEvent.focus(selector);
+ fireEvent.focus(selector);
+ await act(async () => {
+ // arrow down to the first option and select it.
fireEvent.keyDown(selector, {
key: 'ArrowDown',
keyCode: 40,
code: 40,
});
-
- const option = await screen.findByText('Goal Template 1');
- expect(option).toBeVisible();
- });
- });
-
- describe('curated goals', () => {
- it('with no prompts', async () => {
- fetchMock.get('/api/goal-templates/1/prompts?goalIds=1', [
- {
- type: 'multiselect',
- title: 'prompt-1',
- options: [
- 'Option 1',
- 'Option 2',
- ],
- prompt: 'WHYYYYYYYY?',
- },
- ]);
- fetchMock.get('/api/goal-templates/1/source?grantIds=1', {
- source: 'source',
- });
-
- const availableGoals = [{
- label: 'Goal 1',
- value: 1,
- goalIds: [1],
- isCurated: true,
- goalTemplateId: 1,
- }];
-
- act(() => {
- renderGoalPicker(availableGoals, null);
- });
-
- const selector = await screen.findByLabelText(/Select recipient's goal*/i);
- const [availableGoal] = availableGoals;
-
- await act(async () => {
- await selectEvent.select(selector, [availableGoal.label]);
- });
-
- const input = document.querySelector('[name="goalForEditing"]');
- expect(input.value).toBe(availableGoal.value.toString());
});
- it('with prompts', async () => {
- fetchMock.get('/api/goal-templates/1/source?grantIds=1', {
- source: 'source',
- });
-
- fetchMock.get('/api/goal-templates/1/prompts?goalIds=1', [
- {
- type: 'multiselect',
- title: 'prompt-1',
- options: [
- 'Option 1',
- 'Option 2',
- ],
- prompt: 'WHYYYYYYYY?',
- },
- ]);
- const availableGoals = [{
- label: 'Goal 1',
- value: 1,
- goalIds: [1],
- isCurated: true,
- goalTemplateId: 1,
- }];
-
- act(() => {
- renderGoalPicker(availableGoals, null);
- });
-
- const selector = await screen.findByLabelText(/Select recipient's goal*/i);
- const [availableGoal] = availableGoals;
-
- await act(async () => {
- await selectEvent.select(selector, [availableGoal.label]);
- });
-
- const input = document.querySelector('[name="goalForEditing"]');
- expect(input.value).toBe(availableGoal.value.toString());
- });
- });
- describe('monitoring goals', () => {
- it('correctly retrieves citations for monitoring goals', async () => {
- fetchMock.get('/api/goal-templates/1/prompts?goalIds=1', []);
- fetchMock.get('/api/goal-templates/1/source?grantIds=1', {
- source: 'Federal monitoring issues, including CLASS and RANs',
- });
-
- fetchMock.get('/api/citations/region/1?grantIds=1&reportStartDate=2024-12-03', [
- {
- citation: 'test citation 1',
- grants: [
- {
- acro: 'DEF',
- citation: 'test citation 1',
- findingId: 1,
- findingSource: 'source',
- findingType: 'Deficiency',
- grantId: 1,
- grantNumber: '123',
- monitoringFindingStatusName: 'Active',
- reportDeliveryDate: '2024-12-03',
- reviewName: 'review name',
- severity: 1,
- },
- ],
- standardId: 1,
- },
- ]);
-
- const availableGoals = [{
- label: 'Monitoring Goal',
- value: 1,
- goalIds: [1],
- isCurated: true,
- goalTemplateId: 1,
- source: 'Federal monitoring issues, including CLASS and RANs',
- standard: 'Monitoring',
- goals: [
- {
- grantId: 1,
- },
- ],
- }];
- act(() => {
- renderGoalPicker(availableGoals, null);
+ await act(async () => {
+ await waitFor(async () => {
+ const option = await screen.findByText('Monitoring Template Goal');
+ expect(option).toBeVisible();
});
-
- const selector = await screen.findByLabelText(/Select recipient's goal*/i);
- const [availableGoal] = availableGoals;
-
- await act(async () => {
- await selectEvent.select(selector, [availableGoal.label]);
- });
-
- const input = document.querySelector('[name="goalForEditing"]');
- expect(input.value).toBe(availableGoal.value.toString());
-
- // Select 'Create a new objective' from the dropdown.
- const objectiveSelector = await screen.findByLabelText(/Select TTA objective/i);
- await selectEvent.select(objectiveSelector, 'Create a new objective');
-
- // Open the citations dropdown.
- const citationSelector = await screen.findByLabelText(/citation/i);
- await selectEvent.select(citationSelector, /test citation 1/i);
-
- // Check that the citation is displayed.
- const citation = await screen.findByText(/test citation 1/i);
- expect(citation).toBeVisible();
});
+ expect(await screen.findByText(/this grant does not have the standard monitoring goal/i)).toBeVisible();
+ expect(await screen.findByText(/grant 1 name/i)).toBeVisible();
+ expect(await screen.findByText(/to avoid errors when submitting the report, you can either/i)).toBeVisible();
});
});
diff --git a/src/services/citations.ts b/src/services/citations.ts
index 53f1cba3b0..46db9a99d3 100644
--- a/src/services/citations.ts
+++ b/src/services/citations.ts
@@ -16,8 +16,8 @@ export async function textByCitation(
}
// TODO: Update this to the day we deploy to PROD.
-const cutOffStartDate = new Date().toISOString().split('T')[0];
-// const cutOffStartDate = '2021-01-01';
+// const cutOffStartDate = new Date().toISOString().split('T')[0];
+const cutOffStartDate = '2021-01-01';
/*
The purpose of this function is to get citations by grant id.
We then need to format the response for how it needs to be
From 93d297c55ee5e0a1a74accab0c9941926afc1746 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Thu, 12 Dec 2024 14:46:08 -0500
Subject: [PATCH 118/198] put back date
---
src/services/citations.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/services/citations.ts b/src/services/citations.ts
index 46db9a99d3..53f1cba3b0 100644
--- a/src/services/citations.ts
+++ b/src/services/citations.ts
@@ -16,8 +16,8 @@ export async function textByCitation(
}
// TODO: Update this to the day we deploy to PROD.
-// const cutOffStartDate = new Date().toISOString().split('T')[0];
-const cutOffStartDate = '2021-01-01';
+const cutOffStartDate = new Date().toISOString().split('T')[0];
+// const cutOffStartDate = '2021-01-01';
/*
The purpose of this function is to get citations by grant id.
We then need to format the response for how it needs to be
From 48cdcdd2ada6472653a403088cc064c698680747 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Thu, 12 Dec 2024 16:09:45 -0500
Subject: [PATCH 119/198] put back removed tests
---
.../Pages/components/__tests__/GoalPicker.js | 533 ++++++++++++++----
1 file changed, 426 insertions(+), 107 deletions(-)
diff --git a/frontend/src/pages/ActivityReport/Pages/components/__tests__/GoalPicker.js b/frontend/src/pages/ActivityReport/Pages/components/__tests__/GoalPicker.js
index 48452c76a0..aff6b6b8d5 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/__tests__/GoalPicker.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/__tests__/GoalPicker.js
@@ -20,8 +20,6 @@ import GoalPicker from '../GoalPicker';
import UserContext from '../../../../../UserContext';
import { mockRSSData } from '../../../../../testHelpers';
-const history = createMemoryHistory();
-
const defaultSelectedGoals = [
{
label: '123',
@@ -40,7 +38,7 @@ const GP = ({
selectedGoals,
goalForEditing,
goalTemplates,
- additionalGrantRecipients = [],
+ additionalRecipients = [],
}) => {
const hookForm = useForm({
mode: 'onChange',
@@ -53,10 +51,10 @@ const GP = ({
role: 'central office',
},
collaborators: [],
- activityRecipients: [...additionalGrantRecipients, { activityRecipientId: 1, name: 'Grant 1 Name' }],
+ activityRecipients: [...additionalRecipients, { activityRecipientId: 1, name: 'Grant 1 Name' }],
},
});
-
+ const history = createMemoryHistory();
return (
-
-
+
+
-
-
+
+
);
@@ -90,7 +88,6 @@ const renderGoalPicker = (
selectedGoals = defaultSelectedGoals,
goalForEditing = defaultGoalForEditing,
goalTemplates = [],
- additionalGrantRecipients = [],
) => {
render(
,
);
};
@@ -112,84 +108,78 @@ describe('GoalPicker', () => {
afterEach(() => fetchMock.restore());
- it('correctly displays the monitoring warning if non monitoring recipients are selected', async () => {
- fetchMock.get('/api/goal-templates/1/prompts?goalIds=1&goalIds=2', []);
- fetchMock.get('/api/goal-templates/1/source?grantIds=2&grantIds=1', {
- source: 'Federal monitoring issues, including CLASS and RANs',
- });
+ it('you can select a goal', async () => {
+ const availableGoals = [{
+ label: 'Goal 1',
+ value: 1,
+ goalIds: [1],
+ name: 'Goal 1',
+ }];
- // api/citations/region/1?grantIds=1&reportStartDate=2024-12-03
- fetchMock.get('/api/citations/region/1?grantIds=1&reportStartDate=2024-12-03', [
- {
- citation: 'Not your citation',
- grants: [
- {
- acro: 'DEF',
- citation: 'test citation 1',
- findingId: 1,
- findingSource: 'source',
- findingType: 'Not your citation type',
- grantId: 2,
- grantNumber: '123',
- monitoringFindingStatusName: 'Active',
- reportDeliveryDate: '2024-12-03',
- reviewName: 'review name',
- severity: 1,
- },
- ],
- standardId: 1,
- },
- ]);
+ renderGoalPicker(availableGoals);
- fetchMock.get('/api/citations/region/1?grantIds=1&grantIds=2&reportStartDate=2024-12-03', [
- {
- citation: 'Not your citation',
- grants: [
- {
- acro: 'DEF',
- citation: 'test citation 1',
- findingId: 1,
- findingSource: 'source',
- findingType: 'Not your citation type',
- grantId: 2,
- grantNumber: '123',
- monitoringFindingStatusName: 'Active',
- reportDeliveryDate: '2024-12-03',
- reviewName: 'review name',
- severity: 1,
- },
- ],
- standardId: 1,
- },
- ]);
+ const selector = await screen.findByLabelText(/Select recipient's goal*/i);
+ const [availableGoal] = availableGoals;
+
+ await selectEvent.select(selector, [availableGoal.label]);
+ const input = document.querySelector('[name="goalForEditing"');
+ expect(input.value).toBe(availableGoal.value.toString());
+ });
+
+ it('you can select a goal that has objectives, keeping the objectives', async () => {
const availableGoals = [{
label: 'Goal 1',
value: 1,
- goalIds: [1, 2],
+ goalIds: [1],
name: 'Goal 1',
- objectives: [],
}];
- const availableTemplates = [{
- label: 'Monitoring Template Goal',
+
+ const goalForEditing = {
+ objectives: [{
+ topics: [],
+ id: 1,
+ title: 'Objective 1',
+ resources: [],
+ ttaProvided: '',
+ objectiveCreatedHere: true,
+ }],
+ goalIds: [],
+ };
+
+ renderGoalPicker(
+ availableGoals,
+ defaultSelectedGoals,
+ goalForEditing,
+ );
+
+ const selector = await screen.findByLabelText(/Select recipient's goal*/i);
+ const [availableGoal] = availableGoals;
+
+ await selectEvent.select(selector, [availableGoal.label]);
+
+ expect(await screen.findByText('You have selected a different goal.')).toBeVisible();
+
+ const button = await screen.findByRole('button', { name: /keep objective/i });
+ userEvent.click(button);
+
+ const input = document.querySelector('[name="goalForEditing"');
+ expect(input.value).toBe(availableGoal.value.toString());
+
+ const objective = await screen.findByText('Objective 1', { selector: 'textarea' });
+ expect(objective).toBeVisible();
+ expect(objective).toHaveAttribute('name', 'goalForEditing.objectives[0].title');
+ });
+
+ it('you can select a goal that has objectives, losing the objectives', async () => {
+ const availableGoals = [{
+ label: 'Goal 1',
value: 1,
- goalIds: [1, 2],
- isCurated: true,
- goalTemplateId: 1,
- source: 'Federal monitoring issues, including CLASS and RANs',
- standard: 'Monitoring',
- objectives: [],
- goals: [
- {
- grantId: 1,
- },
- {
- grantId: 2,
- },
- ],
+ goalIds: [1],
+ name: 'Goal 1',
}];
+
const goalForEditing = {
- standard: 'Monitoring',
objectives: [{
topics: [],
id: 1,
@@ -200,49 +190,378 @@ describe('GoalPicker', () => {
}],
goalIds: [],
};
- act(() => {
- renderGoalPicker(availableGoals, null, goalForEditing, availableTemplates, [{ activityRecipientId: 2, name: 'Grant 2 Name' }]);
- });
- let selector = screen.queryByLabelText(/Select recipient's goal*/i);
+
+ renderGoalPicker(
+ availableGoals,
+ defaultSelectedGoals,
+ goalForEditing,
+ );
+
+ const selector = await screen.findByLabelText(/Select recipient's goal*/i);
+ const [availableGoal] = availableGoals;
+
+ await selectEvent.select(selector, [availableGoal.label]);
+
+ expect(await screen.findByText('You have selected a different goal.')).toBeVisible();
+
+ const button = await screen.findByRole('button', { name: /remove objective/i });
+ userEvent.click(button);
+
+ const input = document.querySelector('[name="goalForEditing"');
+ expect(input.value).toBe(availableGoal.value.toString());
+
+ const objective = document.querySelector('[name="goalForEditing.objectives[0].title"]');
+ expect(objective).toBeNull();
+ });
+
+ it('you can select a goal with no selected goals', async () => {
+ const availableGoals = [{
+ label: 'Goal 1',
+ value: 1,
+ goalIds: [1],
+ }];
+
+ renderGoalPicker(availableGoals, null);
+
+ const selector = await screen.findByLabelText(/Select recipient's goal*/i);
+ const [availableGoal] = availableGoals;
+
+ await selectEvent.select(selector, [availableGoal.label]);
+
+ const input = document.querySelector('[name="goalForEditing"');
+ expect(input.value).toBe(availableGoal.value.toString());
+ });
+
+ it('properly renders when there is no goal for editing selected', async () => {
+ renderGoalPicker([], null);
+ const selector = await screen.findByLabelText(/Select recipient's goal*/i);
+
expect(selector).toBeVisible();
+ });
- // Check box to use curated goals.
- const checkbox = await screen.findByRole('checkbox', { name: /use ohs standard goal/i });
- await act(async () => {
- // use selectEvent to check the checkbox.
- await userEvent.click(checkbox);
- await waitFor(async () => {
- // wait for check box to be checked.
- expect(checkbox).toBeChecked();
- });
- });
+ describe('with checkbox', () => {
+ it('you can toggle the list of curated goals', async () => {
+ fetchMock.get('/api/goal-templates/1/prompts?goalIds=1', []);
+ fetchMock.get('/api/goal-templates/1/prompts', []);
+ const availableGoals = [];
- selector = await screen.findByLabelText(/Select recipient's goal*/i);
+ renderGoalPicker(availableGoals, null, null, [
+ {
+ id: 1,
+ goalTemplateId: 1,
+ name: 'Goal Template 1',
+ label: 'Goal Template 1',
+ goals: [],
+ isCurated: true,
+ objectives: [],
+ value: 1,
+ source: '',
+ },
+ ]);
- await act(async () => {
- await selectEvent.select(selector, ['Monitoring Template Goal']);
- });
+ let selector = screen.queryByLabelText(/Select recipient's goal*/i);
+ expect(selector).toBeVisible();
+
+ const checkbox = await screen.findByLabelText(/use ohs standard goal/i);
+ act(() => {
+ userEvent.click(checkbox);
+ });
- // Select first template goal.
+ selector = screen.queryByLabelText(/Select recipient's goal*/i);
- fireEvent.focus(selector);
- await act(async () => {
- // arrow down to the first option and select it.
+ fireEvent.focus(selector);
fireEvent.keyDown(selector, {
key: 'ArrowDown',
keyCode: 40,
code: 40,
});
+
+ const option = await screen.findByText('Goal Template 1');
+ expect(option).toBeVisible();
+ });
+ });
+
+ describe('curated goals', () => {
+ it('with no prompts', async () => {
+ fetchMock.get('/api/goal-templates/1/prompts?goalIds=1', [
+ {
+ type: 'multiselect',
+ title: 'prompt-1',
+ options: [
+ 'Option 1',
+ 'Option 2',
+ ],
+ prompt: 'WHYYYYYYYY?',
+ },
+ ]);
+ fetchMock.get('/api/goal-templates/1/source?grantIds=1', {
+ source: 'source',
+ });
+
+ const availableGoals = [{
+ label: 'Goal 1',
+ value: 1,
+ goalIds: [1],
+ isCurated: true,
+ goalTemplateId: 1,
+ }];
+
+ act(() => {
+ renderGoalPicker(availableGoals, null);
+ });
+
+ const selector = await screen.findByLabelText(/Select recipient's goal*/i);
+ const [availableGoal] = availableGoals;
+
+ await act(async () => {
+ await selectEvent.select(selector, [availableGoal.label]);
+ });
+
+ const input = document.querySelector('[name="goalForEditing"]');
+ expect(input.value).toBe(availableGoal.value.toString());
+ });
+ it('with prompts', async () => {
+ fetchMock.get('/api/goal-templates/1/source?grantIds=1', {
+ source: 'source',
+ });
+
+ fetchMock.get('/api/goal-templates/1/prompts?goalIds=1', [
+ {
+ type: 'multiselect',
+ title: 'prompt-1',
+ options: [
+ 'Option 1',
+ 'Option 2',
+ ],
+ prompt: 'WHYYYYYYYY?',
+ },
+ ]);
+ const availableGoals = [{
+ label: 'Goal 1',
+ value: 1,
+ goalIds: [1],
+ isCurated: true,
+ goalTemplateId: 1,
+ }];
+
+ act(() => {
+ renderGoalPicker(availableGoals, null);
+ });
+
+ const selector = await screen.findByLabelText(/Select recipient's goal*/i);
+ const [availableGoal] = availableGoals;
+
+ await act(async () => {
+ await selectEvent.select(selector, [availableGoal.label]);
+ });
+
+ const input = document.querySelector('[name="goalForEditing"]');
+ expect(input.value).toBe(availableGoal.value.toString());
+ });
+ });
+ describe('monitoring goals', () => {
+ it('correctly retrieves citations for monitoring goals', async () => {
+ fetchMock.get('/api/goal-templates/1/prompts?goalIds=1', []);
+ fetchMock.get('/api/goal-templates/1/source?grantIds=1', {
+ source: 'Federal monitoring issues, including CLASS and RANs',
+ });
+
+ fetchMock.get('/api/citations/region/1?grantIds=1&reportStartDate=2024-12-03', [
+ {
+ citation: 'test citation 1',
+ grants: [
+ {
+ acro: 'DEF',
+ citation: 'test citation 1',
+ findingId: 1,
+ findingSource: 'source',
+ findingType: 'Deficiency',
+ grantId: 1,
+ grantNumber: '123',
+ monitoringFindingStatusName: 'Active',
+ reportDeliveryDate: '2024-12-03',
+ reviewName: 'review name',
+ severity: 1,
+ },
+ ],
+ standardId: 1,
+ },
+ ]);
+
+ const availableGoals = [{
+ label: 'Monitoring Goal',
+ value: 1,
+ goalIds: [1],
+ isCurated: true,
+ goalTemplateId: 1,
+ source: 'Federal monitoring issues, including CLASS and RANs',
+ standard: 'Monitoring',
+ goals: [
+ {
+ grantId: 1,
+ },
+ ],
+ }];
+
+ act(() => {
+ renderGoalPicker(availableGoals, null);
+ });
+
+ const selector = await screen.findByLabelText(/Select recipient's goal*/i);
+ const [availableGoal] = availableGoals;
+
+ await act(async () => {
+ await selectEvent.select(selector, [availableGoal.label]);
+ });
+
+ const input = document.querySelector('[name="goalForEditing"]');
+ expect(input.value).toBe(availableGoal.value.toString());
+
+ // Select 'Create a new objective' from the dropdown.
+ const objectiveSelector = await screen.findByLabelText(/Select TTA objective/i);
+ await selectEvent.select(objectiveSelector, 'Create a new objective');
+
+ // Open the citations dropdown.
+ const citationSelector = await screen.findByLabelText(/citation/i);
+ await selectEvent.select(citationSelector, /test citation 1/i);
+
+ // Check that the citation is displayed.
+ const citation = await screen.findByText(/test citation 1/i);
+ expect(citation).toBeVisible();
});
- await act(async () => {
- await waitFor(async () => {
- const option = await screen.findByText('Monitoring Template Goal');
- expect(option).toBeVisible();
+ it('correctly displays the monitoring warning if non monitoring recipients are selected', async () => {
+ fetchMock.get('/api/goal-templates/1/prompts?goalIds=1&goalIds=2', []);
+ fetchMock.get('/api/goal-templates/1/source?grantIds=2&grantIds=1', {
+ source: 'Federal monitoring issues, including CLASS and RANs',
+ });
+
+ // api/citations/region/1?grantIds=1&reportStartDate=2024-12-03
+ fetchMock.get('/api/citations/region/1?grantIds=1&reportStartDate=2024-12-03', [
+ {
+ citation: 'Not your citation',
+ grants: [
+ {
+ acro: 'DEF',
+ citation: 'test citation 1',
+ findingId: 1,
+ findingSource: 'source',
+ findingType: 'Not your citation type',
+ grantId: 2,
+ grantNumber: '123',
+ monitoringFindingStatusName: 'Active',
+ reportDeliveryDate: '2024-12-03',
+ reviewName: 'review name',
+ severity: 1,
+ },
+ ],
+ standardId: 1,
+ },
+ ]);
+
+ fetchMock.get('/api/citations/region/1?grantIds=1&grantIds=2&reportStartDate=2024-12-03', [
+ {
+ citation: 'Not your citation',
+ grants: [
+ {
+ acro: 'DEF',
+ citation: 'test citation 1',
+ findingId: 1,
+ findingSource: 'source',
+ findingType: 'Not your citation type',
+ grantId: 2,
+ grantNumber: '123',
+ monitoringFindingStatusName: 'Active',
+ reportDeliveryDate: '2024-12-03',
+ reviewName: 'review name',
+ severity: 1,
+ },
+ ],
+ standardId: 1,
+ },
+ ]);
+
+ const availableGoals = [{
+ label: 'Goal 1',
+ value: 1,
+ goalIds: [1, 2],
+ name: 'Goal 1',
+ objectives: [],
+ }];
+ const availableTemplates = [{
+ label: 'Monitoring Template Goal',
+ value: 1,
+ goalIds: [1, 2],
+ isCurated: true,
+ goalTemplateId: 1,
+ source: 'Federal monitoring issues, including CLASS and RANs',
+ standard: 'Monitoring',
+ objectives: [],
+ goals: [
+ {
+ grantId: 1,
+ },
+ {
+ grantId: 2,
+ },
+ ],
+ }];
+ const goalForEditing = {
+ standard: 'Monitoring',
+ objectives: [{
+ topics: [],
+ id: 1,
+ title: 'Objective 1',
+ resources: [],
+ ttaProvided: '',
+ objectiveCreatedHere: true,
+ }],
+ goalIds: [],
+ };
+ act(() => {
+ renderGoalPicker(availableGoals, null, goalForEditing, availableTemplates, [{ activityRecipientId: 2, name: 'Grant 2 Name' }]);
+ });
+ let selector = screen.queryByLabelText(/Select recipient's goal*/i);
+ expect(selector).toBeVisible();
+
+ // Check box to use curated goals.
+ const checkbox = await screen.findByRole('checkbox', { name: /use ohs standard goal/i });
+ await act(async () => {
+ // use selectEvent to check the checkbox.
+ await userEvent.click(checkbox);
+ await waitFor(async () => {
+ // wait for check box to be checked.
+ expect(checkbox).toBeChecked();
+ });
+ });
+
+ selector = await screen.findByLabelText(/Select recipient's goal*/i);
+
+ await act(async () => {
+ await selectEvent.select(selector, ['Monitoring Template Goal']);
+ });
+
+ // Select first template goal.
+
+ fireEvent.focus(selector);
+ await act(async () => {
+ // arrow down to the first option and select it.
+ fireEvent.keyDown(selector, {
+ key: 'ArrowDown',
+ keyCode: 40,
+ code: 40,
+ });
+ });
+
+ await act(async () => {
+ await waitFor(async () => {
+ const option = await screen.findByText('Monitoring Template Goal');
+ expect(option).toBeVisible();
+ });
});
+ expect(await screen.findByText(/this grant does not have the standard monitoring goal/i)).toBeVisible();
+ expect(await screen.findByText(/grant 1 name/i)).toBeVisible();
+ expect(await screen.findByText(/to avoid errors when submitting the report, you can either/i)).toBeVisible();
});
- expect(await screen.findByText(/this grant does not have the standard monitoring goal/i)).toBeVisible();
- expect(await screen.findByText(/grant 1 name/i)).toBeVisible();
- expect(await screen.findByText(/to avoid errors when submitting the report, you can either/i)).toBeVisible();
});
});
From ec8da054b93b167721153ae17581d14a26bea9f4 Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Fri, 13 Dec 2024 12:51:57 -0500
Subject: [PATCH 120/198] Initial commit
---
.../src/components/CitationDrawerContent.js | 37 +++++++++++++++++++
.../GoalForm/GenericSelectWithDrawer.js | 12 ++++--
.../__tests__/GenericSelectWithDrawer.js | 2 +
frontend/src/fetchers/citations.js | 23 +++++++++++-
.../Pages/components/Objective.js | 20 +++++++++-
.../ActivityReport/Pages/goalsObjectives.js | 3 +-
.../pages/ActivityReport/__tests__/index.js | 7 +++-
.../src/pages/ActivityReport/testHelpers.js | 4 +-
8 files changed, 96 insertions(+), 12 deletions(-)
create mode 100644 frontend/src/components/CitationDrawerContent.js
diff --git a/frontend/src/components/CitationDrawerContent.js b/frontend/src/components/CitationDrawerContent.js
new file mode 100644
index 0000000000..19cea1de7b
--- /dev/null
+++ b/frontend/src/components/CitationDrawerContent.js
@@ -0,0 +1,37 @@
+import React, { useEffect, useState } from 'react';
+import PropTypes from 'prop-types';
+import { uniqueId } from 'lodash';
+import { fetchCitationTextByName } from '../fetchers/citations';
+
+export default function CitationDrawerContent({ citations }) {
+ const [content, setContent] = useState([]); // { text: string, citation: string }[]
+
+ useEffect(() => {
+ async function fetchCitations() {
+ try {
+ const response = await fetchCitationTextByName(citations);
+ setContent(response);
+ } catch (err) {
+ // eslint-disable-next-line no-console
+ console.error(err);
+ }
+ }
+
+ fetchCitations();
+ }, [citations]);
+
+ return (
+
+ {content.map((standard) => (
+
+
{standard.citation}
+
{standard.text}
+
+ ))}
+
+ );
+}
+
+CitationDrawerContent.propTypes = {
+ citations: PropTypes.arrayOf(PropTypes.string).isRequired,
+};
diff --git a/frontend/src/components/GoalForm/GenericSelectWithDrawer.js b/frontend/src/components/GoalForm/GenericSelectWithDrawer.js
index b7a4aa0f37..93dfb082a4 100644
--- a/frontend/src/components/GoalForm/GenericSelectWithDrawer.js
+++ b/frontend/src/components/GoalForm/GenericSelectWithDrawer.js
@@ -7,7 +7,6 @@ import Select from 'react-select';
import selectOptionsReset from '../selectOptionsReset';
import Drawer from '../Drawer';
import Req from '../Req';
-import ContentFromFeedByTag from '../ContentFromFeedByTag';
import DrawerTriggerButton from '../DrawerTriggerButton';
export default function GenericSelectWithDrawer({
@@ -19,12 +18,15 @@ export default function GenericSelectWithDrawer({
onChangeValues,
inputName,
isLoading,
+
+ // drawer props
+ drawerContent,
+ drawerTitle,
}) {
const drawerTriggerRef = useRef(null);
if (options && options.length > 0) {
options.sort((a, b) => (a.name || '').localeCompare(b.name || ''));
}
- const nameToLower = name ? name.toLowerCase() : '';
return (
<>
@@ -32,9 +34,9 @@ export default function GenericSelectWithDrawer({
triggerRef={drawerTriggerRef}
stickyHeader
stickyFooter
- title={`${name} guidance`}
+ title={drawerTitle}
>
-
+ {drawerContent}
@@ -94,6 +96,8 @@ GenericSelectWithDrawer.propTypes = {
onChangeValues: PropTypes.func.isRequired,
inputName: PropTypes.string.isRequired,
isLoading: PropTypes.bool,
+ drawerContent: PropTypes.node.isRequired,
+ drawerTitle: PropTypes.string.isRequired,
};
GenericSelectWithDrawer.defaultProps = {
diff --git a/frontend/src/components/GoalForm/__tests__/GenericSelectWithDrawer.js b/frontend/src/components/GoalForm/__tests__/GenericSelectWithDrawer.js
index d80f999dfb..953b4251d6 100644
--- a/frontend/src/components/GoalForm/__tests__/GenericSelectWithDrawer.js
+++ b/frontend/src/components/GoalForm/__tests__/GenericSelectWithDrawer.js
@@ -44,6 +44,8 @@ describe('ObjectiveTopics', () => {
isOnReport={isOnReport}
goalStatus={goalStatus}
userCanEdit={userCanEdit}
+ drawerContent={
Drawer Content
}
+ drawerTitle="Drawer Title"
/>
));
diff --git a/frontend/src/fetchers/citations.js b/frontend/src/fetchers/citations.js
index 51a663722b..4d4f78648c 100644
--- a/frontend/src/fetchers/citations.js
+++ b/frontend/src/fetchers/citations.js
@@ -1,4 +1,3 @@
-/* eslint-disable import/prefer-default-export */
import join from 'url-join';
import {
get,
@@ -17,3 +16,25 @@ export async function fetchCitationsByGrant(region, grantIds, reportStartDate) {
const citations = await get(url);
return citations.json();
}
+
+/**
+ * Fetch citation text by citation name.
+ * @param {String[]} citationIds
+ * @returns {Promise<{ text: String; citation: String; }[]>}
+ */
+export async function fetchCitationTextByName(citationIds) {
+ const params = new URLSearchParams();
+ citationIds.forEach((name) => {
+ params.append('citationIds', encodeURIComponent(name));
+ });
+
+ const url = join(
+ '/',
+ 'api',
+ 'citations',
+ 'text',
+ `?${params.toString()}`,
+ );
+ const citations = await get(url);
+ return citations.json();
+}
diff --git a/frontend/src/pages/ActivityReport/Pages/components/Objective.js b/frontend/src/pages/ActivityReport/Pages/components/Objective.js
index 135db27e5f..b195d8c521 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/Objective.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/Objective.js
@@ -1,5 +1,8 @@
import React, {
- useState, useContext, useRef,
+ useState,
+ useContext,
+ useRef,
+ useMemo,
} from 'react';
import PropTypes from 'prop-types';
import { v4 as uuidv4 } from 'uuid';
@@ -28,6 +31,8 @@ import './Objective.scss';
import ObjectiveSuspendModal from '../../../../components/ObjectiveSuspendModal';
import IpdCourseSelect from '../../../../components/ObjectiveCourseSelect';
import FormFieldThatIsSometimesReadOnly from '../../../../components/GoalForm/FormFieldThatIsSometimesReadOnly';
+import ContentFromFeedByTag from '../../../../components/ContentFromFeedByTag';
+import CitationDrawerContent from '../../../../components/CitationDrawerContent';
export default function Objective({
objective,
@@ -46,6 +51,9 @@ export default function Objective({
}) {
const modalRef = useRef();
+ const citationNames = useMemo(() => rawCitations.map((rawCitation) => rawCitation.citation),
+ [rawCitations]);
+
// the below is a concession to the fact that the objective may
// exist pre-migration to the new UI, and might not have complete data
const initialObjective = (() => ({
@@ -417,9 +425,15 @@ export default function Objective({
values={objectiveCitations}
onChangeValues={selectedCitationsChanged}
inputName={objectiveCitationsInputName}
+ drawerTitle="Citation guidance"
+ drawerContent={(
+
+ )}
/>
)
- }
+ }
, [])}
/>
{
it('you can add a goal and objective and add a file after saving', async () => {
const data = formData();
fetchMock.get('/api/topic', [{ id: 64, name: 'Communication' }]);
+ fetchMock.get('/api/courses', []);
fetchMock.get('/api/activity-reports/goals?grantIds=12539', []);
fetchMock.get('/api/goal-templates?grantIds=12539&reportStartDate=2012-05-20', []);
fetchMock.put('/api/activity-reports/1/goals/edit?goalIds=37504', {});
+ fetchMock.get('//api/feeds/item?tag=ttahub-tta-support-type', mockRSSData());
fetchMock.get('/api/activity-reports/1', {
...data,
+ startDate: moment().format('YYYY-MM-DD'),
activityRecipientType: 'recipient',
activityRecipients: [
{
@@ -1028,7 +1031,7 @@ describe('ActivityReport', () => {
fetchMock.put('/api/activity-reports/1', {
id: 23786,
userId: 355,
- startDate: moment().format('MM/DD/YYYY'),
+ startDate: moment().format('YYYY-MM-DD'),
endDate: null,
lastUpdatedById: 355,
ECLKCResourcesUsed: [],
diff --git a/frontend/src/pages/ActivityReport/testHelpers.js b/frontend/src/pages/ActivityReport/testHelpers.js
index 13c8b2a889..6dc0034602 100644
--- a/frontend/src/pages/ActivityReport/testHelpers.js
+++ b/frontend/src/pages/ActivityReport/testHelpers.js
@@ -35,7 +35,7 @@ export const formData = () => ({
3: 'in-progress',
4: 'in-progress',
},
- endDate: moment().format('MM/DD/YYYY'),
+ endDate: moment().format('YYYY-MM-DD'),
activityRecipients: ['Recipient Name 1'],
numberOfParticipants: '1',
reason: ['reason 1'],
@@ -46,7 +46,7 @@ export const formData = () => ({
calculatedStatus: REPORT_STATUSES.DRAFT,
submissionStatus: REPORT_STATUSES.DRAFT,
resourcesUsed: 'eclkcurl',
- startDate: moment().format('MM/DD/YYYY'),
+ startDate: moment().format('YYYY-MM-DD'),
targetPopulations: ['target 1'],
author: { name: 'test', roles: { fullName: 'Reporter' } },
topics: 'first',
From 0ffc99d323fbb0d06237a75446e66ad871df21ed Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Fri, 13 Dec 2024 13:00:25 -0500
Subject: [PATCH 121/198] Add test for citation content drawer
---
.../__tests__/CitationDrawerContent.js | 52 +++++++++++++++++++
src/services/citations.ts | 4 +-
2 files changed, 54 insertions(+), 2 deletions(-)
create mode 100644 frontend/src/components/__tests__/CitationDrawerContent.js
diff --git a/frontend/src/components/__tests__/CitationDrawerContent.js b/frontend/src/components/__tests__/CitationDrawerContent.js
new file mode 100644
index 0000000000..cabe6bcbc3
--- /dev/null
+++ b/frontend/src/components/__tests__/CitationDrawerContent.js
@@ -0,0 +1,52 @@
+import '@testing-library/jest-dom';
+import React from 'react';
+import {
+ render,
+ screen,
+} from '@testing-library/react';
+import fetchMock from 'fetch-mock';
+import CitationDrawerContent from '../CitationDrawerContent';
+
+describe('CitationDrawerContent', () => {
+ const citationUrl = '/api/citations/text?citationIds=citation1&citationIds=citation2';
+ const mockCitations = [
+ {
+ citation: 'citation1',
+ text: 'text1',
+ },
+ {
+ citation: 'citation2',
+ text: 'text2',
+ },
+ ];
+ afterEach(() => fetchMock.restore());
+
+ const renderTest = () => {
+ render( );
+ };
+
+ it('fetches citations', async () => {
+ fetchMock.get(citationUrl, mockCitations);
+
+ renderTest();
+
+ expect(fetchMock.called(citationUrl)).toBe(true);
+
+ expect(await screen.findByText('citation1')).toBeInTheDocument();
+ expect(await screen.findByText('text1')).toBeInTheDocument();
+ expect(await screen.findByText('citation2')).toBeInTheDocument();
+ expect(await screen.findByText('text2')).toBeInTheDocument();
+ });
+
+ it('handles errors', async () => {
+ fetchMock.get(citationUrl, 500);
+
+ renderTest();
+
+ expect(fetchMock.called(citationUrl)).toBe(true);
+ expect(screen.queryByText('citation1')).not.toBeInTheDocument();
+ expect(screen.queryByText('text1')).not.toBeInTheDocument();
+ expect(screen.queryByText('citation2')).not.toBeInTheDocument();
+ expect(screen.queryByText('text2')).not.toBeInTheDocument();
+ });
+});
diff --git a/src/services/citations.ts b/src/services/citations.ts
index c9d182b44f..13a0a48b28 100644
--- a/src/services/citations.ts
+++ b/src/services/citations.ts
@@ -16,8 +16,8 @@ export async function textByCitation(
}
// TODO: Update this to the day we deploy to PROD.
-const cutOffStartDate = new Date().toISOString().split('T')[0];
-// const cutOffStartDate = '2021-01-01';
+// const cutOffStartDate = new Date().toISOString().split('T')[0];
+const cutOffStartDate = '2021-01-01';
/*
The purpose of this function is to get citations by grant id.
We then need to format the response for how it needs to be
From f558c1ff9230816701f1bda0866609ed4464d33a Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Fri, 13 Dec 2024 13:03:13 -0500
Subject: [PATCH 122/198] Add test for fetcher
---
frontend/src/fetchers/__tests__/citations.js | 12 ++++++++----
1 file changed, 8 insertions(+), 4 deletions(-)
diff --git a/frontend/src/fetchers/__tests__/citations.js b/frontend/src/fetchers/__tests__/citations.js
index 39794190be..2c3c9dc0a6 100644
--- a/frontend/src/fetchers/__tests__/citations.js
+++ b/frontend/src/fetchers/__tests__/citations.js
@@ -1,7 +1,5 @@
-// import join from 'url-join';
import fetchMock from 'fetch-mock';
-
-import { fetchCitationsByGrant } from '../citations';
+import { fetchCitationsByGrant, fetchCitationTextByName } from '../citations';
describe('Citations fetcher', () => {
beforeEach(() => fetchMock.reset());
@@ -9,6 +7,12 @@ describe('Citations fetcher', () => {
it('fetches citations', async () => {
fetchMock.get('/api/citations/region/1?grantIds=1&grantIds=2&reportStartDate=2024-12-03', []);
await fetchCitationsByGrant(1, [1, 2], '2024-12-03');
- expect(fetchMock.called()).toBeTruthy();
+ expect(fetchMock.called('/api/citations/region/1?grantIds=1&grantIds=2&reportStartDate=2024-12-03')).toBeTruthy();
+ });
+
+ it('fetches citation text', async () => {
+ fetchMock.get('/api/citations/text?citationIds=123&citationIds=456', []);
+ await fetchCitationTextByName(['123', '456']);
+ expect(fetchMock.called('/api/citations/text?citationIds=123&citationIds=456')).toBeTruthy();
});
});
From 1652364b0f3eae35bcaef5cd9730bfa0dab6287d Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Fri, 13 Dec 2024 13:21:42 -0500
Subject: [PATCH 123/198] Add citation drawer to RTR
---
.../Monitoring/__tests__/CitationDrawer.js | 40 +++++++++++++++++++
.../pages/Monitoring/__tests__/index.js | 7 ++++
.../Monitoring/components/CitationCard.js | 5 ++-
.../Monitoring/components/CitationDrawer.js | 34 ++++++++++++++++
.../components/FindingWithinReview.js | 3 +-
5 files changed, 86 insertions(+), 3 deletions(-)
create mode 100644 frontend/src/pages/RecipientRecord/pages/Monitoring/__tests__/CitationDrawer.js
create mode 100644 frontend/src/pages/RecipientRecord/pages/Monitoring/components/CitationDrawer.js
diff --git a/frontend/src/pages/RecipientRecord/pages/Monitoring/__tests__/CitationDrawer.js b/frontend/src/pages/RecipientRecord/pages/Monitoring/__tests__/CitationDrawer.js
new file mode 100644
index 0000000000..2b45e07398
--- /dev/null
+++ b/frontend/src/pages/RecipientRecord/pages/Monitoring/__tests__/CitationDrawer.js
@@ -0,0 +1,40 @@
+import '@testing-library/jest-dom';
+import React from 'react';
+import {
+ render,
+ screen,
+} from '@testing-library/react';
+import fetchMock from 'fetch-mock';
+import CitationDrawer from '../components/CitationDrawer';
+
+describe('CitationDrawer', () => {
+ const citationUrl = '/api/citations/text?citationIds=citation1';
+ const mockCitations = [
+ {
+ citation: 'citation1',
+ text: 'text1',
+ },
+ {
+ citation: 'citation2',
+ text: 'text2',
+ },
+ ];
+ afterEach(() => fetchMock.restore());
+
+ const renderTest = () => {
+ render( );
+ };
+
+ it('fetches citations', async () => {
+ fetchMock.get(citationUrl, mockCitations);
+
+ renderTest();
+
+ expect(fetchMock.called(citationUrl)).toBe(true);
+
+ const button = await screen.findByRole('button', { name: 'citation1' });
+
+ expect(button).toBeVisible();
+ expect(await screen.findByText('text1')).not.toBeVisible();
+ });
+});
diff --git a/frontend/src/pages/RecipientRecord/pages/Monitoring/__tests__/index.js b/frontend/src/pages/RecipientRecord/pages/Monitoring/__tests__/index.js
index 3d2cceda40..ec7b87ed79 100644
--- a/frontend/src/pages/RecipientRecord/pages/Monitoring/__tests__/index.js
+++ b/frontend/src/pages/RecipientRecord/pages/Monitoring/__tests__/index.js
@@ -58,6 +58,13 @@ describe('Monitoring', () => {
beforeEach(() => {
fetchMock.get(citationUrl, citationData);
fetchMock.get(reviewUrl, reviewData);
+
+ // citation drawer content fetchers
+ fetchMock.get('/api/citations/text?citationIds=1302.47%28b%29%285%29%28iv%29', []);
+ fetchMock.get('/api/citations/text?citationIds=1392.47%28b%29%285%29%28i%29', []);
+ fetchMock.get('/api/citations/text?citationIds=1302.91%28a%29', []);
+ fetchMock.get('/api/citations/text?citationIds=1302.12%28m%29', []);
+ fetchMock.get('/api/citations/text?citationIds=1302.47%28b%29%285%29%28i%29', []);
});
afterEach(() => {
diff --git a/frontend/src/pages/RecipientRecord/pages/Monitoring/components/CitationCard.js b/frontend/src/pages/RecipientRecord/pages/Monitoring/components/CitationCard.js
index fc598af0e4..f6b5987f98 100644
--- a/frontend/src/pages/RecipientRecord/pages/Monitoring/components/CitationCard.js
+++ b/frontend/src/pages/RecipientRecord/pages/Monitoring/components/CitationCard.js
@@ -7,6 +7,7 @@ import DescriptionList from './DescriptionList';
import ExpanderButton from '../../../../../components/ExpanderButton';
import ReviewWithinCitation from './ReviewWithinCitation';
import './CitationCard.css';
+import CitationDrawer from './CitationDrawer';
export default function CitationCard({ citation, regionId }) {
const [expanded, setExpanded] = useState(false);
@@ -17,8 +18,8 @@ export default function CitationCard({ citation, regionId }) {
className="ttahub-monitoring-citation-card"
>
-
- {citation.citationNumber}
+
+
diff --git a/frontend/src/pages/RecipientRecord/pages/Monitoring/components/CitationDrawer.js b/frontend/src/pages/RecipientRecord/pages/Monitoring/components/CitationDrawer.js
new file mode 100644
index 0000000000..19ecec27b2
--- /dev/null
+++ b/frontend/src/pages/RecipientRecord/pages/Monitoring/components/CitationDrawer.js
@@ -0,0 +1,34 @@
+import React, { useRef } from 'react';
+import PropTypes from 'prop-types';
+import Drawer from '../../../../../components/Drawer';
+import DrawerTriggerButton from '../../../../../components/DrawerTriggerButton';
+import CitationDrawerContent from '../../../../../components/CitationDrawerContent';
+
+export default function CitationDrawer({ citationNumber, bolded }) {
+ const drawerTriggerRef = useRef(null);
+
+ return (
+ <>
+
+ {citationNumber}
+
+
+
+
+ >
+ );
+}
+
+CitationDrawer.propTypes = {
+ citationNumber: PropTypes.string.isRequired,
+ bolded: PropTypes.bool,
+};
+
+CitationDrawer.defaultProps = {
+ bolded: false,
+};
diff --git a/frontend/src/pages/RecipientRecord/pages/Monitoring/components/FindingWithinReview.js b/frontend/src/pages/RecipientRecord/pages/Monitoring/components/FindingWithinReview.js
index 89da7899c2..f8b5a0c65d 100644
--- a/frontend/src/pages/RecipientRecord/pages/Monitoring/components/FindingWithinReview.js
+++ b/frontend/src/pages/RecipientRecord/pages/Monitoring/components/FindingWithinReview.js
@@ -5,6 +5,7 @@ import DescriptionItem from './DescriptionItem';
import DescriptionList from './DescriptionList';
import ReviewObjective from './ReviewObjective';
import NoTtaProvidedAgainst from './NoTtaProvidedAgainst';
+import CitationDrawer from './CitationDrawer';
import './FindingWithinReview.css';
export default function FindingWithinReview({ finding, regionId }) {
@@ -12,7 +13,7 @@ export default function FindingWithinReview({ finding, regionId }) {
- {finding.citation}
+
{finding.status}
From a3a92221a1cd9f9afb84b600d352ba953124b9ca Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Fri, 13 Dec 2024 13:24:40 -0500
Subject: [PATCH 124/198] Revert citation fetch change
---
src/services/citations.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/services/citations.ts b/src/services/citations.ts
index 13a0a48b28..c9d182b44f 100644
--- a/src/services/citations.ts
+++ b/src/services/citations.ts
@@ -16,8 +16,8 @@ export async function textByCitation(
}
// TODO: Update this to the day we deploy to PROD.
-// const cutOffStartDate = new Date().toISOString().split('T')[0];
-const cutOffStartDate = '2021-01-01';
+const cutOffStartDate = new Date().toISOString().split('T')[0];
+// const cutOffStartDate = '2021-01-01';
/*
The purpose of this function is to get citations by grant id.
We then need to format the response for how it needs to be
From b6241724f7e4f743d36bfd1e52332030721acda4 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Fri, 13 Dec 2024 15:02:45 -0500
Subject: [PATCH 125/198] add missing message and prevent submitting on ar
---
.../Pages/Review/Submitter/Draft.js | 29 +++++++++-
.../Pages/Review/Submitter/__tests__/index.js | 54 +++++++++++++++++++
.../Pages/Review/Submitter/index.js | 41 ++++++++++++++
.../pages/ActivityReport/__tests__/index.js | 8 +--
.../src/pages/ActivityReport/testHelpers.js | 4 +-
src/services/citations.ts | 4 +-
6 files changed, 131 insertions(+), 9 deletions(-)
diff --git a/frontend/src/pages/ActivityReport/Pages/Review/Submitter/Draft.js b/frontend/src/pages/ActivityReport/Pages/Review/Submitter/Draft.js
index 498b4af94f..ef46398df7 100644
--- a/frontend/src/pages/ActivityReport/Pages/Review/Submitter/Draft.js
+++ b/frontend/src/pages/ActivityReport/Pages/Review/Submitter/Draft.js
@@ -1,7 +1,7 @@
import React, { useState, useContext } from 'react';
import PropTypes from 'prop-types';
import moment from 'moment-timezone';
-import { Redirect } from 'react-router-dom';
+import { Redirect, Link } from 'react-router-dom';
import { useFormContext } from 'react-hook-form';
import {
Form, Fieldset, Button, Alert, Dropdown,
@@ -28,6 +28,7 @@ const Draft = ({
approverStatusList,
lastSaveTime,
creatorRole,
+ grantsMissingMonitoring,
}) => {
const {
watch,
@@ -76,7 +77,7 @@ const Draft = ({
};
const onSubmit = (e) => {
- if (allGoalsHavePromptResponses && !hasIncompletePages) {
+ if (allGoalsHavePromptResponses && !hasIncompletePages && !grantsMissingMonitoring.length) {
onFormSubmit(e);
updatedJustSubmitted(true);
}
@@ -161,6 +162,29 @@ const Draft = ({
/>
+ {
+ grantsMissingMonitoring.length > 0 && (
+
+ {
+ grantsMissingMonitoring.length > 1
+ ? 'These grants do not have the standard monitoring goal:'
+ : 'This grant does not have the standard monitoring goal:'
+ }
+
+ {grantsMissingMonitoring.map((grant) => {grant} )}
+
+ You can either:
+
+ Add a different goal to the report
+
+ Remove the grant from the
+ {' '}
+ Activity summary
+
+
+
+ )
+ }
{hasIncompletePages && }
{!allGoalsHavePromptResponses && (
{
const formData = {
approvers,
@@ -90,6 +91,42 @@ const renderReview = (
}];
}
+ if (hasGrantsMissingMonitoring) {
+ formData.activityRecipients = [{
+ activityRecipientId: 1,
+ name: 'recipient missing monitoring',
+ },
+ {
+ activityRecipientId: 2,
+ name: 'recipient with monitoring 2',
+ },
+ ];
+
+ formData.goalsAndObjectives = [{
+ isCurated: true,
+ prompts: [{
+ allGoalsHavePromptResponse: false,
+ title: 'FEI Goal',
+ }],
+ standard: 'Monitoring',
+ objectives: [
+ {
+ id: 1,
+ citations: [
+ {
+ id: 1,
+ text: 'citation 1',
+ monitoringReferences: [{
+ grantId: 2,
+ }],
+ },
+ ],
+ },
+ ],
+ goalIds: [1, 2],
+ }];
+ }
+
const history = createMemoryHistory();
const pages = complete ? completePages : incompletePages;
render(
@@ -138,6 +175,23 @@ describe('Submitter review page', () => {
expect(alert).toBeVisible();
});
+ it('shows an error that some grants don\'t have monitoring', async () => {
+ renderReview(
+ REPORT_STATUSES.DRAFT,
+ () => { },
+ false,
+ jest.fn(),
+ jest.fn(),
+ [],
+ defaultUser,
+ null,
+ false,
+ true,
+ );
+ expect(await screen.findByText(/this grant does not have the standard monitoring goal/i)).toBeVisible();
+ expect(await screen.findByText(/recipient missing monitoring/i)).toBeVisible();
+ });
+
it('shows an error if goals are missing prompts', async () => {
fetchMock.get('/api/goals/region/1/incomplete?goalIds=1&goalIds=2', [
{
diff --git a/frontend/src/pages/ActivityReport/Pages/Review/Submitter/index.js b/frontend/src/pages/ActivityReport/Pages/Review/Submitter/index.js
index ec934c7395..193b23470e 100644
--- a/frontend/src/pages/ActivityReport/Pages/Review/Submitter/index.js
+++ b/frontend/src/pages/ActivityReport/Pages/Review/Submitter/index.js
@@ -27,6 +27,8 @@ const Submitter = ({
calculatedStatus,
approvers,
creatorRole,
+ goalsAndObjectives,
+ activityRecipients,
} = formData;
const draft = calculatedStatus === REPORT_STATUSES.DRAFT;
const submitted = calculatedStatus === REPORT_STATUSES.SUBMITTED;
@@ -92,6 +94,37 @@ const Submitter = ({
const filtered = pages.filter((p) => !(p.state === 'Complete' || p.review));
const incompletePages = filtered.map((f) => f.label);
+ const grantsMissingMonitoring = () => {
+ // First determine if we have a monitoring goal selected.
+ const hasMonitoringGoalSelected = (goalsAndObjectives || []).find((goal) => (goal.standard && goal.standard === 'Monitoring'));
+
+ if (hasMonitoringGoalSelected) {
+ // Then get the grantIds from activityRecipients
+ // Then compare the two lists and return the difference
+ const grantsWithCitations = hasMonitoringGoalSelected.objectives.reduce(
+ (acc, objective) => objective.citations.reduce((acc2, citation) => {
+ const { monitoringReferences } = citation;
+ if (monitoringReferences) {
+ const grantIds = monitoringReferences.map((ref) => ref.grantId);
+ return [...acc2, ...grantIds];
+ }
+ return acc2;
+ }, acc), [],
+ );
+
+ const distinctGrants = [...new Set(grantsWithCitations)];
+
+ // From activityRecipients get the name of the grants that matcht the activityRecipientId.
+ const grantNames = activityRecipients.filter(
+ (recipient) => !distinctGrants.includes(recipient.activityRecipientId),
+ ).map(
+ (recipient) => recipient.name,
+ );
+ return grantNames;
+ }
+ return [];
+ };
+
return (
<>
{renderTopAlert()}
@@ -116,6 +149,7 @@ const Submitter = ({
approverStatusList={approverStatusList}
lastSaveTime={lastSaveTime}
creatorRole={creatorRole}
+ grantsMissingMonitoring={grantsMissingMonitoring()}
/>
)}
{submitted
@@ -179,6 +213,13 @@ Submitter.propTypes = {
status: PropTypes.string,
}),
),
+ goalsAndObjectives: PropTypes.arrayOf(PropTypes.shape({
+ standard: PropTypes.string,
+ })),
+ activityRecipients: PropTypes.arrayOf(PropTypes.shape({
+ activityRecipientId: PropTypes.number,
+ name: PropTypes.string,
+ })),
}).isRequired,
lastSaveTime: PropTypes.instanceOf(moment),
diff --git a/frontend/src/pages/ActivityReport/__tests__/index.js b/frontend/src/pages/ActivityReport/__tests__/index.js
index fc60d5dd14..a79859a380 100644
--- a/frontend/src/pages/ActivityReport/__tests__/index.js
+++ b/frontend/src/pages/ActivityReport/__tests__/index.js
@@ -15,7 +15,7 @@ import {
import fetchMock from 'fetch-mock';
import userEvent from '@testing-library/user-event';
import { REPORT_STATUSES, SUPPORT_TYPES } from '@ttahub/common';
-import { mockWindowProperty, withText } from '../../../testHelpers';
+import { mockRSSData, mockWindowProperty, withText } from '../../../testHelpers';
import { unflattenResourcesUsed, findWhatsChanged } from '../formDataHelpers';
import {
history,
@@ -956,9 +956,11 @@ describe('ActivityReport', () => {
it('you can add a goal and objective and add a file after saving', async () => {
const data = formData();
fetchMock.get('/api/topic', [{ id: 64, name: 'Communication' }]);
+ fetchMock.get('/api/courses', []);
fetchMock.get('/api/activity-reports/goals?grantIds=12539', []);
fetchMock.get('/api/goal-templates?grantIds=12539&reportStartDate=2012-05-20', []);
fetchMock.put('/api/activity-reports/1/goals/edit?goalIds=37504', {});
+ fetchMock.get('//api/feeds/item?tag=ttahub-tta-support-type', mockRSSData());
fetchMock.get('/api/activity-reports/1', {
...data,
activityRecipientType: 'recipient',
@@ -1028,7 +1030,7 @@ describe('ActivityReport', () => {
fetchMock.put('/api/activity-reports/1', {
id: 23786,
userId: 355,
- startDate: moment().format('MM/DD/YYYY'),
+ startDate: moment().format('YYYY-MM-DD'),
endDate: null,
lastUpdatedById: 355,
ECLKCResourcesUsed: [],
@@ -1195,7 +1197,7 @@ describe('ActivityReport', () => {
});
fetchMock.get('/api/goals?reportId=1&goalIds=37504', [{
- startDate: moment().format('MM/DD/YYYY'),
+ startDate: moment().format('YYYY-MM-DD'),
status: 'Draft',
value: 37504,
label: 'dfghgh',
diff --git a/frontend/src/pages/ActivityReport/testHelpers.js b/frontend/src/pages/ActivityReport/testHelpers.js
index 13c8b2a889..6dc0034602 100644
--- a/frontend/src/pages/ActivityReport/testHelpers.js
+++ b/frontend/src/pages/ActivityReport/testHelpers.js
@@ -35,7 +35,7 @@ export const formData = () => ({
3: 'in-progress',
4: 'in-progress',
},
- endDate: moment().format('MM/DD/YYYY'),
+ endDate: moment().format('YYYY-MM-DD'),
activityRecipients: ['Recipient Name 1'],
numberOfParticipants: '1',
reason: ['reason 1'],
@@ -46,7 +46,7 @@ export const formData = () => ({
calculatedStatus: REPORT_STATUSES.DRAFT,
submissionStatus: REPORT_STATUSES.DRAFT,
resourcesUsed: 'eclkcurl',
- startDate: moment().format('MM/DD/YYYY'),
+ startDate: moment().format('YYYY-MM-DD'),
targetPopulations: ['target 1'],
author: { name: 'test', roles: { fullName: 'Reporter' } },
topics: 'first',
diff --git a/src/services/citations.ts b/src/services/citations.ts
index 53f1cba3b0..e9e395a9bf 100644
--- a/src/services/citations.ts
+++ b/src/services/citations.ts
@@ -16,8 +16,8 @@ export async function textByCitation(
}
// TODO: Update this to the day we deploy to PROD.
-const cutOffStartDate = new Date().toISOString().split('T')[0];
-// const cutOffStartDate = '2021-01-01';
+//const cutOffStartDate = new Date().toISOString().split('T')[0];
+const cutOffStartDate = '2021-01-01';
/*
The purpose of this function is to get citations by grant id.
We then need to format the response for how it needs to be
From e676ceb2f02a7a5603caae764141edb5f21d1f5a Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Fri, 13 Dec 2024 15:14:27 -0500
Subject: [PATCH 126/198] lint
---
src/services/citations.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/services/citations.ts b/src/services/citations.ts
index e9e395a9bf..53f1cba3b0 100644
--- a/src/services/citations.ts
+++ b/src/services/citations.ts
@@ -16,8 +16,8 @@ export async function textByCitation(
}
// TODO: Update this to the day we deploy to PROD.
-//const cutOffStartDate = new Date().toISOString().split('T')[0];
-const cutOffStartDate = '2021-01-01';
+const cutOffStartDate = new Date().toISOString().split('T')[0];
+// const cutOffStartDate = '2021-01-01';
/*
The purpose of this function is to get citations by grant id.
We then need to format the response for how it needs to be
From 7bc1ee4ea7ff10cd9b3fca7de8ce2c1326b779bf Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Fri, 13 Dec 2024 16:40:12 -0500
Subject: [PATCH 127/198] make sure alerts only show if we have one goal that
is monitoring
---
.../Pages/Review/Submitter/__tests__/index.js | 80 ++++++++---
.../Pages/Review/Submitter/index.js | 4 +-
.../Pages/components/GoalPicker.js | 8 +-
.../Pages/components/__tests__/GoalPicker.js | 134 ++++++++++++++++++
4 files changed, 199 insertions(+), 27 deletions(-)
diff --git a/frontend/src/pages/ActivityReport/Pages/Review/Submitter/__tests__/index.js b/frontend/src/pages/ActivityReport/Pages/Review/Submitter/__tests__/index.js
index f6a79532aa..d6cbc4b51b 100644
--- a/frontend/src/pages/ActivityReport/Pages/Review/Submitter/__tests__/index.js
+++ b/frontend/src/pages/ActivityReport/Pages/Review/Submitter/__tests__/index.js
@@ -70,6 +70,7 @@ const renderReview = (
creatorRole = null,
hasIncompleteGoalPrompts = false,
hasGrantsMissingMonitoring = false,
+ goalsAndObjectives = [],
) => {
const formData = {
approvers,
@@ -102,29 +103,31 @@ const renderReview = (
},
];
- formData.goalsAndObjectives = [{
- isCurated: true,
- prompts: [{
- allGoalsHavePromptResponse: false,
- title: 'FEI Goal',
- }],
- standard: 'Monitoring',
- objectives: [
- {
- id: 1,
- citations: [
- {
- id: 1,
- text: 'citation 1',
- monitoringReferences: [{
- grantId: 2,
- }],
- },
- ],
- },
- ],
- goalIds: [1, 2],
- }];
+ formData.goalsAndObjectives = [
+ ...goalsAndObjectives,
+ {
+ isCurated: true,
+ prompts: [{
+ allGoalsHavePromptResponse: false,
+ title: 'FEI Goal',
+ }],
+ standard: 'Monitoring',
+ objectives: [
+ {
+ id: 1,
+ citations: [
+ {
+ id: 1,
+ text: 'citation 1',
+ monitoringReferences: [{
+ grantId: 2,
+ }],
+ },
+ ],
+ },
+ ],
+ goalIds: [1, 2],
+ }];
}
const history = createMemoryHistory();
@@ -192,6 +195,37 @@ describe('Submitter review page', () => {
expect(await screen.findByText(/recipient missing monitoring/i)).toBeVisible();
});
+ it('hides error that some grants don\'t have monitoring if we have more than one goal', async () => {
+ renderReview(
+ REPORT_STATUSES.DRAFT,
+ () => { },
+ false,
+ jest.fn(),
+ jest.fn(),
+ [],
+ defaultUser,
+ null,
+ false,
+ true,
+ [{
+ isCurated: false,
+ prompts: [{
+ allGoalsHavePromptResponse: false,
+ title: 'A regular goal',
+ }],
+ objectives: [
+ {
+ id: 1,
+ citations: null,
+ },
+ ],
+ goalIds: [1],
+ }],
+ );
+ expect(screen.queryAllByText(/this grant does not have the standard monitoring goal/i).length).toBe(0);
+ expect(screen.queryAllByText(/recipient missing monitoring/i).length).toBe(0);
+ });
+
it('shows an error if goals are missing prompts', async () => {
fetchMock.get('/api/goals/region/1/incomplete?goalIds=1&goalIds=2', [
{
diff --git a/frontend/src/pages/ActivityReport/Pages/Review/Submitter/index.js b/frontend/src/pages/ActivityReport/Pages/Review/Submitter/index.js
index 193b23470e..9e82931b41 100644
--- a/frontend/src/pages/ActivityReport/Pages/Review/Submitter/index.js
+++ b/frontend/src/pages/ActivityReport/Pages/Review/Submitter/index.js
@@ -97,8 +97,8 @@ const Submitter = ({
const grantsMissingMonitoring = () => {
// First determine if we have a monitoring goal selected.
const hasMonitoringGoalSelected = (goalsAndObjectives || []).find((goal) => (goal.standard && goal.standard === 'Monitoring'));
-
- if (hasMonitoringGoalSelected) {
+ // If we only have a monitoring goal.
+ if ((!goalsAndObjectives || goalsAndObjectives.length === 1) && hasMonitoringGoalSelected) {
// Then get the grantIds from activityRecipients
// Then compare the two lists and return the difference
const grantsWithCitations = hasMonitoringGoalSelected.objectives.reduce(
diff --git a/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js b/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js
index c9130db1e0..2efea828fb 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js
@@ -122,8 +122,12 @@ const GoalPicker = ({
// Fetch citations for the goal if the source is CLASS or RANs.
useDeepCompareEffect(() => {
async function fetchCitations() {
- // If its a monitoring goal and the source is CLASS or RANs, fetch the citations.
- if (goalForEditing && goalForEditing.standard && goalForEditing.standard === 'Monitoring') {
+ // If we have no other goals except a monitoring goal
+ // and the source is CLASS or RANs, fetch the citations.
+ if ((!selectedGoals || selectedGoals.length === 0)
+ && goalForEditing
+ && goalForEditing.standard
+ && goalForEditing.standard === 'Monitoring') {
const retrievedCitationOptions = await fetchCitationsByGrant(
regionId,
grantIds,
diff --git a/frontend/src/pages/ActivityReport/Pages/components/__tests__/GoalPicker.js b/frontend/src/pages/ActivityReport/Pages/components/__tests__/GoalPicker.js
index aff6b6b8d5..aa9920f49f 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/__tests__/GoalPicker.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/__tests__/GoalPicker.js
@@ -563,5 +563,139 @@ describe('GoalPicker', () => {
expect(await screen.findByText(/grant 1 name/i)).toBeVisible();
expect(await screen.findByText(/to avoid errors when submitting the report, you can either/i)).toBeVisible();
});
+
+ it('correctly hides the monitoring warning if non monitoring recipients are selected with another goal', async () => {
+ fetchMock.get('/api/goal-templates/1/prompts?goalIds=1&goalIds=2', []);
+ fetchMock.get('/api/goal-templates/1/source?grantIds=2&grantIds=1', {
+ source: 'Federal monitoring issues, including CLASS and RANs',
+ });
+
+ // api/citations/region/1?grantIds=1&reportStartDate=2024-12-03
+ fetchMock.get('/api/citations/region/1?grantIds=1&reportStartDate=2024-12-03', [
+ {
+ citation: 'Not your citation',
+ grants: [
+ {
+ acro: 'DEF',
+ citation: 'test citation 1',
+ findingId: 1,
+ findingSource: 'source',
+ findingType: 'Not your citation type',
+ grantId: 2,
+ grantNumber: '123',
+ monitoringFindingStatusName: 'Active',
+ reportDeliveryDate: '2024-12-03',
+ reviewName: 'review name',
+ severity: 1,
+ },
+ ],
+ standardId: 1,
+ },
+ ]);
+
+ fetchMock.get('/api/citations/region/1?grantIds=1&grantIds=2&reportStartDate=2024-12-03', [
+ {
+ citation: 'Not your citation',
+ grants: [
+ {
+ acro: 'DEF',
+ citation: 'test citation 1',
+ findingId: 1,
+ findingSource: 'source',
+ findingType: 'Not your citation type',
+ grantId: 2,
+ grantNumber: '123',
+ monitoringFindingStatusName: 'Active',
+ reportDeliveryDate: '2024-12-03',
+ reviewName: 'review name',
+ severity: 1,
+ },
+ ],
+ standardId: 1,
+ },
+ ]);
+
+ const availableGoals = [{
+ label: 'Goal 1',
+ value: 1,
+ goalIds: [1, 2],
+ name: 'Goal 1',
+ objectives: [],
+ }];
+ const availableTemplates = [{
+ label: 'Monitoring Template Goal',
+ value: 1,
+ goalIds: [1, 2],
+ isCurated: true,
+ goalTemplateId: 1,
+ source: 'Federal monitoring issues, including CLASS and RANs',
+ standard: 'Monitoring',
+ objectives: [],
+ goals: [
+ {
+ grantId: 1,
+ },
+ {
+ grantId: 2,
+ },
+ ],
+ }];
+ const goalForEditing = {
+ standard: 'Monitoring',
+ objectives: [{
+ topics: [],
+ id: 1,
+ title: 'Objective 1',
+ resources: [],
+ ttaProvided: '',
+ objectiveCreatedHere: true,
+ }],
+ goalIds: [],
+ };
+ act(() => {
+ renderGoalPicker(availableGoals, [{ id: 1, grantId: 1 }], goalForEditing, availableTemplates, [{ activityRecipientId: 2, name: 'Grant 2 Name' }]);
+ });
+ let selector = screen.queryByLabelText(/Select recipient's goal*/i);
+ expect(selector).toBeVisible();
+
+ // Check box to use curated goals.
+ const checkbox = await screen.findByRole('checkbox', { name: /use ohs standard goal/i });
+ await act(async () => {
+ // use selectEvent to check the checkbox.
+ await userEvent.click(checkbox);
+ await waitFor(async () => {
+ // wait for check box to be checked.
+ expect(checkbox).toBeChecked();
+ });
+ });
+
+ selector = await screen.findByLabelText(/Select recipient's goal*/i);
+
+ await act(async () => {
+ await selectEvent.select(selector, ['Monitoring Template Goal']);
+ });
+
+ // Select first template goal.
+
+ fireEvent.focus(selector);
+ await act(async () => {
+ // arrow down to the first option and select it.
+ fireEvent.keyDown(selector, {
+ key: 'ArrowDown',
+ keyCode: 40,
+ code: 40,
+ });
+ });
+
+ await act(async () => {
+ await waitFor(async () => {
+ const option = await screen.findByText('Monitoring Template Goal');
+ expect(option).toBeVisible();
+ });
+ });
+ expect(screen.queryAllByText(/this grant does not have the standard monitoring goal/i).length).toBe(0);
+ expect(screen.queryAllByText(/grant 1 name/i).length).toBe(0);
+ expect(screen.queryAllByText(/to avoid errors when submitting the report, you can either/i).length).toBe(0);
+ });
});
});
From e856a25d5c2034b0870166457592c7e01b476711 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Mon, 16 Dec 2024 11:29:55 -0500
Subject: [PATCH 128/198] fix the issue Matt found of multiple citaitons for
different grants not showing once saved
---
src/goalServices/reduceGoals.ts | 65 +++++++++++++++++++--------------
1 file changed, 37 insertions(+), 28 deletions(-)
diff --git a/src/goalServices/reduceGoals.ts b/src/goalServices/reduceGoals.ts
index 6dd0e25716..42a5ce2803 100644
--- a/src/goalServices/reduceGoals.ts
+++ b/src/goalServices/reduceGoals.ts
@@ -178,19 +178,26 @@ export function reduceObjectivesForActivityReport(
'course',
exists,
);
- // Citations should return null if they are not applicable.
+
+ // Citations should return null not exists (every subsequent adding of objective).
exists.citations = objective.activityReportObjectives
&& objective.activityReportObjectives.length > 0
- && objective.activityReportObjectives[0].activityReportObjectiveCitations
- && objective.activityReportObjectives[0].activityReportObjectiveCitations.length > 0
- ? uniq(objective.activityReportObjectives[0].activityReportObjectiveCitations.map(
- (c) => ({
- ...c.dataValues,
- id: c.monitoringReferences[0].standardId,
- name: `${c.monitoringReferences[0].acro} - ${c.citation} - ${c.monitoringReferences[0].findingSource}`,
- }),
- ))
- : null;
+ ? uniq([
+ ...exists.citations,
+ ...objective.activityReportObjectives.flatMap(
+ (aro) => aro.activityReportObjectiveCitations.map((c) => ({
+ ...c.dataValues,
+ id: c.monitoringReferences[0].standardId,
+ name: `${c.monitoringReferences[0].acro} - ${c.citation} - ${c.monitoringReferences[0].findingSource}`,
+ })),
+ ),
+ ])
+ : [];
+
+ // Set to null if we don't have any citations.
+ if (exists.citations.length === 0) {
+ exists.citations = null;
+ }
exists.files = uniqBy([
...exists.files,
@@ -226,7 +233,7 @@ export function reduceObjectivesForActivityReport(
const { id } = objective;
- return [...objectives, {
+ const newObjective = {
...objective.dataValues,
title: objective.title.trim(),
value: id,
@@ -258,7 +265,7 @@ export function reduceObjectivesForActivityReport(
'value',
),
files: objective.activityReportObjectives
- && objective.activityReportObjectives.length > 0
+ && objective.activityReportObjectives.length > 0
? objective.activityReportObjectives[0].activityReportObjectiveFiles
.map((f) => ({ ...f.file.dataValues, url: f.file.url }))
: [],
@@ -267,23 +274,25 @@ export function reduceObjectivesForActivityReport(
'activityReportObjectiveCourses',
'course',
),
- // Citations should return null if they are not applicable.
+ // Citations should return null if they are not applicable (first time we add the objective).
citations: objective.activityReportObjectives
&& objective.activityReportObjectives.length > 0
- && objective.activityReportObjectives[0].activityReportObjectiveCitations
- && objective.activityReportObjectives[0].activityReportObjectiveCitations.length > 0
- ? uniq(
- objective.activityReportObjectives[0].activityReportObjectiveCitations.map(
- (c) => (
- {
- ...c.dataValues,
- id: c.monitoringReferences[0].standardId,
- name: `${c.monitoringReferences[0].acro} - ${c.citation} - ${c.monitoringReferences[0].findingSource}`,
- }),
- ),
- )
- : null,
- }];
+ ? uniq(objective.activityReportObjectives.flatMap(
+ (aro) => aro.activityReportObjectiveCitations.map((c) => ({
+ ...c.dataValues,
+ id: c.monitoringReferences[0].standardId,
+ name: `${c.monitoringReferences[0].acro} - ${c.citation} - ${c.monitoringReferences[0].findingSource}`,
+ })),
+ ))
+ : [],
+ };
+
+ // If we have no citations set to null.
+ if (newObjective.citations.length === 0) {
+ newObjective.citations = null;
+ }
+
+ return [...objectives, newObjective];
}, currentObjectives);
// Sort by AR Order in place.
From 622731629228afa6ca65ba81afd5f462f1c6bdd3 Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Mon, 16 Dec 2024 11:48:47 -0500
Subject: [PATCH 129/198] Fix multiple grant spacing ar review
---
.../pages/ActivityReport/Pages/Review/ReviewObjectiveCitation.js | 1 +
1 file changed, 1 insertion(+)
diff --git a/frontend/src/pages/ActivityReport/Pages/Review/ReviewObjectiveCitation.js b/frontend/src/pages/ActivityReport/Pages/Review/ReviewObjectiveCitation.js
index e8048024da..339cbb1f88 100644
--- a/frontend/src/pages/ActivityReport/Pages/Review/ReviewObjectiveCitation.js
+++ b/frontend/src/pages/ActivityReport/Pages/Review/ReviewObjectiveCitation.js
@@ -25,6 +25,7 @@ const ReviewObjectiveCitation = ({
From af96d0a8da0d4d9f659eed9c836c68fa42ebe837 Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Mon, 16 Dec 2024 12:59:39 -0500
Subject: [PATCH 130/198] Update capitalization and ensure order in citation
service
---
frontend/src/components/GoalForm/GenericSelectWithDrawer.js | 2 +-
src/services/citations.ts | 1 +
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/frontend/src/components/GoalForm/GenericSelectWithDrawer.js b/frontend/src/components/GoalForm/GenericSelectWithDrawer.js
index 93dfb082a4..befc980f42 100644
--- a/frontend/src/components/GoalForm/GenericSelectWithDrawer.js
+++ b/frontend/src/components/GoalForm/GenericSelectWithDrawer.js
@@ -51,7 +51,7 @@ export default function GenericSelectWithDrawer({
Get help choosing
{' '}
- {name}
+ {name.toLowerCase()}
s
diff --git a/src/services/citations.ts b/src/services/citations.ts
index c9d182b44f..6c792d33b3 100644
--- a/src/services/citations.ts
+++ b/src/services/citations.ts
@@ -12,6 +12,7 @@ export async function textByCitation(
citation: citationIds,
},
group: ['text', 'citation'],
+ order: ['citation'],
});
}
From e277bdf67189bdab46593a0c27773e562e9705b2 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Mon, 16 Dec 2024 15:53:59 -0500
Subject: [PATCH 131/198] simply check monitoring goal is linked to grant id
---
.../Pages/Review/Submitter/__tests__/index.js | 1 +
.../Pages/Review/Submitter/index.js | 18 ++---
.../Pages/components/GoalPicker.js | 62 ++++++++------
.../Pages/components/__tests__/GoalPicker.js | 3 -
src/services/citations.ts | 80 ++++++++++++++++++-
5 files changed, 121 insertions(+), 43 deletions(-)
diff --git a/frontend/src/pages/ActivityReport/Pages/Review/Submitter/__tests__/index.js b/frontend/src/pages/ActivityReport/Pages/Review/Submitter/__tests__/index.js
index d6cbc4b51b..c026ee147d 100644
--- a/frontend/src/pages/ActivityReport/Pages/Review/Submitter/__tests__/index.js
+++ b/frontend/src/pages/ActivityReport/Pages/Review/Submitter/__tests__/index.js
@@ -127,6 +127,7 @@ const renderReview = (
},
],
goalIds: [1, 2],
+ grantIds: [2],
}];
}
diff --git a/frontend/src/pages/ActivityReport/Pages/Review/Submitter/index.js b/frontend/src/pages/ActivityReport/Pages/Review/Submitter/index.js
index 9e82931b41..650c977bdd 100644
--- a/frontend/src/pages/ActivityReport/Pages/Review/Submitter/index.js
+++ b/frontend/src/pages/ActivityReport/Pages/Review/Submitter/index.js
@@ -101,22 +101,13 @@ const Submitter = ({
if ((!goalsAndObjectives || goalsAndObjectives.length === 1) && hasMonitoringGoalSelected) {
// Then get the grantIds from activityRecipients
// Then compare the two lists and return the difference
- const grantsWithCitations = hasMonitoringGoalSelected.objectives.reduce(
- (acc, objective) => objective.citations.reduce((acc2, citation) => {
- const { monitoringReferences } = citation;
- if (monitoringReferences) {
- const grantIds = monitoringReferences.map((ref) => ref.grantId);
- return [...acc2, ...grantIds];
- }
- return acc2;
- }, acc), [],
- );
-
- const distinctGrants = [...new Set(grantsWithCitations)];
+ const missingGrants = activityRecipients.filter(
+ (recipient) => !goalsAndObjectives[0].grantIds.includes(recipient.activityRecipientId),
+ ).map((recipient) => recipient.activityRecipientId);
// From activityRecipients get the name of the grants that matcht the activityRecipientId.
const grantNames = activityRecipients.filter(
- (recipient) => !distinctGrants.includes(recipient.activityRecipientId),
+ (recipient) => missingGrants.includes(recipient.activityRecipientId),
).map(
(recipient) => recipient.name,
);
@@ -215,6 +206,7 @@ Submitter.propTypes = {
),
goalsAndObjectives: PropTypes.arrayOf(PropTypes.shape({
standard: PropTypes.string,
+ grantIds: PropTypes.arrayOf(PropTypes.number),
})),
activityRecipients: PropTypes.arrayOf(PropTypes.shape({
activityRecipientId: PropTypes.number,
diff --git a/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js b/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js
index 2efea828fb..c1588e432e 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js
@@ -110,6 +110,10 @@ const GoalPicker = ({
defaultValue: '',
});
+ const isMonitoringGoal = goalForEditing
+ && goalForEditing.standard
+ && goalForEditing.standard === 'Monitoring';
+
// for fetching topic options from API
useEffect(() => {
async function fetchTopics() {
@@ -124,10 +128,7 @@ const GoalPicker = ({
async function fetchCitations() {
// If we have no other goals except a monitoring goal
// and the source is CLASS or RANs, fetch the citations.
- if ((!selectedGoals || selectedGoals.length === 0)
- && goalForEditing
- && goalForEditing.standard
- && goalForEditing.standard === 'Monitoring') {
+ if (isMonitoringGoal) {
const retrievedCitationOptions = await fetchCitationsByGrant(
regionId,
grantIds,
@@ -156,36 +157,16 @@ const GoalPicker = ({
return acc;
}, {},
));
-
- // Get a list of grants with monitoring goals.
- const grantIdsFromCitations = retrievedCitationOptions.flatMap(
- (citation) => citation.grants.map((grant) => grant.grantId),
- );
- const distinctGrantIds = [...new Set(grantIdsFromCitations)];
-
- // Determine if any grant selected is missing a monitoring goal.
- let grantsIdsMissingMonitoring = [];
- grantsIdsMissingMonitoring = grantIds.filter(
- (grantId) => !distinctGrantIds.includes(grantId),
- );
-
- // Get the full grant names from the grantsIdsMissingMonitoring.
- const grantsIdsMissingMonitoringFullNames = activityRecipients.filter(
- (ar) => grantsIdsMissingMonitoring.includes(ar.activityRecipientId),
- ).map((grant) => grant.name);
-
setCitationOptions(uniqueCitationOptions);
setRawCitations(retrievedCitationOptions);
- setGrantsWithoutMonitoring(grantsIdsMissingMonitoringFullNames);
}
} else {
setCitationOptions([]);
setRawCitations([]);
- setGrantsWithoutMonitoring([]);
}
}
fetchCitations();
- }, [goalForEditing, regionId, startDate, grantIds]);
+ }, [goalForEditing, regionId, startDate, grantIds, isMonitoringGoal]);
const uniqueAvailableGoals = uniqBy(allAvailableGoals, 'name');
@@ -275,6 +256,37 @@ const GoalPicker = ({
onChangeGoal(goal);
};
+ useDeepCompareEffect(() => {
+ // We have only a single monitoring goal selected.
+ if (isMonitoringGoal && (!selectedGoals || selectedGoals.length === 0)) {
+ // Get the monitoring goal from the templates.
+ const monitoringGoal = goalTemplates.find((goal) => goal.standard === 'Monitoring');
+ if (monitoringGoal) {
+ // Find any grants that are missing from the monitoring goal.
+ const missingGrants = grantIds.filter(
+ (grantId) => !monitoringGoal.goals.find((g) => g.grantId === grantId),
+ );
+
+ if (missingGrants.length > 0) {
+ // get the names of the grants that are missing from goalForEditing.grants
+ const grantsIdsMissingMonitoringFullNames = activityRecipients.filter(
+ (ar) => missingGrants.includes(ar.activityRecipientId),
+ ).map((grant) => grant.name);
+ setGrantsWithoutMonitoring(grantsIdsMissingMonitoringFullNames);
+ } else {
+ setGrantsWithoutMonitoring([]);
+ }
+ }
+ } else {
+ setGrantsWithoutMonitoring([]);
+ }
+ }, [goalForEditing,
+ grantIds,
+ selectedGoals,
+ activityRecipients,
+ isMonitoringGoal,
+ goalTemplates]);
+
const pickerOptions = useOhsStandardGoal ? goalTemplates : options;
return (
diff --git a/frontend/src/pages/ActivityReport/Pages/components/__tests__/GoalPicker.js b/frontend/src/pages/ActivityReport/Pages/components/__tests__/GoalPicker.js
index aa9920f49f..fc8baca8f2 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/__tests__/GoalPicker.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/__tests__/GoalPicker.js
@@ -498,9 +498,6 @@ describe('GoalPicker', () => {
standard: 'Monitoring',
objectives: [],
goals: [
- {
- grantId: 1,
- },
{
grantId: 2,
},
diff --git a/src/services/citations.ts b/src/services/citations.ts
index 53f1cba3b0..dbdc5fb187 100644
--- a/src/services/citations.ts
+++ b/src/services/citations.ts
@@ -16,8 +16,8 @@ export async function textByCitation(
}
// TODO: Update this to the day we deploy to PROD.
-const cutOffStartDate = new Date().toISOString().split('T')[0];
-// const cutOffStartDate = '2021-01-01';
+// const cutOffStartDate = new Date().toISOString().split('T')[0];
+const cutOffStartDate = '2021-01-01';
/*
The purpose of this function is to get citations by grant id.
We then need to format the response for how it needs to be
@@ -46,6 +46,82 @@ export async function getCitationsByGrantIds(
grantIds: number[],
reportStartDate: string,
): Promise {
+ console.log('\n\n\n---- before sql');
+ console.log('\n\n-sql: ', `
+ SELECT
+ ms."standardId",
+ ms.citation,
+ JSONB_AGG( DISTINCT
+ JSONB_BUILD_OBJECT(
+ 'findingId', mf."findingId",
+ 'grantId', gr.id,
+ 'grantNumber', gr.number,
+ 'reviewName', mfh."name",
+ 'reportDeliveryDate', mfh."reportDeliveryDate",
+ 'findingType', mf."findingType",
+ 'findingSource', mf."source",
+ 'monitoringFindingStatusName', mfs."name",
+ 'reportDeliveryDate', mfh."reportDeliveryDate",
+ 'citation', ms.citation,
+ 'severity', CASE
+ WHEN mf."findingType" = 'Deficiency' THEN 1
+ WHEN mf."findingType" = 'Noncompliance' THEN 2
+ ELSE 3
+ END,
+ 'acro', CASE
+ WHEN mf."findingType" = 'Deficiency' THEN 'DEF'
+ WHEN mf."findingType" = 'Noncompliance' THEN 'ANC'
+ ELSE 'AOC'
+ END
+ )
+ ) grants
+ FROM "Grants" gr
+ JOIN "Goals" g
+ ON gr."id" = g."grantId"
+ AND g."status" NOT IN ('Closed', 'Suspended')
+ JOIN "GoalTemplates" gt
+ ON g."goalTemplateId" = gt."id"
+ AND gt."standard" = 'Monitoring'
+ JOIN "MonitoringReviewGrantees" mrg
+ ON gr.number = mrg."grantNumber"
+ JOIN (
+ -- The below 'DISTINCT ON' determines the single record to return values from by the 'ORDER BY' clause.
+ SELECT DISTINCT ON (mfh."findingId", gr.id)
+ mfh."findingId",
+ gr.id AS "grantId",
+ mr."reviewId",
+ mr."name",
+ mr."reportDeliveryDate"
+ FROM "MonitoringFindingHistories" mfh
+ JOIN "MonitoringReviews" mr
+ ON mfh."reviewId" = mr."reviewId"
+ JOIN "MonitoringReviewGrantees" mrg
+ ON mrg."reviewId" = mr."reviewId"
+ JOIN "Grants" gr
+ ON gr.number = mrg."grantNumber"
+ ORDER BY mfh."findingId", gr.id, mr."reportDeliveryDate" DESC
+ ) mfh -- Subquery ensures only the most recent history for each finding-grant combination
+ ON mfh."grantId" = gr.id
+ JOIN "MonitoringFindings" mf
+ ON mfh."findingId" = mf."findingId"
+ JOIN "MonitoringFindingStatuses" mfs
+ ON mf."statusId" = mfs."statusId"
+ JOIN "MonitoringFindingStandards" mfs2
+ ON mf."findingId" = mfs2."findingId"
+ JOIN "MonitoringStandards" ms
+ ON mfs2."standardId" = ms."standardId"
+ JOIN "MonitoringFindingGrants" mfg
+ ON mf."findingId" = mfg."findingId"
+ AND mrg."granteeId" = mfg."granteeId"
+ WHERE 1 = 1
+ AND gr.id IN (${grantIds.join(',')}) -- :grantIds
+ AND mfh."reportDeliveryDate"::date BETWEEN '${cutOffStartDate}' AND '${reportStartDate}'
+ AND gr.status = 'Active'
+ AND mfs.name = 'Active'
+ GROUP BY 1,2
+ ORDER BY 2,1;
+ `);
+
// Query to get the citations by grant id.
const grantsByCitations = await sequelize.query(
/* sql */
From d1963a8c62d105a34f1071d5090eebe7bce35906 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Tue, 17 Dec 2024 08:54:11 -0500
Subject: [PATCH 132/198] add warnings to goal and objectives page
---
.../Pages/components/Objective.js | 100 ++++++++--
.../Pages/components/__tests__/Objective.js | 177 ++++++++++++------
2 files changed, 203 insertions(+), 74 deletions(-)
diff --git a/frontend/src/pages/ActivityReport/Pages/components/Objective.js b/frontend/src/pages/ActivityReport/Pages/components/Objective.js
index 135db27e5f..7a743a6d74 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/Objective.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/Objective.js
@@ -1,11 +1,16 @@
import React, {
useState, useContext, useRef,
+ useEffect,
} from 'react';
import PropTypes from 'prop-types';
import { v4 as uuidv4 } from 'uuid';
+import { Link } from 'react-router-dom';
import {
useController, useFormContext,
} from 'react-hook-form';
+import {
+ Alert,
+} from '@trussworks/react-uswds';
import ObjectiveTitle from './ObjectiveTitle';
import GenericSelectWithDrawer from '../../../../components/GoalForm/GenericSelectWithDrawer';
import ResourceRepeater from '../../../../components/GoalForm/ResourceRepeater';
@@ -56,10 +61,14 @@ export default function Objective({
}))();
const [selectedObjective, setSelectedObjective] = useState(initialObjective);
const [statusForCalculations, setStatusForCalculations] = useState(initialObjectiveStatus);
- const { getValues, setError, clearErrors } = useFormContext();
+ const {
+ getValues, setError, clearErrors, watch,
+ } = useFormContext();
const { setAppLoadingText, setIsAppLoading } = useContext(AppLoadingContext);
const { objectiveCreatedHere } = initialObjective;
const [onApprovedAR, setOnApprovedAR] = useState(initialObjective.onApprovedAR);
+ const [citationWarnings, setCitationWarnings] = useState([]);
+ const activityRecipients = watch('activityRecipients');
/**
* add controllers for all the controlled fields
@@ -67,6 +76,24 @@ export default function Objective({
* but we want to keep the logic in one place for the AR/RTR
* if at all possible
*/
+ const {
+ field: {
+ onChange: onChangeCitations,
+ onBlur: onBlurCitations,
+ value: objectiveCitations,
+ name: objectiveCitationsInputName,
+ },
+ } = useController({
+ name: `${fieldArrayName}[${index}].citations`,
+ rules: {
+ validate: {
+ notEmpty: (value) => (value && value.length) || OBJECTIVE_CITATIONS,
+ },
+ },
+ // If citations are not available, set citations to null
+ defaultValue: objective.citations,
+ });
+
const {
field: {
onChange: onChangeTitle,
@@ -102,24 +129,6 @@ export default function Objective({
defaultValue: objective.topics,
});
- const {
- field: {
- onChange: onChangeCitations,
- onBlur: onBlurCitations,
- value: objectiveCitations,
- name: objectiveCitationsInputName,
- },
- } = useController({
- name: `${fieldArrayName}[${index}].citations`,
- rules: {
- validate: {
- notEmpty: (value) => (value && value.length) || OBJECTIVE_CITATIONS,
- },
- },
- // If citations are not available, set citations to null
- defaultValue: objective.citations,
- });
-
const {
field: {
onChange: onChangeResources,
@@ -374,9 +383,28 @@ export default function Objective({
})),
],
}));
+
onChangeCitations([...newCitationsObjects]);
};
+ useEffect(() => {
+ // Get a distinct list of grantId's from the citation.grants array.
+ if (!objectiveCitations || !objectiveCitations.length) {
+ return;
+ }
+ const grantIdsWithCitations = Array.from(objectiveCitations.reduce((acc, citation) => {
+ const grantsToAdd = citation.monitoringReferences.map((grant) => grant.grantId);
+ return new Set([...acc, ...grantsToAdd]);
+ }, new Set()));
+
+ // Find all the grantIds not in grantIdsWithCitations.
+ const missingRecipientGrantNames = (activityRecipients || []).filter(
+ (recipient) => !grantIdsWithCitations.includes(recipient.activityRecipientId),
+ ).map((recipient) => recipient.name);
+
+ setCitationWarnings(missingRecipientGrantNames);
+ }, [objectiveCitations, activityRecipients]);
+
return (
<>
+ {
+ citationOptions.length > 0 && citationWarnings.length > 0 && (
+
+
+
+ {citationWarnings.length > 1
+ ? 'These grants do not have any of the citations selected:'
+ : 'This grant does not have any of the citations selected:'}
+
+ {citationWarnings.map((grant) => (
+ {grant}
+ ))}
+
+
+
+ To avoid errors when submitting the report, you can either:
+
+
+ Add a citation for this grant under an objective for the monitoring goal
+
+
+ Remove the grant from the
+ {' '}
+ Activity summary
+
+
+ Add another goal to the report
+
+
+
+
+
+ )
+ }
{
citationOptions.length > 0 && (
{}, citationOptions = [], rawCitations = [],
+ objective = defaultObjective,
+ onRemove = () => {},
+ citationOptions = [],
+ rawCitations = [],
+ additionalHookFormData = {},
}) => {
const hookForm = useForm({
mode: 'onBlur',
defaultValues: {
+ ...additionalHookFormData,
objectives: [objective],
collaborators: [],
author: {
@@ -77,65 +87,67 @@ const RenderObjective = ({
};
return (
-
-
-
+
+
+
-
-
-
-
+ >
+
+
+
+
+
);
};
@@ -289,4 +301,59 @@ describe('Objective', () => {
expect(await screen.findByLabelText(/objective status/i)).toBeVisible();
expect(await screen.findByText(/not started/i)).toBeVisible();
});
+
+ it('shows a warning when the citations selected are not for all the grants selected', async () => {
+ const citationOptions = [{
+ label: 'Label 1',
+ options: [
+ { name: 'Citation 1', id: 1 },
+ ],
+ }];
+
+ const rawCitations = [{
+ citation: 'Citation 1',
+ standardId: 1,
+ grants: [{
+ acro: 'DEF',
+ citation: 'Citation 1',
+ grantId: 1,
+ grantNumber: '12345',
+ }],
+ }];
+
+ const additionalHookFormData = {
+ activityRecipients: [
+ {
+ id: 1,
+ activityRecipientId: 1,
+ name: 'Recipient 1',
+ },
+ {
+ id: 2,
+ activityRecipientId: 2,
+ name: 'Recipient 2',
+ },
+ ],
+ };
+
+ render( );
+
+ const helpButton = screen.getByRole('button', { name: /get help choosing citation/i });
+ expect(helpButton).toBeVisible();
+ const citationsButton = screen.getByRole('button', { name: /Citation/i });
+ expect(citationsButton).toBeVisible();
+
+ const citationSelect = await screen.findByLabelText(/citation/i);
+ await selectEvent.select(citationSelect, [/Citation 1/i]);
+
+ expect(await screen.findByText(/Citation 1/i)).toBeVisible();
+
+ expect(await screen.findByText(/This grant does not have any of the citations selected/i)).toBeVisible();
+ expect(await screen.findByText(/Recipient 2/i)).toBeVisible();
+ expect(await screen.findByText(/To avoid errors when submitting the report, you can either/i)).toBeVisible();
+ });
});
From ea84764cf18d7eab41dd56d14cb8fcaf69bfd132 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Tue, 17 Dec 2024 11:09:47 -0500
Subject: [PATCH 133/198] add submit alert
---
.../Pages/Review/Submitter/Draft.js | 26 +++++++
.../Pages/Review/Submitter/__tests__/index.js | 77 +++++++++++++++++++
.../Pages/Review/Submitter/index.js | 45 ++++++++++-
.../Pages/components/Objective.js | 16 ++--
4 files changed, 158 insertions(+), 6 deletions(-)
diff --git a/frontend/src/pages/ActivityReport/Pages/Review/Submitter/Draft.js b/frontend/src/pages/ActivityReport/Pages/Review/Submitter/Draft.js
index ef46398df7..8c5cd85b76 100644
--- a/frontend/src/pages/ActivityReport/Pages/Review/Submitter/Draft.js
+++ b/frontend/src/pages/ActivityReport/Pages/Review/Submitter/Draft.js
@@ -29,6 +29,7 @@ const Draft = ({
lastSaveTime,
creatorRole,
grantsMissingMonitoring,
+ grantsMissingCitations,
}) => {
const {
watch,
@@ -185,6 +186,30 @@ const Draft = ({
)
}
+ {
+ grantsMissingCitations.length > 0 && (
+
+ {
+ grantsMissingCitations.length > 1
+ ? 'These grants do not have any of the citations selected:'
+ : 'This grant does not have any of the citations selected:'
+ }
+
+ {grantsMissingCitations.map((grant) => {grant} )}
+
+ You can either:
+
+ Add a citation for this grant under an objective for the monitoring goal
+
+ Remove the grant from the
+ {' '}
+ Activity summary
+
+ Add another goal to the report
+
+
+ )
+ }
{hasIncompletePages && }
{!allGoalsHavePromptResponses && (
{
const formData = {
approvers,
@@ -116,6 +117,7 @@ const renderReview = (
{
id: 1,
citations: [
+ ...additionalCitations,
{
id: 1,
text: 'citation 1',
@@ -191,11 +193,77 @@ describe('Submitter review page', () => {
null,
false,
true,
+ [],
+ [
+ {
+ id: 1,
+ text: 'additional citation',
+ monitoringReferences: [{
+ grantId: 1,
+ }],
+ },
+ ],
);
expect(await screen.findByText(/this grant does not have the standard monitoring goal/i)).toBeVisible();
expect(await screen.findByText(/recipient missing monitoring/i)).toBeVisible();
});
+ it('shows an error if some of the grants are missing citations', async () => {
+ renderReview(
+ REPORT_STATUSES.DRAFT,
+ () => { },
+ false,
+ jest.fn(),
+ jest.fn(),
+ [],
+ defaultUser,
+ null,
+ false,
+ true,
+ );
+ expect(await screen.findByText(/This grant does not have any of the citations selected/i)).toBeVisible();
+ expect(screen.queryAllByText(/recipient missing monitoring/i).length).toBe(2);
+ });
+
+ it('hides an error if some of the grants are missing citations', async () => {
+ renderReview(
+ REPORT_STATUSES.DRAFT,
+ () => { },
+ false,
+ jest.fn(),
+ jest.fn(),
+ [],
+ defaultUser,
+ null,
+ false,
+ true,
+ [{
+ isCurated: false,
+ prompts: [{
+ allGoalsHavePromptResponse: false,
+ title: 'A regular goal',
+ }],
+ objectives: [
+ {
+ id: 1,
+ citations: null,
+ },
+ ],
+ goalIds: [1],
+ }],
+ [
+ {
+ id: 1,
+ text: 'additional citation',
+ monitoringReferences: [{
+ grantId: 1,
+ }],
+ },
+ ],
+ );
+ expect(screen.queryAllByText(/This grant does not have any of the citations selected/i).length).toBe(0);
+ });
+
it('hides error that some grants don\'t have monitoring if we have more than one goal', async () => {
renderReview(
REPORT_STATUSES.DRAFT,
@@ -222,6 +290,15 @@ describe('Submitter review page', () => {
],
goalIds: [1],
}],
+ [
+ {
+ id: 1,
+ text: 'additional citation',
+ monitoringReferences: [{
+ grantId: 1,
+ }],
+ },
+ ],
);
expect(screen.queryAllByText(/this grant does not have the standard monitoring goal/i).length).toBe(0);
expect(screen.queryAllByText(/recipient missing monitoring/i).length).toBe(0);
diff --git a/frontend/src/pages/ActivityReport/Pages/Review/Submitter/index.js b/frontend/src/pages/ActivityReport/Pages/Review/Submitter/index.js
index 650c977bdd..57600f51af 100644
--- a/frontend/src/pages/ActivityReport/Pages/Review/Submitter/index.js
+++ b/frontend/src/pages/ActivityReport/Pages/Review/Submitter/index.js
@@ -105,7 +105,49 @@ const Submitter = ({
(recipient) => !goalsAndObjectives[0].grantIds.includes(recipient.activityRecipientId),
).map((recipient) => recipient.activityRecipientId);
- // From activityRecipients get the name of the grants that matcht the activityRecipientId.
+ // From activityRecipients get the name of the grants that match the activityRecipientId.
+ const grantNames = activityRecipients.filter(
+ (recipient) => missingGrants.includes(recipient.activityRecipientId),
+ ).map(
+ (recipient) => recipient.name,
+ );
+ return grantNames;
+ }
+ return [];
+ };
+
+ const grantsMissingCitations = () => {
+ // Determine if we have a monitoring goal selected.
+ const hasMonitoringGoalSelected = (goalsAndObjectives || []).find((goal) => (goal.standard && goal.standard === 'Monitoring'));
+ if ((!goalsAndObjectives || goalsAndObjectives.length === 1) && hasMonitoringGoalSelected) {
+ const citationGrantIds = Array.from(hasMonitoringGoalSelected.objectives.reduce(
+ (acc, objective) => {
+ const monitoringReferencesFlat = objective.citations.map(
+ (citation) => citation.monitoringReferences,
+ ).flat();
+
+ const monitoringReferenceGrantIds = new Set(monitoringReferencesFlat.map(
+ (reference) => reference.grantId,
+ ));
+
+ // if acc doesnt have the grant ids in monitoringReferenceGrantIds, add them.
+ monitoringReferenceGrantIds.forEach((grantId) => {
+ if (!acc.has(grantId)) {
+ acc.add(grantId);
+ }
+ });
+ return acc;
+ }, new Set(),
+ ));
+
+ // console.log('hasMonitoringGoalSelected', hasMonitoringGoalSelected);
+ // Then get the grantIds from activityRecipients
+ // Then compare the two lists and return the difference
+ const missingGrants = activityRecipients.filter(
+ (recipient) => !citationGrantIds.includes(recipient.activityRecipientId),
+ ).map((recipient) => recipient.activityRecipientId);
+
+ // From activityRecipients get the name of the grants that match the activityRecipientId.
const grantNames = activityRecipients.filter(
(recipient) => missingGrants.includes(recipient.activityRecipientId),
).map(
@@ -141,6 +183,7 @@ const Submitter = ({
lastSaveTime={lastSaveTime}
creatorRole={creatorRole}
grantsMissingMonitoring={grantsMissingMonitoring()}
+ grantsMissingCitations={grantsMissingCitations()}
/>
)}
{submitted
diff --git a/frontend/src/pages/ActivityReport/Pages/components/Objective.js b/frontend/src/pages/ActivityReport/Pages/components/Objective.js
index 7a743a6d74..5ca19f0d6b 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/Objective.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/Objective.js
@@ -1,12 +1,13 @@
import React, {
useState, useContext, useRef,
- useEffect,
+ // useEffect,
} from 'react';
+import useDeepCompareEffect from 'use-deep-compare-effect';
import PropTypes from 'prop-types';
import { v4 as uuidv4 } from 'uuid';
import { Link } from 'react-router-dom';
import {
- useController, useFormContext,
+ useController, useFormContext, useWatch,
} from 'react-hook-form';
import {
Alert,
@@ -69,6 +70,7 @@ export default function Objective({
const [onApprovedAR, setOnApprovedAR] = useState(initialObjective.onApprovedAR);
const [citationWarnings, setCitationWarnings] = useState([]);
const activityRecipients = watch('activityRecipients');
+ const selectedGoals = useWatch({ name: 'goals' });
/**
* add controllers for all the controlled fields
@@ -387,9 +389,13 @@ export default function Objective({
onChangeCitations([...newCitationsObjects]);
};
- useEffect(() => {
+ useDeepCompareEffect(() => {
// Get a distinct list of grantId's from the citation.grants array.
- if (!objectiveCitations || !objectiveCitations.length) {
+ if ((!objectiveCitations || !objectiveCitations.length)
+ || (selectedGoals && selectedGoals.length > 0)) {
+ if (citationWarnings.length > 0) {
+ setCitationWarnings([]);
+ }
return;
}
const grantIdsWithCitations = Array.from(objectiveCitations.reduce((acc, citation) => {
@@ -403,7 +409,7 @@ export default function Objective({
).map((recipient) => recipient.name);
setCitationWarnings(missingRecipientGrantNames);
- }, [objectiveCitations, activityRecipients]);
+ }, [objectiveCitations, activityRecipients, selectedGoals, citationWarnings]);
return (
<>
From ae3ac831b05cfe62d413fb6adb251e34754f5ded Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Tue, 17 Dec 2024 11:28:56 -0500
Subject: [PATCH 134/198] Update with required join
---
src/services/monitoring.ts | 47 ++++++++++++++++++++------------------
1 file changed, 25 insertions(+), 22 deletions(-)
diff --git a/src/services/monitoring.ts b/src/services/monitoring.ts
index 61c004ae2b..62ca354ec1 100644
--- a/src/services/monitoring.ts
+++ b/src/services/monitoring.ts
@@ -2,7 +2,7 @@
import { Op } from 'sequelize';
import moment from 'moment';
import { uniq, uniqBy } from 'lodash';
-import db from '../models';
+import db, { sequelize } from '../models';
import {
ITTAByReviewResponse,
IMonitoringReview,
@@ -58,7 +58,7 @@ async function grantNumbersByRecipientAndRegion(recipientId: number, regionId: n
},
}) as { number: string }[];
- return grants.map((grant) => grant.number);
+ return grants.map((gr) => gr.number);
}
async function aroCitationsByGrantNumbers(grantNumbers: string[]): Promise {
@@ -208,24 +208,20 @@ async function aroCitationsByGrantNumbers(grantNumbers: string[]): Promise granteeId);
- }
+ const granteeIds = monitoringReviewGrantees.map(({ granteeId }) => granteeId);
return {
grantNumbers,
@@ -241,7 +237,8 @@ export async function ttaByReviews(
const {
grantNumbers,
citationsOnActivityReports,
- } = await extractExternalData(recipientId, regionId, false);
+ granteeIds,
+ } = await extractExternalData(recipientId, regionId);
const reviews = await MonitoringReview.findAll({
where: {
@@ -280,11 +277,21 @@ export async function ttaByReviews(
{
model: MonitoringFindingHistory,
as: 'monitoringFindingHistories',
+ required: true,
include: [
{
model: MonitoringFindingLink,
as: 'monitoringFindingLink',
+ required: true,
include: [
+ {
+ model: MonitoringFindingGrant,
+ as: 'monitoringFindingGrants',
+ where: {
+ granteeId: granteeIds,
+ },
+ required: true,
+ },
{
model: MonitoringFindingStandard,
as: 'monitoringFindingStandards',
@@ -333,10 +340,6 @@ export async function ttaByReviews(
let specialists = [];
monitoringFindingHistories.forEach((history) => {
- if (!history.monitoringFindingLink) {
- return;
- }
-
let citation = '';
const [findingStandards] = history.monitoringFindingLink.monitoringFindingStandards;
if (findingStandards) {
@@ -389,7 +392,7 @@ export async function ttaByCitations(
grantNumbers,
citationsOnActivityReports,
granteeIds,
- } = await extractExternalData(recipientId, regionId, true);
+ } = await extractExternalData(recipientId, regionId);
const citations = await MonitoringStandard.findAll({
include: [
From de3c27050552a1691ada9a37304d7d8cb566e12b Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Tue, 17 Dec 2024 11:37:38 -0500
Subject: [PATCH 135/198] fix BE issue and cleanup code
---
.../Pages/components/GoalPicker.js | 2 +-
src/goalServices/reduceGoals.ts | 47 ++++++-----
src/services/citations.ts | 80 +------------------
3 files changed, 28 insertions(+), 101 deletions(-)
diff --git a/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js b/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js
index c1588e432e..945bd976bb 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js
@@ -277,7 +277,7 @@ const GoalPicker = ({
setGrantsWithoutMonitoring([]);
}
}
- } else {
+ } else if (grantsWithoutMonitoring.length > 0) {
setGrantsWithoutMonitoring([]);
}
}, [goalForEditing,
diff --git a/src/goalServices/reduceGoals.ts b/src/goalServices/reduceGoals.ts
index 42a5ce2803..ef33572173 100644
--- a/src/goalServices/reduceGoals.ts
+++ b/src/goalServices/reduceGoals.ts
@@ -180,19 +180,21 @@ export function reduceObjectivesForActivityReport(
);
// Citations should return null not exists (every subsequent adding of objective).
- exists.citations = objective.activityReportObjectives
+ exists.citations = uniq(
+ objective.activityReportObjectives
&& objective.activityReportObjectives.length > 0
- ? uniq([
- ...exists.citations,
- ...objective.activityReportObjectives.flatMap(
- (aro) => aro.activityReportObjectiveCitations.map((c) => ({
- ...c.dataValues,
- id: c.monitoringReferences[0].standardId,
- name: `${c.monitoringReferences[0].acro} - ${c.citation} - ${c.monitoringReferences[0].findingSource}`,
- })),
- ),
- ])
- : [];
+ ? [
+ ...exists.citations,
+ ...objective.activityReportObjectives.flatMap(
+ (aro) => aro.activityReportObjectiveCitations.map((c) => ({
+ ...c.dataValues,
+ id: c.monitoringReferences[0].standardId,
+ name: `${c.monitoringReferences[0].acro} - ${c.citation} - ${c.monitoringReferences[0].findingSource}`,
+ })),
+ ),
+ ]
+ : [],
+ );
// Set to null if we don't have any citations.
if (exists.citations.length === 0) {
@@ -275,16 +277,17 @@ export function reduceObjectivesForActivityReport(
'course',
),
// Citations should return null if they are not applicable (first time we add the objective).
- citations: objective.activityReportObjectives
- && objective.activityReportObjectives.length > 0
- ? uniq(objective.activityReportObjectives.flatMap(
- (aro) => aro.activityReportObjectiveCitations.map((c) => ({
- ...c.dataValues,
- id: c.monitoringReferences[0].standardId,
- name: `${c.monitoringReferences[0].acro} - ${c.citation} - ${c.monitoringReferences[0].findingSource}`,
- })),
- ))
- : [],
+ citations:
+ uniq(objective.activityReportObjectives
+ && objective.activityReportObjectives.length > 0
+ ? objective.activityReportObjectives.flatMap(
+ (aro) => aro.activityReportObjectiveCitations.map((c) => ({
+ ...c.dataValues,
+ id: c.monitoringReferences[0].standardId,
+ name: `${c.monitoringReferences[0].acro} - ${c.citation} - ${c.monitoringReferences[0].findingSource}`,
+ })),
+ )
+ : []),
};
// If we have no citations set to null.
diff --git a/src/services/citations.ts b/src/services/citations.ts
index dbdc5fb187..53f1cba3b0 100644
--- a/src/services/citations.ts
+++ b/src/services/citations.ts
@@ -16,8 +16,8 @@ export async function textByCitation(
}
// TODO: Update this to the day we deploy to PROD.
-// const cutOffStartDate = new Date().toISOString().split('T')[0];
-const cutOffStartDate = '2021-01-01';
+const cutOffStartDate = new Date().toISOString().split('T')[0];
+// const cutOffStartDate = '2021-01-01';
/*
The purpose of this function is to get citations by grant id.
We then need to format the response for how it needs to be
@@ -46,82 +46,6 @@ export async function getCitationsByGrantIds(
grantIds: number[],
reportStartDate: string,
): Promise {
- console.log('\n\n\n---- before sql');
- console.log('\n\n-sql: ', `
- SELECT
- ms."standardId",
- ms.citation,
- JSONB_AGG( DISTINCT
- JSONB_BUILD_OBJECT(
- 'findingId', mf."findingId",
- 'grantId', gr.id,
- 'grantNumber', gr.number,
- 'reviewName', mfh."name",
- 'reportDeliveryDate', mfh."reportDeliveryDate",
- 'findingType', mf."findingType",
- 'findingSource', mf."source",
- 'monitoringFindingStatusName', mfs."name",
- 'reportDeliveryDate', mfh."reportDeliveryDate",
- 'citation', ms.citation,
- 'severity', CASE
- WHEN mf."findingType" = 'Deficiency' THEN 1
- WHEN mf."findingType" = 'Noncompliance' THEN 2
- ELSE 3
- END,
- 'acro', CASE
- WHEN mf."findingType" = 'Deficiency' THEN 'DEF'
- WHEN mf."findingType" = 'Noncompliance' THEN 'ANC'
- ELSE 'AOC'
- END
- )
- ) grants
- FROM "Grants" gr
- JOIN "Goals" g
- ON gr."id" = g."grantId"
- AND g."status" NOT IN ('Closed', 'Suspended')
- JOIN "GoalTemplates" gt
- ON g."goalTemplateId" = gt."id"
- AND gt."standard" = 'Monitoring'
- JOIN "MonitoringReviewGrantees" mrg
- ON gr.number = mrg."grantNumber"
- JOIN (
- -- The below 'DISTINCT ON' determines the single record to return values from by the 'ORDER BY' clause.
- SELECT DISTINCT ON (mfh."findingId", gr.id)
- mfh."findingId",
- gr.id AS "grantId",
- mr."reviewId",
- mr."name",
- mr."reportDeliveryDate"
- FROM "MonitoringFindingHistories" mfh
- JOIN "MonitoringReviews" mr
- ON mfh."reviewId" = mr."reviewId"
- JOIN "MonitoringReviewGrantees" mrg
- ON mrg."reviewId" = mr."reviewId"
- JOIN "Grants" gr
- ON gr.number = mrg."grantNumber"
- ORDER BY mfh."findingId", gr.id, mr."reportDeliveryDate" DESC
- ) mfh -- Subquery ensures only the most recent history for each finding-grant combination
- ON mfh."grantId" = gr.id
- JOIN "MonitoringFindings" mf
- ON mfh."findingId" = mf."findingId"
- JOIN "MonitoringFindingStatuses" mfs
- ON mf."statusId" = mfs."statusId"
- JOIN "MonitoringFindingStandards" mfs2
- ON mf."findingId" = mfs2."findingId"
- JOIN "MonitoringStandards" ms
- ON mfs2."standardId" = ms."standardId"
- JOIN "MonitoringFindingGrants" mfg
- ON mf."findingId" = mfg."findingId"
- AND mrg."granteeId" = mfg."granteeId"
- WHERE 1 = 1
- AND gr.id IN (${grantIds.join(',')}) -- :grantIds
- AND mfh."reportDeliveryDate"::date BETWEEN '${cutOffStartDate}' AND '${reportStartDate}'
- AND gr.status = 'Active'
- AND mfs.name = 'Active'
- GROUP BY 1,2
- ORDER BY 2,1;
- `);
-
// Query to get the citations by grant id.
const grantsByCitations = await sequelize.query(
/* sql */
From d411afbf06cb24781659f6abfca3ad1fe82c804b Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Tue, 17 Dec 2024 12:10:32 -0500
Subject: [PATCH 136/198] fix if current of previous objective doesnt have
citations
---
src/goalServices/reduceGoals.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/goalServices/reduceGoals.ts b/src/goalServices/reduceGoals.ts
index ef33572173..314098f49e 100644
--- a/src/goalServices/reduceGoals.ts
+++ b/src/goalServices/reduceGoals.ts
@@ -184,7 +184,7 @@ export function reduceObjectivesForActivityReport(
objective.activityReportObjectives
&& objective.activityReportObjectives.length > 0
? [
- ...exists.citations,
+ ...exists.citations || [],
...objective.activityReportObjectives.flatMap(
(aro) => aro.activityReportObjectiveCitations.map((c) => ({
...c.dataValues,
From 9e43a1e83af547ae9c943792495e88a158f5b435 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Tue, 17 Dec 2024 13:03:10 -0500
Subject: [PATCH 137/198] fix test
---
src/goalServices/reduceGoals.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/goalServices/reduceGoals.ts b/src/goalServices/reduceGoals.ts
index 314098f49e..ac56793f11 100644
--- a/src/goalServices/reduceGoals.ts
+++ b/src/goalServices/reduceGoals.ts
@@ -186,7 +186,7 @@ export function reduceObjectivesForActivityReport(
? [
...exists.citations || [],
...objective.activityReportObjectives.flatMap(
- (aro) => aro.activityReportObjectiveCitations.map((c) => ({
+ (aro) => (aro.activityReportObjectiveCitations || []).map((c) => ({
...c.dataValues,
id: c.monitoringReferences[0].standardId,
name: `${c.monitoringReferences[0].acro} - ${c.citation} - ${c.monitoringReferences[0].findingSource}`,
From 6b3063392b6e0127255b90e2dec567985370dcae Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Tue, 17 Dec 2024 14:39:22 -0500
Subject: [PATCH 138/198] fix weird pop up citations on objective
---
.../pages/ActivityReport/Pages/components/GoalForm.js | 4 ++++
.../pages/ActivityReport/Pages/components/GoalPicker.js | 1 +
.../pages/ActivityReport/Pages/components/Objective.js | 7 +++++--
.../pages/ActivityReport/Pages/components/Objectives.js | 4 ++++
.../Pages/components/__tests__/Objective.js | 9 ++++++++-
5 files changed, 22 insertions(+), 3 deletions(-)
diff --git a/frontend/src/pages/ActivityReport/Pages/components/GoalForm.js b/frontend/src/pages/ActivityReport/Pages/components/GoalForm.js
index 1ae7119414..43adc96e95 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/GoalForm.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/GoalForm.js
@@ -30,6 +30,7 @@ export default function GoalForm({
isMultiRecipientReport,
citationOptions,
rawCitations,
+ isMonitoringGoal,
}) {
// pull the errors out of the form context
const { errors, watch } = useFormContext();
@@ -224,6 +225,7 @@ export default function GoalForm({
reportId={parseInt(reportId, DECIMAL_BASE)}
citationOptions={citationOptions}
rawCitations={rawCitations}
+ isMonitoringGoal={isMonitoringGoal}
/>
>
);
@@ -262,6 +264,7 @@ GoalForm.propTypes = {
value: PropTypes.number,
label: PropTypes.string,
})),
+ isMonitoringGoal: PropTypes.bool,
rawCitations: PropTypes.arrayOf(PropTypes.shape({
standardId: PropTypes.number,
citation: PropTypes.string,
@@ -292,4 +295,5 @@ GoalForm.defaultProps = {
isMultiRecipientReport: false,
citationOptions: [],
rawCitations: [],
+ isMonitoringGoal: false,
};
diff --git a/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js b/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js
index 945bd976bb..235f230e98 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js
@@ -382,6 +382,7 @@ const GoalPicker = ({
isMultiRecipientReport={isMultiRecipientReport}
citationOptions={citationOptions}
rawCitations={rawCitations}
+ isMonitoringGoal={isMonitoringGoal}
/>
) : null}
diff --git a/frontend/src/pages/ActivityReport/Pages/components/Objective.js b/frontend/src/pages/ActivityReport/Pages/components/Objective.js
index 5ca19f0d6b..57c2c3ebda 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/Objective.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/Objective.js
@@ -49,6 +49,7 @@ export default function Objective({
reportId,
citationOptions,
rawCitations,
+ isMonitoringGoal,
}) {
const modalRef = useRef();
@@ -440,7 +441,7 @@ export default function Objective({
/>
{
- citationOptions.length > 0 && citationWarnings.length > 0 && (
+ isMonitoringGoal && citationWarnings.length > 0 && (
@@ -474,7 +475,7 @@ export default function Objective({
)
}
{
- citationOptions.length > 0 && (
+ isMonitoringGoal && (
0;
@@ -141,6 +142,7 @@ export default function Objectives({
reportId={reportId}
citationOptions={citationOptions}
rawCitations={rawCitations}
+ isMonitoringGoal={isMonitoringGoal}
/>
);
})}
@@ -158,6 +160,7 @@ Objectives.propTypes = {
value: PropTypes.number,
label: PropTypes.string,
})),
+ isMonitoringGoal: PropTypes.bool,
rawCitations: PropTypes.arrayOf(PropTypes.shape({
standardId: PropTypes.number,
citation: PropTypes.string,
@@ -180,4 +183,5 @@ Objectives.propTypes = {
Objectives.defaultProps = {
citationOptions: [],
rawCitations: [],
+ isMonitoringGoal: false,
};
diff --git a/frontend/src/pages/ActivityReport/Pages/components/__tests__/Objective.js b/frontend/src/pages/ActivityReport/Pages/components/__tests__/Objective.js
index cb96cba499..9297a7a0c9 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/__tests__/Objective.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/__tests__/Objective.js
@@ -64,6 +64,7 @@ const RenderObjective = ({
citationOptions = [],
rawCitations = [],
additionalHookFormData = {},
+ isMonitoringGoal = false,
}) => {
const hookForm = useForm({
mode: 'onBlur',
@@ -102,6 +103,7 @@ const RenderObjective = ({
topicOptions={[]}
citationOptions={citationOptions}
rawCitations={rawCitations}
+ isMonitoringGoal={isMonitoringGoal}
options={[
{
label: 'Create a new objective',
@@ -188,7 +190,11 @@ describe('Objective', () => {
}],
}];
- render( );
+ render( );
const helpButton = screen.getByRole('button', { name: /get help choosing citation/i });
expect(helpButton).toBeVisible();
const citationsButton = screen.getByRole('button', { name: /Citation/i });
@@ -340,6 +346,7 @@ describe('Objective', () => {
citationOptions={citationOptions}
rawCitations={rawCitations}
additionalHookFormData={additionalHookFormData}
+ isMonitoringGoal
/>);
const helpButton = screen.getByRole('button', { name: /get help choosing citation/i });
From 7c5a1e579ffc977ee3bd6772b72329998a8dc1ba Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Tue, 17 Dec 2024 14:51:36 -0500
Subject: [PATCH 139/198] remove console logs
---
src/services/citations.ts | 76 ---------------------------------------
1 file changed, 76 deletions(-)
diff --git a/src/services/citations.ts b/src/services/citations.ts
index dbdc5fb187..46db9a99d3 100644
--- a/src/services/citations.ts
+++ b/src/services/citations.ts
@@ -46,82 +46,6 @@ export async function getCitationsByGrantIds(
grantIds: number[],
reportStartDate: string,
): Promise {
- console.log('\n\n\n---- before sql');
- console.log('\n\n-sql: ', `
- SELECT
- ms."standardId",
- ms.citation,
- JSONB_AGG( DISTINCT
- JSONB_BUILD_OBJECT(
- 'findingId', mf."findingId",
- 'grantId', gr.id,
- 'grantNumber', gr.number,
- 'reviewName', mfh."name",
- 'reportDeliveryDate', mfh."reportDeliveryDate",
- 'findingType', mf."findingType",
- 'findingSource', mf."source",
- 'monitoringFindingStatusName', mfs."name",
- 'reportDeliveryDate', mfh."reportDeliveryDate",
- 'citation', ms.citation,
- 'severity', CASE
- WHEN mf."findingType" = 'Deficiency' THEN 1
- WHEN mf."findingType" = 'Noncompliance' THEN 2
- ELSE 3
- END,
- 'acro', CASE
- WHEN mf."findingType" = 'Deficiency' THEN 'DEF'
- WHEN mf."findingType" = 'Noncompliance' THEN 'ANC'
- ELSE 'AOC'
- END
- )
- ) grants
- FROM "Grants" gr
- JOIN "Goals" g
- ON gr."id" = g."grantId"
- AND g."status" NOT IN ('Closed', 'Suspended')
- JOIN "GoalTemplates" gt
- ON g."goalTemplateId" = gt."id"
- AND gt."standard" = 'Monitoring'
- JOIN "MonitoringReviewGrantees" mrg
- ON gr.number = mrg."grantNumber"
- JOIN (
- -- The below 'DISTINCT ON' determines the single record to return values from by the 'ORDER BY' clause.
- SELECT DISTINCT ON (mfh."findingId", gr.id)
- mfh."findingId",
- gr.id AS "grantId",
- mr."reviewId",
- mr."name",
- mr."reportDeliveryDate"
- FROM "MonitoringFindingHistories" mfh
- JOIN "MonitoringReviews" mr
- ON mfh."reviewId" = mr."reviewId"
- JOIN "MonitoringReviewGrantees" mrg
- ON mrg."reviewId" = mr."reviewId"
- JOIN "Grants" gr
- ON gr.number = mrg."grantNumber"
- ORDER BY mfh."findingId", gr.id, mr."reportDeliveryDate" DESC
- ) mfh -- Subquery ensures only the most recent history for each finding-grant combination
- ON mfh."grantId" = gr.id
- JOIN "MonitoringFindings" mf
- ON mfh."findingId" = mf."findingId"
- JOIN "MonitoringFindingStatuses" mfs
- ON mf."statusId" = mfs."statusId"
- JOIN "MonitoringFindingStandards" mfs2
- ON mf."findingId" = mfs2."findingId"
- JOIN "MonitoringStandards" ms
- ON mfs2."standardId" = ms."standardId"
- JOIN "MonitoringFindingGrants" mfg
- ON mf."findingId" = mfg."findingId"
- AND mrg."granteeId" = mfg."granteeId"
- WHERE 1 = 1
- AND gr.id IN (${grantIds.join(',')}) -- :grantIds
- AND mfh."reportDeliveryDate"::date BETWEEN '${cutOffStartDate}' AND '${reportStartDate}'
- AND gr.status = 'Active'
- AND mfs.name = 'Active'
- GROUP BY 1,2
- ORDER BY 2,1;
- `);
-
// Query to get the citations by grant id.
const grantsByCitations = await sequelize.query(
/* sql */
From 81a91b9aa0e846688567b4a14b536eaf9a927dcb Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Tue, 17 Dec 2024 14:59:54 -0500
Subject: [PATCH 140/198] prevent submit
---
.../src/pages/ActivityReport/Pages/Review/Submitter/Draft.js | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/frontend/src/pages/ActivityReport/Pages/Review/Submitter/Draft.js b/frontend/src/pages/ActivityReport/Pages/Review/Submitter/Draft.js
index 8c5cd85b76..104b371624 100644
--- a/frontend/src/pages/ActivityReport/Pages/Review/Submitter/Draft.js
+++ b/frontend/src/pages/ActivityReport/Pages/Review/Submitter/Draft.js
@@ -78,7 +78,10 @@ const Draft = ({
};
const onSubmit = (e) => {
- if (allGoalsHavePromptResponses && !hasIncompletePages && !grantsMissingMonitoring.length) {
+ if (allGoalsHavePromptResponses
+ && !hasIncompletePages
+ && !grantsMissingMonitoring.length
+ && !grantsMissingCitations.length) {
onFormSubmit(e);
updatedJustSubmitted(true);
}
From f0b292a2c396959e20668e5924b862c39c26f956 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Tue, 17 Dec 2024 15:48:00 -0500
Subject: [PATCH 141/198] cleanup per Matt
---
.../ActivityReport/Pages/Review/Submitter/Draft.js | 10 ++++++----
.../pages/ActivityReport/Pages/components/Objective.js | 1 -
2 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/frontend/src/pages/ActivityReport/Pages/Review/Submitter/Draft.js b/frontend/src/pages/ActivityReport/Pages/Review/Submitter/Draft.js
index 104b371624..ded8e3ac5b 100644
--- a/frontend/src/pages/ActivityReport/Pages/Review/Submitter/Draft.js
+++ b/frontend/src/pages/ActivityReport/Pages/Review/Submitter/Draft.js
@@ -77,11 +77,13 @@ const Draft = ({
return completeRoleList.sort();
};
+ const canSubmitReport = allGoalsHavePromptResponses
+ && !hasIncompletePages
+ && !grantsMissingMonitoring.length
+ && !grantsMissingCitations.length;
+
const onSubmit = (e) => {
- if (allGoalsHavePromptResponses
- && !hasIncompletePages
- && !grantsMissingMonitoring.length
- && !grantsMissingCitations.length) {
+ if (canSubmitReport) {
onFormSubmit(e);
updatedJustSubmitted(true);
}
diff --git a/frontend/src/pages/ActivityReport/Pages/components/Objective.js b/frontend/src/pages/ActivityReport/Pages/components/Objective.js
index 57c2c3ebda..68d141bd44 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/Objective.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/Objective.js
@@ -1,6 +1,5 @@
import React, {
useState, useContext, useRef,
- // useEffect,
} from 'react';
import useDeepCompareEffect from 'use-deep-compare-effect';
import PropTypes from 'prop-types';
From ac32d480315b791c5d1d0bc19d6e99dc5ad2d15f Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Wed, 18 Dec 2024 11:54:25 -0500
Subject: [PATCH 142/198] update test w citations
---
src/goalServices/reduceGoals.test.ts | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/goalServices/reduceGoals.test.ts b/src/goalServices/reduceGoals.test.ts
index 7efe77595e..22e4a0e980 100644
--- a/src/goalServices/reduceGoals.test.ts
+++ b/src/goalServices/reduceGoals.test.ts
@@ -206,6 +206,7 @@ describe('reduceGoals', () => {
activityReportObjectiveTopics: [],
activityReportObjectiveCourses: [],
activityReportObjectiveFiles: [],
+ activityReportObjectiveCitations: [],
},
],
},
@@ -223,6 +224,7 @@ describe('reduceGoals', () => {
resources: [],
files: [],
courses: [],
+ citations: [],
},
];
From 4dee22fbd572d0f8d9fb75b40c30c5890a8c9cd1 Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Wed, 18 Dec 2024 14:30:57 -0500
Subject: [PATCH 143/198] Fix date formats
---
src/services/monitoring.ts | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/src/services/monitoring.ts b/src/services/monitoring.ts
index 62ca354ec1..52a0be0ff7 100644
--- a/src/services/monitoring.ts
+++ b/src/services/monitoring.ts
@@ -2,7 +2,7 @@
import { Op } from 'sequelize';
import moment from 'moment';
import { uniq, uniqBy } from 'lodash';
-import db, { sequelize } from '../models';
+import db from '../models';
import {
ITTAByReviewResponse,
IMonitoringReview,
@@ -353,8 +353,8 @@ export async function ttaByReviews(
const objectives = citationsOnActivityReports.filter((c) => c.findingIds.includes(findingId));
objectives.forEach(({ endDate }) => {
- if (!lastTTADate || moment(endDate, 'MM/DD/YYYY').isAfter(lastTTADate)) {
- lastTTADate = moment(endDate, 'MM/DD/YYYY');
+ if (!lastTTADate || moment(endDate, 'YYYY-MM-DD').isAfter(lastTTADate)) {
+ lastTTADate = moment(endDate, 'YYYY-MM-DD');
}
specialists = specialists.concat(objectives.map((o) => o.specialists).flat());
});
@@ -363,7 +363,7 @@ export async function ttaByReviews(
citation,
status,
findingType: finding.findingType,
- correctionDeadline: finding.correctionDeadLine || '',
+ correctionDeadline: finding.correctionDeadLine ? moment(finding.correctionDeadLine).format('MM/DD/YYYY') : '',
category: finding.source,
objectives,
});
@@ -515,8 +515,8 @@ export async function ttaByCitations(
const objectives = citationsOnActivityReports.filter((c) => c.findingIds.includes(finding.findingId));
objectives.forEach(({ endDate }) => {
- if (!lastTTADate || moment(endDate, 'MM/DD/YYYY').isAfter(lastTTADate)) {
- lastTTADate = moment(endDate, 'MM/DD/YYYY');
+ if (!lastTTADate || moment(endDate, 'YYYY-MM-DD').isAfter(lastTTADate)) {
+ lastTTADate = moment(endDate, 'YYYY-MM-DD');
}
});
From ad3677565bc319123ea5395ed2180080681fb266 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Wed, 18 Dec 2024 16:56:02 -0500
Subject: [PATCH 144/198] fix dates
---
src/services/monitoring.ts | 12 ++++++++----
1 file changed, 8 insertions(+), 4 deletions(-)
diff --git a/src/services/monitoring.ts b/src/services/monitoring.ts
index 52a0be0ff7..86b6fb391a 100644
--- a/src/services/monitoring.ts
+++ b/src/services/monitoring.ts
@@ -2,6 +2,7 @@
import { Op } from 'sequelize';
import moment from 'moment';
import { uniq, uniqBy } from 'lodash';
+import { REPORT_STATUSES } from '@ttahub/common';
import db from '../models';
import {
ITTAByReviewResponse,
@@ -103,6 +104,9 @@ async function aroCitationsByGrantNumbers(grantNumbers: string[]): Promise c.findingIds.includes(findingId));
objectives.forEach(({ endDate }) => {
- if (!lastTTADate || moment(endDate, 'YYYY-MM-DD').isAfter(lastTTADate)) {
- lastTTADate = moment(endDate, 'YYYY-MM-DD');
+ if (!lastTTADate || moment(endDate, 'MM/DD/YYYY').isAfter(lastTTADate)) {
+ lastTTADate = moment(endDate, 'MM/DD/YYYY');
}
specialists = specialists.concat(objectives.map((o) => o.specialists).flat());
});
@@ -515,8 +519,8 @@ export async function ttaByCitations(
const objectives = citationsOnActivityReports.filter((c) => c.findingIds.includes(finding.findingId));
objectives.forEach(({ endDate }) => {
- if (!lastTTADate || moment(endDate, 'YYYY-MM-DD').isAfter(lastTTADate)) {
- lastTTADate = moment(endDate, 'YYYY-MM-DD');
+ if (!lastTTADate || moment(endDate, 'MM/DD/YYYY').isAfter(lastTTADate)) {
+ lastTTADate = moment(endDate, 'MM/DD/YYYY');
}
});
From 9b9c03087e2d1fbf1e0148b978b4f7238daefeff Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Thu, 19 Dec 2024 09:18:20 -0500
Subject: [PATCH 145/198] per Matt add supsended to reactivate
---
src/tools/createMonitoringGoals.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/tools/createMonitoringGoals.js b/src/tools/createMonitoringGoals.js
index c5e8208ff2..ab9d34ec8f 100644
--- a/src/tools/createMonitoringGoals.js
+++ b/src/tools/createMonitoringGoals.js
@@ -138,7 +138,7 @@ const createMonitoringGoals = async () => {
'FA-2', 'FA2-CR',
'Special'
)
- AND g.status = 'Closed'
+ AND g.status IN ('Closed', 'Suspended')
GROUP BY 1
)
UPDATE "Goals"
From ab4223b9419c5e692cfc06090cd0bcd2ae2a3ceb Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Thu, 19 Dec 2024 10:47:48 -0500
Subject: [PATCH 146/198] fix cleanup bug and existing bug that created extra
objectives
---
frontend/src/pages/ActivityReport/formDataHelpers.js | 4 ++++
src/models/hooks/activityReportObjective.js | 1 +
2 files changed, 5 insertions(+)
diff --git a/frontend/src/pages/ActivityReport/formDataHelpers.js b/frontend/src/pages/ActivityReport/formDataHelpers.js
index 126b08f084..2d47b5d593 100644
--- a/frontend/src/pages/ActivityReport/formDataHelpers.js
+++ b/frontend/src/pages/ActivityReport/formDataHelpers.js
@@ -133,6 +133,8 @@ export const packageGoals = (goals, goal, grantIds, prompts) => {
closeSuspendReason: objective.closeSuspendReason,
closeSuspendContext: objective.closeSuspendContext,
createdHere: objective.createdHere,
+ // eslint-disable-next-line max-len
+ goalId: g.id, // DO NOT REMOVE: This is required so we don't duplicate objectives when we update text on AR's.
})),
})),
];
@@ -163,6 +165,8 @@ export const packageGoals = (goals, goal, grantIds, prompts) => {
closeSuspendReason: objective.closeSuspendReason,
closeSuspendContext: objective.closeSuspendContext,
createdHere: objective.createdHere,
+ // eslint-disable-next-line max-len
+ goalId: goal.id, // DO NOT REMOVE: This is required so we don't duplicate objectives when we update text on AR's.
})),
grantIds,
prompts: grantIds.length < 2 ? prompts : [],
diff --git a/src/models/hooks/activityReportObjective.js b/src/models/hooks/activityReportObjective.js
index 5da1aebcb0..e4c5e060d5 100644
--- a/src/models/hooks/activityReportObjective.js
+++ b/src/models/hooks/activityReportObjective.js
@@ -11,6 +11,7 @@ const propagateDestroyToMetadata = async (sequelize, instance, options) => Promi
sequelize.models.ActivityReportObjectiveResource,
sequelize.models.ActivityReportObjectiveTopic,
sequelize.models.ActivityReportObjectiveCourse,
+ sequelize.models.ActivityReportObjectiveCitation,
].map(async (model) => model.destroy({
where: {
activityReportObjectiveId: instance.id,
From 459129e19124536d3d4e605b74458da1358e53bd Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Thu, 19 Dec 2024 13:34:08 -0500
Subject: [PATCH 147/198] Add additional test coverage
---
src/services/monitoring.testHelpers.ts | 79 +++++++++++++++++++-------
src/services/monitoring.ts | 2 +
src/services/ttaByCitation.test.js | 52 +++++++++++++----
src/services/ttaByReview.test.js | 56 ++++++++++++++----
4 files changed, 149 insertions(+), 40 deletions(-)
diff --git a/src/services/monitoring.testHelpers.ts b/src/services/monitoring.testHelpers.ts
index 224493b9c6..a3589997a5 100644
--- a/src/services/monitoring.testHelpers.ts
+++ b/src/services/monitoring.testHelpers.ts
@@ -297,25 +297,48 @@ async function createReportAndCitationData(grantNumber: string, findingId: strin
});
const goal = await createGoal({ grantId: grant.id, status: GOAL_STATUS.IN_PROGRESS });
- const objective = await Objective.create({
+ const objectiveOne = await Objective.create({
goalId: goal.id,
title: 'Objective Title',
status: OBJECTIVE_STATUS.IN_PROGRESS,
});
- const report = await createReport({
+ const objectiveTwo = await Objective.create({
+ goalId: goal.id,
+ title: 'Objective Title Two',
+ status: OBJECTIVE_STATUS.IN_PROGRESS,
+ });
+
+ const reportOne = await createReport({
+ activityRecipients: [{ grantId: grant.id }],
+ regionId: grant.regionId,
+ userId: 1,
+ });
+
+ const aroOne = await ActivityReportObjective.create({
+ activityReportId: reportOne.id,
+ objectiveId: objectiveOne.id,
+ });
+
+ await ActivityReportCollaborator.create({
+ activityReportId: reportOne.id,
+ userId: 1,
+ });
+
+ const reportTwo = await createReport({
activityRecipients: [{ grantId: grant.id }],
regionId: grant.regionId,
userId: 1,
+ endDate: new Date(),
});
- const aro = await ActivityReportObjective.create({
- activityReportId: report.id,
- objectiveId: objective.id,
+ const aroTwo = await ActivityReportObjective.create({
+ activityReportId: reportTwo.id,
+ objectiveId: objectiveTwo.id,
});
await ActivityReportCollaborator.create({
- activityReportId: report.id,
+ activityReportId: reportTwo.id,
userId: 1,
});
@@ -324,12 +347,27 @@ async function createReportAndCitationData(grantNumber: string, findingId: strin
});
await ActivityReportObjectiveTopic.create({
- activityReportObjectiveId: aro.id,
+ activityReportObjectiveId: aroOne.id,
topicId: topic.id,
});
- const citation = await ActivityReportObjectiveCitation.create({
- activityReportObjectiveId: aro.id,
+ await ActivityReportObjectiveTopic.create({
+ activityReportObjectiveId: aroTwo.id,
+ topicId: topic.id,
+ });
+
+ const citationOne = await ActivityReportObjectiveCitation.create({
+ activityReportObjectiveId: aroOne.id,
+ citation: 'Citation',
+ monitoringReferences: [{
+ findingId,
+ grantNumber,
+ reviewName: 'REVIEW!!!',
+ }],
+ });
+
+ const citationTwo = await ActivityReportObjectiveCitation.create({
+ activityReportObjectiveId: aroTwo.id,
citation: 'Citation',
monitoringReferences: [{
findingId,
@@ -340,22 +378,22 @@ async function createReportAndCitationData(grantNumber: string, findingId: strin
return {
goal,
- objective,
- report,
+ objectives: [objectiveOne, objectiveTwo],
+ reports: [reportOne, reportTwo],
topic,
- citation,
+ citations: [citationOne, citationTwo],
};
}
async function destroyReportAndCitationData(
goal:{ id: number },
- objective: { id: number },
- report: { id: number },
+ objectives: { id: number }[],
+ reports: { id: number }[],
topic: { id: number },
- citation: { id: number },
+ citations: { id: number }[],
) {
await ActivityReportObjectiveCitation.destroy({
- where: { id: citation.id },
+ where: { id: citations.map((c) => c.id) },
force: true,
individualHooks: true,
});
@@ -373,20 +411,21 @@ async function destroyReportAndCitationData(
});
await ActivityReportCollaborator.destroy({
- where: { activityReportId: report.id },
+ where: { activityReportId: reports.map((r) => r.id) },
force: true,
individualHooks: true,
});
await ActivityReportObjective.destroy({
- where: { objectiveId: objective.id },
+ where: { objectiveId: objectives.map((o) => o.id) },
force: true,
individualHooks: true,
});
- await destroyReport(report);
+ await Promise.all(reports.map((report) => destroyReport(report)));
+
await Objective.destroy({
- where: { id: objective.id },
+ where: { id: objectives.map((o) => o.id) },
force: true,
individualHooks: true,
});
diff --git a/src/services/monitoring.ts b/src/services/monitoring.ts
index 86b6fb391a..b55a8e6067 100644
--- a/src/services/monitoring.ts
+++ b/src/services/monitoring.ts
@@ -398,6 +398,8 @@ export async function ttaByCitations(
granteeIds,
} = await extractExternalData(recipientId, regionId);
+ console.log(citationsOnActivityReports);
+
const citations = await MonitoringStandard.findAll({
include: [
{
diff --git a/src/services/ttaByCitation.test.js b/src/services/ttaByCitation.test.js
index 89b851040b..08e464475c 100644
--- a/src/services/ttaByCitation.test.js
+++ b/src/services/ttaByCitation.test.js
@@ -1,3 +1,4 @@
+import moment from 'moment';
import { ttaByCitations } from './monitoring';
import {
createAdditionalMonitoringData,
@@ -26,10 +27,10 @@ describe('ttaByCitations', () => {
let reviewId;
let goal;
- let objective;
- let report;
+ let objectives;
+ let reports;
let topic;
- let citation;
+ let citations;
beforeAll(async () => {
await Recipient.findOrCreate({
@@ -74,10 +75,10 @@ describe('ttaByCitations', () => {
);
goal = arocResult.goal;
- objective = arocResult.objective;
- report = arocResult.report;
+ objectives = arocResult.objectives;
+ reports = arocResult.reports;
topic = arocResult.topic;
- citation = arocResult.citation;
+ citations = arocResult.citations;
});
afterAll(async () => {
@@ -85,10 +86,10 @@ describe('ttaByCitations', () => {
await destroyAdditionalMonitoringData(findingId, reviewId);
await destroyReportAndCitationData(
goal,
- objective,
- report,
+ objectives,
+ reports,
topic,
- citation,
+ citations,
);
await GrantNumberLink.destroy({ where: { grantNumber: GRANT_NUMBER }, force: true });
@@ -111,7 +112,7 @@ describe('ttaByCitations', () => {
grantNumbers: [
'01HP044446',
],
- lastTTADate: '01/01/2021',
+ lastTTADate: moment().format('MM/DD/YYYY'),
reviews: [
{
findingStatus: 'Complete',
@@ -148,6 +149,37 @@ describe('ttaByCitations', () => {
'Spleunking',
],
},
+ {
+ activityReports: [
+ {
+ displayId: expect.any(String),
+ id: expect.any(Number),
+ },
+ ],
+ endDate: moment().format('MM/DD/YYYY'),
+ findingIds: [
+ findingId,
+ ],
+ grantNumber: GRANT_NUMBER,
+ reviewNames: [
+ 'REVIEW!!!',
+ ],
+ specialists: [
+ {
+ name: 'Hermione Granger, SS',
+ roles: ['SS'],
+ },
+ {
+ name: 'Hermione Granger, SS',
+ roles: ['SS'],
+ },
+ ],
+ status: OBJECTIVE_STATUS.IN_PROGRESS,
+ title: 'Objective Title Two',
+ topics: [
+ 'Spleunking',
+ ],
+ },
],
outcome: 'Complete',
reviewReceived: '02/22/2023',
diff --git a/src/services/ttaByReview.test.js b/src/services/ttaByReview.test.js
index 14996c0388..58199f41e6 100644
--- a/src/services/ttaByReview.test.js
+++ b/src/services/ttaByReview.test.js
@@ -1,3 +1,4 @@
+import moment from 'moment';
import {
createAdditionalMonitoringData,
createMonitoringData,
@@ -25,10 +26,10 @@ describe('ttaByReviews', () => {
let reviewId;
let goal;
- let objective;
- let report;
+ let objectives;
+ let reports;
let topic;
- let citation;
+ let citations;
beforeAll(async () => {
await Recipient.findOrCreate({
@@ -71,10 +72,10 @@ describe('ttaByReviews', () => {
);
goal = arocResult.goal;
- objective = arocResult.objective;
- report = arocResult.report;
+ objectives = arocResult.objectives;
+ reports = arocResult.reports;
topic = arocResult.topic;
- citation = arocResult.citation;
+ citations = arocResult.citations;
});
afterAll(async () => {
@@ -83,10 +84,10 @@ describe('ttaByReviews', () => {
await destroyReportAndCitationData(
goal,
- objective,
- report,
+ objectives,
+ reports,
topic,
- citation,
+ citations,
);
await GrantNumberLink.destroy({ where: { grantNumber: GRANT_NUMBER }, force: true });
@@ -143,6 +144,41 @@ describe('ttaByReviews', () => {
'Spleunking',
],
},
+ {
+ activityReports: [
+ {
+ displayId: expect.any(String),
+ id: expect.any(Number),
+ },
+ ],
+ endDate: moment().format('MM/DD/YYYY'),
+ findingIds: [
+ findingId,
+ ],
+ grantNumber: '01HP044446',
+ reviewNames: [
+ 'REVIEW!!!',
+ ],
+ specialists: [
+ {
+ name: 'Hermione Granger, SS',
+ roles: [
+ 'SS',
+ ],
+ },
+ {
+ name: 'Hermione Granger, SS',
+ roles: [
+ 'SS',
+ ],
+ },
+ ],
+ status: 'In Progress',
+ title: 'Objective Title Two',
+ topics: [
+ 'Spleunking',
+ ],
+ },
],
status: 'Complete',
},
@@ -151,7 +187,7 @@ describe('ttaByReviews', () => {
'01HP044446',
],
id: expect.any(Number),
- lastTTADate: '01/01/2021',
+ lastTTADate: moment().format('MM/DD/YYYY'),
name: 'REVIEW!!!',
outcome: 'Complete',
reviewReceived: '02/22/2023',
From 5fd77788662a90c2c83f625d33f4cab6699da757 Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Thu, 19 Dec 2024 13:36:05 -0500
Subject: [PATCH 148/198] Whoops remove console
---
src/services/monitoring.ts | 2 --
1 file changed, 2 deletions(-)
diff --git a/src/services/monitoring.ts b/src/services/monitoring.ts
index b55a8e6067..86b6fb391a 100644
--- a/src/services/monitoring.ts
+++ b/src/services/monitoring.ts
@@ -398,8 +398,6 @@ export async function ttaByCitations(
granteeIds,
} = await extractExternalData(recipientId, regionId);
- console.log(citationsOnActivityReports);
-
const citations = await MonitoringStandard.findAll({
include: [
{
From 5588a31c7d7a3787ae961228be7161b24f4a0c4e Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Thu, 19 Dec 2024 14:39:13 -0500
Subject: [PATCH 149/198] Fix test order problem
---
src/services/ttaByCitation.test.js | 10 +++++-----
src/services/ttaByReview.test.js | 8 ++++----
2 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/src/services/ttaByCitation.test.js b/src/services/ttaByCitation.test.js
index 08e464475c..b89c2ddcab 100644
--- a/src/services/ttaByCitation.test.js
+++ b/src/services/ttaByCitation.test.js
@@ -112,7 +112,7 @@ describe('ttaByCitations', () => {
grantNumbers: [
'01HP044446',
],
- lastTTADate: moment().format('MM/DD/YYYY'),
+ lastTTADate: expect.any(String),
reviews: [
{
findingStatus: 'Complete',
@@ -125,7 +125,7 @@ describe('ttaByCitations', () => {
id: expect.any(Number),
},
],
- endDate: '01/01/2021',
+ endDate: expect.any(String),
findingIds: [
findingId,
],
@@ -144,7 +144,7 @@ describe('ttaByCitations', () => {
},
],
status: OBJECTIVE_STATUS.IN_PROGRESS,
- title: 'Objective Title',
+ title: expect.any(String),
topics: [
'Spleunking',
],
@@ -156,7 +156,7 @@ describe('ttaByCitations', () => {
id: expect.any(Number),
},
],
- endDate: moment().format('MM/DD/YYYY'),
+ endDate: expect.any(String),
findingIds: [
findingId,
],
@@ -175,7 +175,7 @@ describe('ttaByCitations', () => {
},
],
status: OBJECTIVE_STATUS.IN_PROGRESS,
- title: 'Objective Title Two',
+ title: expect.any(String),
topics: [
'Spleunking',
],
diff --git a/src/services/ttaByReview.test.js b/src/services/ttaByReview.test.js
index 58199f41e6..1f814b0825 100644
--- a/src/services/ttaByReview.test.js
+++ b/src/services/ttaByReview.test.js
@@ -116,7 +116,7 @@ describe('ttaByReviews', () => {
id: expect.any(Number),
},
],
- endDate: '01/01/2021',
+ endDate: expect.any(String),
findingIds: [
findingId,
],
@@ -139,7 +139,7 @@ describe('ttaByReviews', () => {
},
],
status: 'In Progress',
- title: 'Objective Title',
+ title: expect.any(String),
topics: [
'Spleunking',
],
@@ -151,7 +151,7 @@ describe('ttaByReviews', () => {
id: expect.any(Number),
},
],
- endDate: moment().format('MM/DD/YYYY'),
+ endDate: expect.any(String),
findingIds: [
findingId,
],
@@ -174,7 +174,7 @@ describe('ttaByReviews', () => {
},
],
status: 'In Progress',
- title: 'Objective Title Two',
+ title: expect.any(String),
topics: [
'Spleunking',
],
From 3959e29d06af8eba7c8d89e3736c3f0afbecb249 Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Thu, 19 Dec 2024 14:46:16 -0500
Subject: [PATCH 150/198] add users policy test
---
src/policies/user.test.js | 41 +++++++++++++++++++++++++++++++++++++++
1 file changed, 41 insertions(+)
diff --git a/src/policies/user.test.js b/src/policies/user.test.js
index cb97bd2ad0..e23b0cff82 100644
--- a/src/policies/user.test.js
+++ b/src/policies/user.test.js
@@ -196,6 +196,47 @@ describe('User policies', () => {
});
});
+ describe('canViewCitationsInRegion', () => {
+ const writeUser = {
+ permissions: [{
+ regionId: 1,
+ scopeId: SCOPES.READ_WRITE_REPORTS,
+ }],
+ };
+ const approveUser = {
+ permissions: [{
+ regionId: 1,
+ scopeId: SCOPES.APPROVE_REPORTS,
+ }],
+ };
+ const readUser = {
+ permissions: [{
+ regionId: 1,
+ scopeId: SCOPES.READ_REPORTS,
+ }],
+ };
+
+ it('is true if the user has read/write permissions', () => {
+ const policy = new User(writeUser);
+ expect(policy.canViewCitationsInRegion(1)).toBeTruthy();
+ });
+
+ it('is true if the user has read permissions', () => {
+ const policy = new User(readUser);
+ expect(policy.canViewCitationsInRegion(1)).toBeTruthy();
+ });
+
+ it('is true if the user has approve permissions', () => {
+ const policy = new User(approveUser);
+ expect(policy.canViewCitationsInRegion(1)).toBeTruthy();
+ });
+
+ it('is false if the user does not have read/write permissions', () => {
+ const policy = new User(writeUser);
+ expect(policy.canViewCitationsInRegion(2)).toBeFalsy();
+ });
+ });
+
describe('checkPermissions', () => {
const userWithFeatureFlag = {
permissions: [{ regionId: 1, scopeId: SCOPES.READ_WRITE_REPORTS }],
From cf460fc2d07da274265119e73428d61b684d8db2 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Thu, 19 Dec 2024 14:56:10 -0500
Subject: [PATCH 151/198] add test coverage for reduceGoals
---
src/goalServices/reduceGoals.test.ts | 124 +++++++++++++++++++++++++++
1 file changed, 124 insertions(+)
diff --git a/src/goalServices/reduceGoals.test.ts b/src/goalServices/reduceGoals.test.ts
index 7efe77595e..dc33d48ffe 100644
--- a/src/goalServices/reduceGoals.test.ts
+++ b/src/goalServices/reduceGoals.test.ts
@@ -231,4 +231,128 @@ describe('reduceGoals', () => {
expect(result.length).toEqual(1);
expect(result[0].objectiveCreatedHere).toEqual(true);
});
+
+ it('should properly reduce objectives for activity report with activityReportObjectiveCitations', () => {
+ const newObjectives = [
+ {
+ id: 1,
+ otherEntityId: 123,
+ title: 'Objective 1',
+ status: 'Not Started',
+ topics: [],
+ resources: [],
+ files: [],
+ courses: [],
+ goalId: 6,
+ onApprovedAR: false,
+ onAR: false,
+ rtrOrder: 1,
+ activityReportObjectives: [
+ {
+ status: 'Not Started',
+ objectiveCreatedHere: true,
+ activityReportObjectiveResources: [],
+ activityReportObjectiveTopics: [],
+ activityReportObjectiveCourses: [],
+ activityReportObjectiveFiles: [],
+ activityReportObjectiveCitations: [
+ {
+ citation: 'Citation 1',
+ monitoringReferences: [
+ {
+ standardId: 1,
+ findingSource: 'Source 1',
+ acro: 'DEF',
+ },
+ ],
+ },
+ {
+ citation: 'Citation 2',
+ monitoringReferences: [
+ {
+ standardId: 2,
+ findingSource: 'Source 2',
+ acro: 'ANC',
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ id: 2,
+ otherEntityId: 123,
+ title: 'Objective 2',
+ status: 'Not Started',
+ topics: [],
+ resources: [],
+ files: [],
+ courses: [],
+ goalId: 6,
+ onApprovedAR: false,
+ onAR: false,
+ rtrOrder: 1,
+ activityReportObjectives: [
+ {
+ status: 'Not Started',
+ objectiveCreatedHere: true,
+ activityReportObjectiveResources: [],
+ activityReportObjectiveTopics: [],
+ activityReportObjectiveCourses: [],
+ activityReportObjectiveFiles: [],
+ activityReportObjectiveCitations: [
+ {
+ citation: 'Citation 3',
+ monitoringReferences: [
+ {
+ standardId: 3,
+ findingSource: 'Source 3',
+ acro: 'ANC',
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ];
+
+ const currentObjectives = [
+ {
+ status: 'Not Started',
+ title: 'Objective 1',
+ objectiveCreatedHere: false,
+ ids: [],
+ recipientIds: [],
+ activityReports: [],
+ topics: [],
+ resources: [],
+ files: [],
+ courses: [],
+ citations: [],
+ },
+ ];
+
+ // @ts-ignore
+ const result = reduceObjectivesForActivityReport(newObjectives, currentObjectives);
+ expect(result.length).toEqual(2);
+ expect(result[0].citations).toEqual([
+ {
+ id: 1,
+ name: 'DEF - Citation 1 - Source 1',
+ },
+ {
+ id: 2,
+ name: 'ANC - Citation 2 - Source 2',
+ },
+ ]);
+
+ expect(result[1].citations).toEqual([
+ {
+ id: 3,
+ name: 'ANC - Citation 3 - Source 3',
+ },
+ ]);
+ });
});
From 5e7416593060569c9dd675d375dfc813eb30ec65 Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Thu, 19 Dec 2024 15:45:26 -0500
Subject: [PATCH 152/198] Add model tests for AROCitation
---
src/models/activityReportObjectiveCitation.js | 3 --
.../activityReportObjectiveCitation.test.js | 35 ++++++++++++++++---
2 files changed, 30 insertions(+), 8 deletions(-)
diff --git a/src/models/activityReportObjectiveCitation.js b/src/models/activityReportObjectiveCitation.js
index 98e0605dc6..ada2a32a11 100644
--- a/src/models/activityReportObjectiveCitation.js
+++ b/src/models/activityReportObjectiveCitation.js
@@ -54,14 +54,12 @@ export default (sequelize, DataTypes) => {
findingIds: {
type: DataTypes.VIRTUAL,
get() {
- if (!this.monitoringReferences) return null;
return this.monitoringReferences.map((reference) => reference.findingId);
},
},
grantNumber: {
type: DataTypes.VIRTUAL,
get() {
- if (!this.monitoringReferences) return null;
const [reference] = this.monitoringReferences;
if (!reference) return null;
return reference.grantNumber;
@@ -70,7 +68,6 @@ export default (sequelize, DataTypes) => {
reviewNames: {
type: DataTypes.VIRTUAL,
get() {
- if (!this.monitoringReferences) return null;
return this.monitoringReferences.map((reference) => reference.reviewName);
},
},
diff --git a/src/models/tests/activityReportObjectiveCitation.test.js b/src/models/tests/activityReportObjectiveCitation.test.js
index 6c49c8407c..fa71bd1a8c 100644
--- a/src/models/tests/activityReportObjectiveCitation.test.js
+++ b/src/models/tests/activityReportObjectiveCitation.test.js
@@ -1,7 +1,6 @@
-/* eslint-disable max-len */
-/* eslint-disable prefer-destructuring */
import { REPORT_STATUSES } from '@ttahub/common';
import { faker } from '@faker-js/faker';
+import { sequelize } from 'sequelize';
import db, {
User,
Recipient,
@@ -163,13 +162,23 @@ describe('activityReportObjectiveCitation', () => {
const activityReportObjectiveCitation1 = await ActivityReportObjectiveCitation.create({
activityReportObjectiveId: activityReportObjective.id,
citation: 'Sample Citation 1',
- monitoringReferences: [{ grantId: grant.id, findingId: 1, reviewName: 'Review Name 1' }],
+ monitoringReferences: [{
+ grantId: grant.id, findingId: 1, reviewName: 'Review Name 1', grantNumber: grant.number,
+ }],
}, { individualHooks: true });
const activityReportObjectiveCitation2 = await ActivityReportObjectiveCitation.create({
activityReportObjectiveId: activityReportObjective.id,
citation: 'Sample Citation 2',
- monitoringReferences: [{ grantId: grant.id, findingId: 2, reviewName: 'Review Name 2' }],
+ monitoringReferences: [{
+ grantId: grant.id, findingId: 2, reviewName: 'Review Name 2', grantNumber: grant.number,
+ }],
+ }, { individualHooks: true });
+
+ const activityReportObjectiveCitation3 = await ActivityReportObjectiveCitation.create({
+ activityReportObjectiveId: activityReportObjective.id,
+ citation: 'Sample Citation 3',
+ monitoringReferences: [],
}, { individualHooks: true });
// Assert citations.
@@ -181,7 +190,9 @@ describe('activityReportObjectiveCitation', () => {
expect(activityReportObjectiveCitationLookUp.length).toBe(2);
// Assert citation values regardless of order.
- activityReportObjectiveCitationLookUp = activityReportObjectiveCitationLookUp.map((c) => c.get({ plain: true }));
+ activityReportObjectiveCitationLookUp = activityReportObjectiveCitationLookUp.map(
+ (c) => c.get({ plain: true }),
+ );
// Citation 1.
const citation1LookUp = activityReportObjectiveCitationLookUp.find((c) => c.citation === 'Sample Citation 1');
@@ -192,6 +203,11 @@ describe('activityReportObjectiveCitation', () => {
expect(reference.findingId).toBe(1);
expect(reference.reviewName).toBe('Review Name 1');
+ // test virtual column lookups and cases
+ expect(citation1LookUp.findingIds).toStrictEqual([1]);
+ expect(citation1LookUp.grantNumber).toBe(grant.number);
+ expect(citation1LookUp.reviewNames).toStrictEqual(['Review Name 1']);
+
// Citation 2.
const citation2LookUp = activityReportObjectiveCitationLookUp.find((c) => c.citation === 'Sample Citation 2');
expect(citation2LookUp).toBeDefined();
@@ -200,5 +216,14 @@ describe('activityReportObjectiveCitation', () => {
expect(secondReference.grantId).toBe(grant.id);
expect(secondReference.findingId).toBe(2);
expect(secondReference.reviewName).toBe('Review Name 2');
+
+ // citation 3 should have empty monitoring references
+ const citationThree = await ActivityReportObjectiveCitation.findByPk(
+ activityReportObjectiveCitation3.id,
+ );
+
+ expect(citationThree.findingIds).toStrictEqual([]);
+ expect(citationThree.grantNumber).toBeNull();
+ expect(citationThree.reviewNames).toStrictEqual([]);
});
});
From e75759321a973738dddecec86bd48a8458334fe7 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Thu, 19 Dec 2024 16:29:52 -0500
Subject: [PATCH 153/198] fix extra comma in bad merge
---
.../ActivityReport/Pages/__tests__/goalsObjectives.js | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/frontend/src/pages/ActivityReport/Pages/__tests__/goalsObjectives.js b/frontend/src/pages/ActivityReport/Pages/__tests__/goalsObjectives.js
index 0ed9070e38..8fa45f790a 100644
--- a/frontend/src/pages/ActivityReport/Pages/__tests__/goalsObjectives.js
+++ b/frontend/src/pages/ActivityReport/Pages/__tests__/goalsObjectives.js
@@ -38,11 +38,14 @@ const defaultGoals = [{
status: 'In Progress',
courses: [],
}],
-
}];
const RenderGoalsObjectives = ({
- grantIds, activityRecipientType, connectionActive = true, startDate = null, , goalsToUse = defaultGoals,
+ grantIds,
+ activityRecipientType,
+ connectionActive = true,
+ startDate = null,
+ goalsToUse = defaultGoals,
}) => {
const activityRecipients = grantIds.map((activityRecipientId) => ({
activityRecipientId, id: activityRecipientId,
From 91e1d5f8220bacd1540e2cf3e609f2f4017f25f1 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Thu, 19 Dec 2024 16:58:52 -0500
Subject: [PATCH 154/198] deploy to sandbox
---
.circleci/config.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.circleci/config.yml b/.circleci/config.yml
index a171494111..d35ac04316 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -761,7 +761,7 @@ parameters:
default: "main"
type: string
sandbox_git_branch: # change to feature branch to test deployment
- default: "TTAHUB-3678/login"
+ default: "al-ttahub-3603-activity-report-objective-citations"
type: string
prod_new_relic_app_id:
default: "877570491"
From 2e73e7af3d1928816b5c9b77c321f83ce81d6a27 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Fri, 20 Dec 2024 15:22:23 -0500
Subject: [PATCH 155/198] push back dates for testing on SB
---
src/services/citations.ts | 4 ++--
src/tools/createMonitoringGoals.js | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/services/citations.ts b/src/services/citations.ts
index ada5b19caa..f21833b8ab 100644
--- a/src/services/citations.ts
+++ b/src/services/citations.ts
@@ -17,8 +17,8 @@ export async function textByCitation(
}
// TODO: Update this to the day we deploy to PROD.
-const cutOffStartDate = new Date().toISOString().split('T')[0];
-// const cutOffStartDate = '2021-01-01';
+// const cutOffStartDate = new Date().toISOString().split('T')[0];
+const cutOffStartDate = '2024-01-01';
/*
The purpose of this function is to get citations by grant id.
We then need to format the response for how it needs to be
diff --git a/src/tools/createMonitoringGoals.js b/src/tools/createMonitoringGoals.js
index ab9d34ec8f..f4fa4ac849 100644
--- a/src/tools/createMonitoringGoals.js
+++ b/src/tools/createMonitoringGoals.js
@@ -5,7 +5,7 @@ import {
import { auditLogger } from '../logger';
const createMonitoringGoals = async () => {
- const cutOffDate = '2024-11-26'; // TODO: Set this before we deploy to prod.
+ const cutOffDate = '2024-01-01'; // TODO: Set this before we deploy to prod.
// Verify that the monitoring goal template exists.
const monitoringGoalTemplate = await GoalTemplate.findOne({
From cc72c9ac6eb95dbc30cc1a2a9499af75011a5537 Mon Sep 17 00:00:00 2001
From: GarrettEHill
Date: Fri, 20 Dec 2024 16:08:33 -0800
Subject: [PATCH 156/198] Add support for grants with monitoring from a grant
that was replaced
---
src/services/citations.ts | 120 +++++++++++++++++++-------------------
1 file changed, 61 insertions(+), 59 deletions(-)
diff --git a/src/services/citations.ts b/src/services/citations.ts
index f21833b8ab..0fc5c0d9a8 100644
--- a/src/services/citations.ts
+++ b/src/services/citations.ts
@@ -50,45 +50,9 @@ export async function getCitationsByGrantIds(
// Query to get the citations by grant id.
const grantsByCitations = await sequelize.query(
/* sql */
- `
- SELECT
- ms."standardId",
- ms.citation,
- JSONB_AGG( DISTINCT
- JSONB_BUILD_OBJECT(
- 'findingId', mf."findingId",
- 'grantId', gr.id,
- 'grantNumber', gr.number,
- 'reviewName', mfh."name",
- 'reportDeliveryDate', mfh."reportDeliveryDate",
- 'findingType', mf."findingType",
- 'findingSource', mf."source",
- 'monitoringFindingStatusName', mfs."name",
- 'reportDeliveryDate', mfh."reportDeliveryDate",
- 'citation', ms.citation,
- 'severity', CASE
- WHEN mf."findingType" = 'Deficiency' THEN 1
- WHEN mf."findingType" = 'Noncompliance' THEN 2
- ELSE 3
- END,
- 'acro', CASE
- WHEN mf."findingType" = 'Deficiency' THEN 'DEF'
- WHEN mf."findingType" = 'Noncompliance' THEN 'ANC'
- ELSE 'AOC'
- END
- )
- ) grants
- FROM "Grants" gr
- JOIN "Goals" g
- ON gr."id" = g."grantId"
- AND g."status" NOT IN ('Closed', 'Suspended')
- JOIN "GoalTemplates" gt
- ON g."goalTemplateId" = gt."id"
- AND gt."standard" = 'Monitoring'
- JOIN "MonitoringReviewGrantees" mrg
- ON gr.number = mrg."grantNumber"
- JOIN (
- -- The below 'DISTINCT ON' determines the single record to return values from by the 'ORDER BY' clause.
+ `WITH
+ -- Subquery ensures only the most recent history for each finding-grant combination
+ "RecentMonitoring" AS (
SELECT DISTINCT ON (mfh."findingId", gr.id)
mfh."findingId",
gr.id AS "grantId",
@@ -102,27 +66,65 @@ export async function getCitationsByGrantIds(
ON mrg."reviewId" = mr."reviewId"
JOIN "Grants" gr
ON gr.number = mrg."grantNumber"
+ WHERE mr."reportDeliveryDate"::date BETWEEN '${cutOffStartDate}' AND '${reportStartDate}'
ORDER BY mfh."findingId", gr.id, mr."reportDeliveryDate" DESC
- ) mfh -- Subquery ensures only the most recent history for each finding-grant combination
- ON mfh."grantId" = gr.id
- JOIN "MonitoringFindings" mf
- ON mfh."findingId" = mf."findingId"
- JOIN "MonitoringFindingStatuses" mfs
- ON mf."statusId" = mfs."statusId"
- JOIN "MonitoringFindingStandards" mfs2
- ON mf."findingId" = mfs2."findingId"
- JOIN "MonitoringStandards" ms
- ON mfs2."standardId" = ms."standardId"
- JOIN "MonitoringFindingGrants" mfg
- ON mf."findingId" = mfg."findingId"
- AND mrg."granteeId" = mfg."granteeId"
- WHERE 1 = 1
- AND gr.id IN (${grantIds.join(',')}) -- :grantIds
- AND mfh."reportDeliveryDate"::date BETWEEN '${cutOffStartDate}' AND '${reportStartDate}'
- AND gr.status = 'Active'
- AND mfs.name = 'Active'
- GROUP BY 1,2
- ORDER BY 2,1;
+ )
+ SELECT
+ ms."standardId",
+ ms.citation,
+ JSONB_AGG( DISTINCT
+ JSONB_BUILD_OBJECT(
+ 'findingId', mf."findingId",
+ 'grantId', grta."activeGrantId",
+ 'originalGrantId', grta."grantId",
+ 'grantNumber', gr.number,
+ 'reviewName', rm."name",
+ 'reportDeliveryDate', rm."reportDeliveryDate",
+ 'findingType', mf."findingType",
+ 'findingSource', mf."source",
+ 'monitoringFindingStatusName', mfs."name",
+ 'citation', ms.citation,
+ 'severity', CASE
+ WHEN mf."findingType" = 'Deficiency' THEN 1
+ WHEN mf."findingType" = 'Noncompliance' THEN 2
+ ELSE 3 -- Area of Concern
+ END,
+ 'acro', CASE
+ WHEN mf."findingType" = 'Deficiency' THEN 'DEF'
+ WHEN mf."findingType" = 'Noncompliance' THEN 'ANC'
+ ELSE 'AOC' -- Area of Concern
+ END
+ )
+ ) grants
+ FROM "GrantRelationshipToActive" grta
+ JOIN "Grants" gr
+ ON grta."grantId" = gr.id
+ JOIN "Goals" g
+ ON grta."activeGrantId" = g."grantId"
+ AND g."status" NOT IN ('Closed', 'Suspended')
+ JOIN "GoalTemplates" gt
+ ON g."goalTemplateId" = gt."id"
+ AND gt."standard" = 'Monitoring'
+ JOIN "MonitoringReviewGrantees" mrg
+ ON gr.number = mrg."grantNumber"
+ JOIN "RecentMonitoring" rm
+ ON rm."grantId" = gr.id
+ JOIN "MonitoringFindings" mf
+ ON rm."findingId" = mf."findingId"
+ JOIN "MonitoringFindingStatuses" mfs
+ ON mf."statusId" = mfs."statusId"
+ JOIN "MonitoringFindingStandards" mfs2
+ ON mf."findingId" = mfs2."findingId"
+ JOIN "MonitoringStandards" ms
+ ON mfs2."standardId" = ms."standardId"
+ JOIN "MonitoringFindingGrants" mfg
+ ON mf."findingId" = mfg."findingId"
+ AND mrg."granteeId" = mfg."granteeId"
+ WHERE 1 = 1
+ AND grta."activeGrantId" IN (${grantIds.join(',')}) -- :grantIds
+ AND mfs.name = 'Active'
+ GROUP BY 1,2
+ ORDER BY 2,1;
`,
);
From 3faa352425ddde10a996225cf7dbf40afb38f1fa Mon Sep 17 00:00:00 2001
From: GarrettEHill
Date: Fri, 20 Dec 2024 16:48:04 -0800
Subject: [PATCH 157/198] ssdi query to use for identifying grants to use for
testing
/api/ssdi/dataRequests/qa/monitoring/grants-with-citations.sql
---
.../qa/monitoring/grants-with-citations.sql | 153 ++++++++++++++++++
1 file changed, 153 insertions(+)
create mode 100644 src/queries/dataRequests/qa/monitoring/grants-with-citations.sql
diff --git a/src/queries/dataRequests/qa/monitoring/grants-with-citations.sql b/src/queries/dataRequests/qa/monitoring/grants-with-citations.sql
new file mode 100644
index 0000000000..d0a4cef461
--- /dev/null
+++ b/src/queries/dataRequests/qa/monitoring/grants-with-citations.sql
@@ -0,0 +1,153 @@
+/*
+JSON: {
+ "name": "Recent Monitoring Findings Report",
+ "description": {
+ "standard": "This report aggregates the most recent monitoring findings for grants, including citations, active grants, and replacements.",
+ "technical": "The query retrieves monitoring review details, grant relationships, and associated citations. It filters findings based on report delivery date, ensures only the most recent history per grant-finding combination, and groups the results by region and grant IDs."
+ },
+ "output": {
+ "defaultName": "recent_monitoring_findings",
+ "schema": [
+ {
+ "columnName": "regionId",
+ "type": "integer",
+ "nullable": false,
+ "description": "The region ID associated with the grant."
+ },
+ {
+ "columnName": "replacedGrantId",
+ "type": "integer",
+ "nullable": true,
+ "description": "The ID of the grant that has been replaced."
+ },
+ {
+ "columnName": "replacedGrantNumber",
+ "type": "string",
+ "nullable": true,
+ "description": "The number of the grant that has been replaced."
+ },
+ {
+ "columnName": "activeGrantId",
+ "type": "integer",
+ "nullable": false,
+ "description": "The ID of the active grant."
+ },
+ {
+ "columnName": "activeGrantNumber",
+ "type": "string",
+ "nullable": false,
+ "description": "The number of the active grant."
+ },
+ {
+ "columnName": "citations",
+ "type": "array",
+ "nullable": false,
+ "description": "An array of distinct citations associated with the monitoring findings."
+ }
+ ]
+ },
+ "filters": [
+ {
+ "name": "region",
+ "type": "integer[]",
+ "display": "Region IDs",
+ "description": "One or more values for 1 through 12 representing different regions."
+ },
+ {
+ "name": "reportDeliveryDate",
+ "type": "date[]",
+ "display": "Report Delivery Date Range",
+ "description": "Two dates defining the range for the 'reportDeliveryDate' field."
+ }
+ ],
+ "sorting": {
+ "default": [
+ { "level": 1, "name": "regionId", "order": "ASC" },
+ { "level": 2, "name": "replacedGrantId", "order": "ASC" },
+ { "level": 3, "name": "activeGrantId", "order": "ASC" }
+ ]
+ },
+ "customSortingSupported": false,
+ "paginationSupported": false,
+ "exampleUsage": "SELECT SET_CONFIG('ssdi.reportDeliveryDate', '[\"2024-01-01\",\"2024-12-31\"]', TRUE);"
+}
+*/
+
+WITH
+ -- Subquery ensures only the most recent history for each finding-grant combination
+"RecentMonitoring" AS (
+ SELECT DISTINCT ON (mfh."findingId", gr.id)
+ mfh."findingId",
+ gr.id AS "grantId",
+ mr."reviewId",
+ mr."name",
+ mr."reportDeliveryDate"
+ FROM "MonitoringFindingHistories" mfh
+ JOIN "MonitoringReviews" mr
+ ON mfh."reviewId" = mr."reviewId"
+ JOIN "MonitoringReviewGrantees" mrg
+ ON mrg."reviewId" = mr."reviewId"
+ JOIN "Grants" gr
+ ON gr.number = mrg."grantNumber"
+ WHERE 1 = 1
+ AND (
+ (
+ NULLIF(current_setting('ssdi.reportDeliveryDate', true), '') IS NULL
+ AND mr."reportDeliveryDate"::date BETWEEN '2024-01-01' AND NOW()
+ )
+ OR (
+ mr."reportDeliveryDate"::date <@ (
+ SELECT CONCAT(
+ '[', MIN(value::timestamp), ',',
+ COALESCE(NULLIF(MAX(value::timestamp), MIN(value::timestamp)), NOW()::timestamp), ')'
+ )::daterange
+ FROM json_array_elements_text(
+ COALESCE(NULLIF(current_setting('ssdi.reportDeliveryDate', true), ''), '[]')::json
+ ) AS value
+ ) != false
+ )
+ )
+ ORDER BY mfh."findingId", gr.id, mr."reportDeliveryDate" DESC
+)
+SELECT
+ gr."regionId",
+ NULLIF(gr.id, gr2.id) "replacedGrantId",
+ NULLIF(gr.number, gr2.number) "replacedGrantNumber",
+ gr2.id "activeGrantId",
+ gr2.number "activeGrantNumber",
+ ARRAY_AGG(DISTINCT ms.citation)
+FROM "GrantRelationshipToActive" grta
+JOIN "Grants" gr
+ ON grta."grantId" = gr.id
+JOIN "Goals" g
+ ON grta."activeGrantId" = g."grantId"
+ AND g."status" NOT IN ('Closed', 'Suspended')
+JOIN "GoalTemplates" gt
+ ON g."goalTemplateId" = gt."id"
+ AND gt."standard" = 'Monitoring'
+JOIN "MonitoringReviewGrantees" mrg
+ ON gr.number = mrg."grantNumber"
+JOIN "RecentMonitoring" rm
+ON rm."grantId" = gr.id
+JOIN "MonitoringFindings" mf
+ ON rm."findingId" = mf."findingId"
+JOIN "MonitoringFindingStatuses" mfs
+ ON mf."statusId" = mfs."statusId"
+JOIN "MonitoringFindingStandards" mfs2
+ ON mf."findingId" = mfs2."findingId"
+JOIN "MonitoringStandards" ms
+ ON mfs2."standardId" = ms."standardId"
+JOIN "MonitoringFindingGrants" mfg
+ ON mf."findingId" = mfg."findingId"
+ AND mrg."granteeId" = mfg."granteeId"
+JOIN "Grants" gr2
+ON grta."activeGrantId" = gr2.id
+WHERE 1 = 1
+-- Filter for region if ssdi.region is defined
+AND (NULLIF(current_setting('ssdi.region', true), '') IS NULL
+ OR gr."regionId" in (
+ SELECT value::integer AS my_array
+ FROM json_array_elements_text(COALESCE(NULLIF(current_setting('ssdi.region', true), ''),'[]')::json) AS value
+ ))
+GROUP BY 1,2,3,4,5
+ORDER BY 1,2,4
\ No newline at end of file
From 963f109cab3e51dd7ac389d3feb942140f1f8671 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Mon, 23 Dec 2024 11:07:36 -0500
Subject: [PATCH 158/198] add a test to ensure we are returning citations from
the replaced grants
---
src/services/citations.test.js | 136 ++++++++++++++++++++++++++++++++-
1 file changed, 132 insertions(+), 4 deletions(-)
diff --git a/src/services/citations.test.js b/src/services/citations.test.js
index 355924ab3f..cc9ae56c6f 100644
--- a/src/services/citations.test.js
+++ b/src/services/citations.test.js
@@ -18,6 +18,7 @@ import db, {
GoalTemplate,
Goal,
GrantRelationshipToActive,
+ GrantReplacements,
} from '../models';
import { captureSnapshot, rollbackToSnapshot } from '../lib/programmaticTransaction';
@@ -147,9 +148,6 @@ const createMonitoringData = async (
citable,
}, { individualHooks: true });
}));
-
- // Refresh the materialized view.
- await GrantRelationshipToActive.refresh();
};
describe('citations service', () => {
@@ -160,12 +158,16 @@ describe('citations service', () => {
let recipient1;
let recipient2;
+ let recipient3;
let grant1; // Recipient 1
let grant1a; // Recipient 1
let grant2; // Recipient 2
let grant3; // Recipient 2 (Inactive)
+ let grant4Original;
+ let grant4Replacement;
+
beforeAll(async () => {
// Capture a snapshot of the database before running the test.
snapShot = await captureSnapshot();
@@ -183,6 +185,9 @@ describe('citations service', () => {
const grantNumber2 = faker.datatype.string(8);
const grantNumber3 = faker.datatype.string(8);
+ const grantNumber4Original = faker.datatype.string(8);
+ const grantNumber4Replacement = faker.datatype.string(8);
+
// Recipients 1.
recipient1 = await Recipient.create({
id: faker.datatype.number({ min: 64000 }),
@@ -195,6 +200,12 @@ describe('citations service', () => {
name: faker.random.alphaNumeric(6),
});
+ // Recipients 3.
+ recipient3 = await Recipient.create({
+ id: faker.datatype.number({ min: 64000 }),
+ name: faker.random.alphaNumeric(6),
+ });
+
// Grants.
const grants = await Grant.bulkCreate([
{
@@ -237,6 +248,26 @@ describe('citations service', () => {
endDate: new Date(),
status: 'Inactive',
},
+ // Grant 4 for Recipient 3 (original).
+ {
+ id: faker.datatype.number({ min: 9999 }),
+ number: grantNumber4Original,
+ recipientId: recipient3.id,
+ regionId: 1,
+ startDate: new Date(),
+ endDate: new Date(),
+ status: 'Active',
+ },
+ // Grant 4 for Recipient 3 (replacement).
+ {
+ id: faker.datatype.number({ min: 9999 }),
+ number: grantNumber4Replacement,
+ recipientId: recipient3.id,
+ regionId: 1,
+ startDate: new Date(),
+ endDate: new Date(),
+ status: 'Active',
+ },
]);
// set the grants.
@@ -245,6 +276,10 @@ describe('citations service', () => {
grant2 = grants[2];
grant3 = grants[3];
+ // Replacement citations test.
+ grant4Original = grants[4];
+ grant4Replacement = grants[5];
+
// Create Goals and Link them to Grants.
await Goal.create({
name: 'Monitoring Goal 1',
@@ -296,13 +331,38 @@ describe('citations service', () => {
goalTemplateId: monitoringGoalTemplate.id,
});
+ // Regular goal for grant 4 being replaced.
+ await Goal.create({
+ name: 'Regular Goal 4 Original',
+ status: 'In Progress',
+ timeframe: '12 months',
+ isFromSmartsheetTtaPlan: false,
+ grantId: grant4Original.id,
+ createdAt: '2024-11-26T19:16:15.842Z',
+ onApprovedAR: true,
+ createdVia: 'activityReport',
+ });
+
+ // Replacement goal for grant 4 monitoring.
+ await Goal.create({
+ name: 'Monitoring Goal 4 replacement',
+ status: 'Not started',
+ timeframe: '12 months',
+ isFromSmartsheetTtaPlan: false,
+ grantId: grant4Replacement.id,
+ createdAt: '2024-11-26T19:16:15.842Z',
+ onApprovedAR: true,
+ createdVia: 'monitoring',
+ goalTemplateId: monitoringGoalTemplate.id,
+ });
+
/*
Citation Object Properties:
citationText, // Citation Text
monitoringFindingType, // Monitoring Finding ('Deficiency', 'Significant Deficiency', 'Material Weakness', 'No Finding').
monitoringFindingStatusName, // Monitoring Finding Status name must be 'Active'.
monitoringFindingGrantFindingType, // Monitoring Finding Grant Finding Type must be in ('Corrective Action', 'Management Decision', 'No Finding').
- */
+ */
// Create Monitoring Review Citations.
const grant1Citations1 = [
@@ -364,6 +424,39 @@ describe('citations service', () => {
},
];
await createMonitoringData(grant3.number, 5, new Date(), 'AIAN-DEF', 'Complete', grant1Citations3);
+
+ // Create Grant Replacement data.
+ await GrantReplacements.create({
+ replacedGrantId: grant4Original.id,
+ replacingGrantId: grant4Replacement.id,
+ replacementDate: new Date(),
+ });
+
+ // Grant 4 original.
+ const grant1Citations4Original = [
+ {
+ citationText: 'Grant 4 ON REPLACED - Citation 1 - Good',
+ monitoringFindingType: 'Material Weakness',
+ monitoringFindingStatusName: 'Active',
+ monitoringFindingGrantFindingType: 'Corrective Action',
+ },
+ ];
+ await createMonitoringData(grant4Original.number, 6, new Date(), 'AIAN-DEF', 'Complete', grant1Citations4Original);
+
+ // Grant 4 replacement.
+ const grant1Citations4Replacement = [
+ {
+ citationText: 'Grant 4 replacement - Citation 1 - Good',
+ monitoringFindingType: 'Material Weakness',
+ monitoringFindingStatusName: 'Active',
+ monitoringFindingGrantFindingType: 'Corrective Action',
+ },
+ ];
+
+ await createMonitoringData(grant4Replacement.number, 7, new Date(), 'AIAN-DEF', 'Complete', grant1Citations4Replacement);
+
+ // Refresh the materialized view.
+ await GrantRelationshipToActive.refresh();
});
afterAll(async () => {
@@ -418,6 +511,41 @@ describe('citations service', () => {
expect(citation3.grants[0].findingType).toBe('Citation 4 Monitoring Finding Type');
});
+ it('gets the citations linked to a grant that has been replaced by another grant', async () => {
+ const reportStartDate = new Date().toISOString().split('T')[0];
+ const citationsToAssert = await getCitationsByGrantIds([grant4Replacement.id], reportStartDate);
+
+ // Assert correct number of citations.
+ expect(citationsToAssert.length).toBe(2);
+
+ // Assert the citations.
+ // Get the citation with the text 'Grant 4 replacement - Citation 1 - Good'.
+ const citation1 = citationsToAssert.find((c) => c.citation === 'Grant 4 replacement - Citation 1 - Good');
+ expect(citation1).toBeDefined();
+ expect(citation1.grants.length).toBe(1);
+ expect(citation1.grants[0].findingId).toBeDefined();
+ expect(citation1.grants[0].grantId).toBe(grant4Replacement.id);
+ expect(citation1.grants[0].grantNumber).toBe(grant4Replacement.number);
+ expect(citation1.grants[0].reviewName).toBeDefined();
+ expect(citation1.grants[0].reportDeliveryDate).toBeDefined();
+ expect(citation1.grants[0].findingType).toBe('Material Weakness');
+ expect(citation1.grants[0].findingSource).toBe('Internal Controls');
+ expect(citation1.grants[0].monitoringFindingStatusName).toBe('Active');
+
+ // Get the citation with the text 'Grant 4 ON REPLACED - Citation 1 - Good'.
+ const citation2 = citationsToAssert.find((c) => c.citation === 'Grant 4 ON REPLACED - Citation 1 - Good');
+ expect(citation2).toBeDefined();
+ expect(citation2.grants.length).toBe(1);
+ expect(citation2.grants[0].findingId).toBeDefined();
+ expect(citation2.grants[0].grantId).toBe(grant4Replacement.id);
+ expect(citation2.grants[0].grantNumber).toBe(grant4Original.number); // ?
+ expect(citation2.grants[0].reviewName).toBeDefined();
+ expect(citation2.grants[0].reportDeliveryDate).toBeDefined();
+ expect(citation2.grants[0].findingType).toBe('Material Weakness');
+ expect(citation2.grants[0].findingSource).toBe('Internal Controls');
+ expect(citation2.grants[0].monitoringFindingStatusName).toBe('Active');
+ });
+
describe('textByCitation', () => {
it('gets text by citation', async () => {
const response = await textByCitation(['Grant 2 - Citation 1 - Good']);
From 148d05e48b83ec2dfda66895aca5590a241d94f4 Mon Sep 17 00:00:00 2001
From: GarrettEHill
Date: Mon, 30 Dec 2024 15:32:12 -0800
Subject: [PATCH 159/198] Update grants-with-citations.sql
---
.../dataRequests/qa/monitoring/grants-with-citations.sql | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/queries/dataRequests/qa/monitoring/grants-with-citations.sql b/src/queries/dataRequests/qa/monitoring/grants-with-citations.sql
index d0a4cef461..2a42577391 100644
--- a/src/queries/dataRequests/qa/monitoring/grants-with-citations.sql
+++ b/src/queries/dataRequests/qa/monitoring/grants-with-citations.sql
@@ -150,4 +150,4 @@ AND (NULLIF(current_setting('ssdi.region', true), '') IS NULL
FROM json_array_elements_text(COALESCE(NULLIF(current_setting('ssdi.region', true), ''),'[]')::json) AS value
))
GROUP BY 1,2,3,4,5
-ORDER BY 1,2,4
\ No newline at end of file
+ORDER BY 1,2,4;
From 1c75ab336c53f7f52a919db3d7d105501cc5e3da Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Thu, 2 Jan 2025 11:26:40 -0500
Subject: [PATCH 160/198] Match delivery date to citation date
---
src/services/monitoring.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/services/monitoring.ts b/src/services/monitoring.ts
index 86b6fb391a..1958e22304 100644
--- a/src/services/monitoring.ts
+++ b/src/services/monitoring.ts
@@ -47,7 +47,7 @@ const {
Role,
} = db;
-const MIN_DELIVERY_DATE = '2022-01-01';
+const MIN_DELIVERY_DATE = '2024-01-01';
const REVIEW_STATUS_COMPLETE = 'Complete';
async function grantNumbersByRecipientAndRegion(recipientId: number, regionId: number) {
From 5b2b0d5ee25351ac17e1a9f7850eb0ec3c1dd361 Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Thu, 2 Jan 2025 11:50:45 -0500
Subject: [PATCH 161/198] Adjust date in seeders
---
src/seeders/20240228223541-monitoring-data.js | 2 +-
src/services/ttaByCitation.test.js | 1 -
2 files changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/seeders/20240228223541-monitoring-data.js b/src/seeders/20240228223541-monitoring-data.js
index 6de4f8bd49..9ef7689cc2 100644
--- a/src/seeders/20240228223541-monitoring-data.js
+++ b/src/seeders/20240228223541-monitoring-data.js
@@ -27,7 +27,7 @@ const reviews = [
startDate: new Date('2022/12/08'),
endDate: new Date('2022/12/08'),
reviewType: 'RAN',
- reportDeliveryDate: new Date('2023/01/13'),
+ reportDeliveryDate: new Date('2024/01/13'),
outcome: 'Deficient',
hash: 'seedhashrev2',
sourceCreatedAt: new Date('2022/12/08'),
diff --git a/src/services/ttaByCitation.test.js b/src/services/ttaByCitation.test.js
index b89c2ddcab..9ce3480a7d 100644
--- a/src/services/ttaByCitation.test.js
+++ b/src/services/ttaByCitation.test.js
@@ -1,4 +1,3 @@
-import moment from 'moment';
import { ttaByCitations } from './monitoring';
import {
createAdditionalMonitoringData,
From 41f2cfbe2b4ae278e69afb90f9071732b28341ad Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Thu, 2 Jan 2025 12:30:30 -0500
Subject: [PATCH 162/198] Update test helpers
---
src/services/monitoring.testHelpers.ts | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/services/monitoring.testHelpers.ts b/src/services/monitoring.testHelpers.ts
index a3589997a5..f608e9de88 100644
--- a/src/services/monitoring.testHelpers.ts
+++ b/src/services/monitoring.testHelpers.ts
@@ -167,7 +167,7 @@ async function createMonitoringData(
grantNumber,
sourceCreatedAt: '2024-02-12 14:31:55.74-08',
sourceUpdatedAt: '2024-02-12 14:31:55.74-08',
- createTime: '2023-11-14 21:00:00-08',
+ createTime: '2024-11-14 21:00:00-08',
updateTime: '2024-02-12 14:31:55.74-08',
updateBy: 'Support Team',
},
@@ -182,11 +182,11 @@ async function createMonitoringData(
startDate: '2024-02-12',
endDate: '2024-02-12',
reviewType: 'FA-1',
- reportDeliveryDate: '2023-02-21 21:00:00-08',
+ reportDeliveryDate: '2024-02-21 21:00:00-08',
outcome: 'Complete',
hash: 'seedhashrev2',
- sourceCreatedAt: '2023-02-22 21:00:00-08',
- sourceUpdatedAt: '2023-02-22 21:00:00-08',
+ sourceCreatedAt: '2024-02-22 21:00:00-08',
+ sourceUpdatedAt: '2024-02-22 21:00:00-08',
name: 'REVIEW!!!',
},
});
From a1e1511dfe19e2ec4184a1ac19c4c4a619f6b9a2 Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Thu, 2 Jan 2025 12:33:31 -0500
Subject: [PATCH 163/198] Update some additional tests
---
src/services/monitoring.test.js | 6 +++---
src/services/monitoring.testHelpers.ts | 6 +++---
src/services/ttaByCitation.test.js | 2 +-
src/services/ttaByReview.test.js | 2 +-
4 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/src/services/monitoring.test.js b/src/services/monitoring.test.js
index 8e5d2ed2de..9945969480 100644
--- a/src/services/monitoring.test.js
+++ b/src/services/monitoring.test.js
@@ -98,7 +98,7 @@ describe('monitoring services', () => {
emotionalSupport: 6.2303,
classroomOrganization: 5.2303,
instructionalSupport: 3.2303,
- reportDeliveryDate: '2023-05-22 21:00:00-07',
+ reportDeliveryDate: '2024-05-22 21:00:00-07',
});
jest.spyOn(Grant, 'findOne').mockResolvedValueOnce({
@@ -161,7 +161,7 @@ describe('monitoring services', () => {
regionId: REGION_ID,
grant: GRANT_NUMBER,
reviewStatus: 'Complete',
- reviewDate: '02/22/2023',
+ reviewDate: '02/22/2024',
reviewType: 'FA-1',
});
});
@@ -174,7 +174,7 @@ describe('monitoring services', () => {
});
expect(data).not.toBeNull();
- expect(data.reviewDate).toEqual('02/22/2023');
+ expect(data.reviewDate).toEqual('02/22/2024');
await MonitoringReview.destroy({ where: { reviewId: 'C48EAA67-90B9-4125-9DB5-0011D6D7C809' }, force: true });
await MonitoringReview.destroy({ where: { reviewId: 'D58FBB78-91CA-4236-8DB6-0022E7E8D909' }, force: true });
diff --git a/src/services/monitoring.testHelpers.ts b/src/services/monitoring.testHelpers.ts
index f608e9de88..0dd341d3f3 100644
--- a/src/services/monitoring.testHelpers.ts
+++ b/src/services/monitoring.testHelpers.ts
@@ -152,10 +152,10 @@ async function createMonitoringData(
emotionalSupport: 6.2303,
classroomOrganization: 5.2303,
instructionalSupport: 3.2303,
- reportDeliveryDate: '2023-05-22 21:00:00-07',
+ reportDeliveryDate: '2024-05-22 21:00:00-07',
hash: 'seedhashclasssum1',
- sourceCreatedAt: '2023-05-22 21:00:00-07',
- sourceUpdatedAt: '2023-05-22 21:00:00-07',
+ sourceCreatedAt: '2024-05-22 21:00:00-07',
+ sourceUpdatedAt: '2024-05-22 21:00:00-07',
},
});
diff --git a/src/services/ttaByCitation.test.js b/src/services/ttaByCitation.test.js
index 9ce3480a7d..bbf6f104fc 100644
--- a/src/services/ttaByCitation.test.js
+++ b/src/services/ttaByCitation.test.js
@@ -181,7 +181,7 @@ describe('ttaByCitations', () => {
},
],
outcome: 'Complete',
- reviewReceived: '02/22/2023',
+ reviewReceived: '02/22/2024',
reviewType: 'FA-1',
specialists: [
{
diff --git a/src/services/ttaByReview.test.js b/src/services/ttaByReview.test.js
index 1f814b0825..ef23771d48 100644
--- a/src/services/ttaByReview.test.js
+++ b/src/services/ttaByReview.test.js
@@ -190,7 +190,7 @@ describe('ttaByReviews', () => {
lastTTADate: moment().format('MM/DD/YYYY'),
name: 'REVIEW!!!',
outcome: 'Complete',
- reviewReceived: '02/22/2023',
+ reviewReceived: '02/22/2024',
reviewType: 'FA-1',
specialists: [
{
From 8db957759021b403abd87943cd55b6be5bd41707 Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Thu, 2 Jan 2025 14:02:12 -0500
Subject: [PATCH 164/198] Sort reviews and citations
---
src/services/monitoring.ts | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/services/monitoring.ts b/src/services/monitoring.ts
index 1958e22304..2fe576754e 100644
--- a/src/services/monitoring.ts
+++ b/src/services/monitoring.ts
@@ -245,6 +245,7 @@ export async function ttaByReviews(
} = await extractExternalData(recipientId, regionId);
const reviews = await MonitoringReview.findAll({
+ order: [['reportDeliveryDate', 'ASC']],
where: {
reportDeliveryDate: {
[Op.gte]: MIN_DELIVERY_DATE,
@@ -399,6 +400,7 @@ export async function ttaByCitations(
} = await extractExternalData(recipientId, regionId);
const citations = await MonitoringStandard.findAll({
+ order: [['citation', 'ASC']],
include: [
{
model: MonitoringStandardLink,
From 9f23981dbbbfb8e16f82dc06d37ec28b1b7d5932 Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Thu, 2 Jan 2025 14:03:43 -0500
Subject: [PATCH 165/198] Alter review sort
---
src/services/monitoring.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/services/monitoring.ts b/src/services/monitoring.ts
index 2fe576754e..04dab72e0f 100644
--- a/src/services/monitoring.ts
+++ b/src/services/monitoring.ts
@@ -245,7 +245,7 @@ export async function ttaByReviews(
} = await extractExternalData(recipientId, regionId);
const reviews = await MonitoringReview.findAll({
- order: [['reportDeliveryDate', 'ASC']],
+ order: [['reportDeliveryDate', 'DESC']],
where: {
reportDeliveryDate: {
[Op.gte]: MIN_DELIVERY_DATE,
From fcac48585f6985773f0e5c5dbc9a369b42a10031 Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Thu, 2 Jan 2025 15:22:21 -0500
Subject: [PATCH 166/198] Deploy to main
---
.circleci/config.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.circleci/config.yml b/.circleci/config.yml
index c7ba7b6156..0b25009fcc 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -754,14 +754,14 @@ parameters:
type: string
staging_git_branch:
description: "Name of github branch that will deploy to staging"
- default: "main"
+ default: "al-ttahub-3603-activity-report-objective-citations"
type: string
dev_git_branch: # change to feature branch to test deployment
description: "Name of github branch that will deploy to dev"
default: "TTAHUB-3542/TTAHUB-3544/s3Queue-scanQueue-coverage"
type: string
sandbox_git_branch: # change to feature branch to test deployment
- default: "al-ttahub-3603-activity-report-objective-citations"
+ default: "main"
type: string
prod_new_relic_app_id:
default: "877570491"
From 4c84c5a6981322235c8d6e22fe17b836f848878f Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Fri, 3 Jan 2025 12:17:20 -0500
Subject: [PATCH 167/198] check for false on monitoring
---
frontend/src/pages/ActivityReport/Pages/components/GoalForm.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/src/pages/ActivityReport/Pages/components/GoalForm.js b/frontend/src/pages/ActivityReport/Pages/components/GoalForm.js
index 43adc96e95..0cafd23425 100644
--- a/frontend/src/pages/ActivityReport/Pages/components/GoalForm.js
+++ b/frontend/src/pages/ActivityReport/Pages/components/GoalForm.js
@@ -187,7 +187,7 @@ export default function GoalForm({
permissions={isCurated ? [
isSourceEditable,
!goal.onApprovedAR || !goal.source,
- citationOptions.length,
+ !isMonitoringGoal,
] : [!goal.onApprovedAR || !goal.source]}
label="Goal source"
value={goalSource}
From d90b82ddcf2ee466a50755128725ca37ba374c74 Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Tue, 7 Jan 2025 09:24:46 -0500
Subject: [PATCH 168/198] Initial pass
---
.../src/components/CitationDrawerContent.js | 11 +-
frontend/src/fetchers/citations.js | 4 +-
frontend/src/fetchers/monitoring.js | 6 +-
frontend/src/pages/RecipientRecord/index.js | 13 +-
.../RecipientRecord/pages/Monitoring/index.js | 25 +-
src/services/monitoring.ts | 406 ++++++++++--------
src/services/types/ttaByCitationTypes.ts | 2 +
7 files changed, 254 insertions(+), 213 deletions(-)
diff --git a/frontend/src/components/CitationDrawerContent.js b/frontend/src/components/CitationDrawerContent.js
index 19cea1de7b..3b4f8155c9 100644
--- a/frontend/src/components/CitationDrawerContent.js
+++ b/frontend/src/components/CitationDrawerContent.js
@@ -7,9 +7,10 @@ export default function CitationDrawerContent({ citations }) {
const [content, setContent] = useState([]); // { text: string, citation: string }[]
useEffect(() => {
+ const abortController = new AbortController();
async function fetchCitations() {
try {
- const response = await fetchCitationTextByName(citations);
+ const response = await fetchCitationTextByName(citations, abortController.signal);
setContent(response);
} catch (err) {
// eslint-disable-next-line no-console
@@ -17,7 +18,13 @@ export default function CitationDrawerContent({ citations }) {
}
}
- fetchCitations();
+ if (citations) {
+ fetchCitations();
+ }
+
+ return () => {
+ abortController.abort();
+ };
}, [citations]);
return (
diff --git a/frontend/src/fetchers/citations.js b/frontend/src/fetchers/citations.js
index 4d4f78648c..1178336759 100644
--- a/frontend/src/fetchers/citations.js
+++ b/frontend/src/fetchers/citations.js
@@ -22,7 +22,7 @@ export async function fetchCitationsByGrant(region, grantIds, reportStartDate) {
* @param {String[]} citationIds
* @returns {Promise<{ text: String; citation: String; }[]>}
*/
-export async function fetchCitationTextByName(citationIds) {
+export async function fetchCitationTextByName(citationIds, signal) {
const params = new URLSearchParams();
citationIds.forEach((name) => {
params.append('citationIds', encodeURIComponent(name));
@@ -35,6 +35,6 @@ export async function fetchCitationTextByName(citationIds) {
'text',
`?${params.toString()}`,
);
- const citations = await get(url);
+ const citations = await get(url, signal);
return citations.json();
}
diff --git a/frontend/src/fetchers/monitoring.js b/frontend/src/fetchers/monitoring.js
index 1b04477175..ffdecb2a5d 100644
--- a/frontend/src/fetchers/monitoring.js
+++ b/frontend/src/fetchers/monitoring.js
@@ -4,7 +4,7 @@ import { get } from '.';
const monitoringUrl = join('/', 'api', 'monitoring');
const classUrl = join('/', 'api', 'monitoring', 'class');
-export const getTtaByCitation = async (recipientId, regionId) => {
+export const getTtaByCitation = async (recipientId, regionId, signal) => {
const data = await get(
join(
monitoringUrl,
@@ -14,12 +14,13 @@ export const getTtaByCitation = async (recipientId, regionId) => {
'tta',
'citation',
),
+ signal,
);
return data.json();
};
-export const getTtaByReview = async (recipientId, regionId) => {
+export const getTtaByReview = async (recipientId, regionId, signal) => {
const data = await get(
join(
monitoringUrl,
@@ -29,6 +30,7 @@ export const getTtaByReview = async (recipientId, regionId) => {
'tta',
'review',
),
+ signal,
);
return data.json();
diff --git a/frontend/src/pages/RecipientRecord/index.js b/frontend/src/pages/RecipientRecord/index.js
index 53db86945d..92e70f06be 100644
--- a/frontend/src/pages/RecipientRecord/index.js
+++ b/frontend/src/pages/RecipientRecord/index.js
@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from 'react';
+import React, { useContext, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import ReactRouterPropTypes from 'react-router-prop-types';
import useDeepCompareEffect from 'use-deep-compare-effect';
@@ -25,6 +25,7 @@ import ViewGoals from './pages/ViewGoals';
import GoalNameForm from '../../components/GoalNameForm';
import Monitoring from './pages/Monitoring';
import FeatureFlag from '../../components/FeatureFlag';
+import AppLoadingContext from '../../AppLoadingContext';
export function PageWithHeading({
children,
@@ -81,7 +82,7 @@ export default function RecipientRecord({ match, hasAlerts }) {
const history = useHistory();
const { recipientId, regionId } = match.params;
- const [loading, setLoading] = useState(true);
+ const { setIsAppLoading } = useContext(AppLoadingContext);
const [recipientData, setRecipientData] = useState({
'grants.programSpecialistName': '',
'grants.id': '',
@@ -117,7 +118,7 @@ export default function RecipientRecord({ match, hasAlerts }) {
useDeepCompareEffect(() => {
async function fetchRecipient() {
try {
- setLoading(true);
+ setIsAppLoading(true);
const recipient = await getRecipient(recipientId, regionId);
if (recipient) {
setRecipientData({
@@ -127,7 +128,7 @@ export default function RecipientRecord({ match, hasAlerts }) {
} catch (e) {
history.push(`/something-went-wrong/${e.status}`);
} finally {
- setLoading(false);
+ setIsAppLoading(false);
}
}
@@ -144,10 +145,6 @@ export default function RecipientRecord({ match, hasAlerts }) {
const { recipientName } = recipientData;
const recipientNameWithRegion = `${recipientName} - Region ${regionId}`;
- if (loading) {
- return loading...
;
- }
-
return (
<>
diff --git a/frontend/src/pages/RecipientRecord/pages/Monitoring/index.js b/frontend/src/pages/RecipientRecord/pages/Monitoring/index.js
index 92b346cb10..8bcc8cc77f 100644
--- a/frontend/src/pages/RecipientRecord/pages/Monitoring/index.js
+++ b/frontend/src/pages/RecipientRecord/pages/Monitoring/index.js
@@ -2,13 +2,14 @@ import React, {
useEffect, useState, useMemo, useContext,
} from 'react';
import ReactRouterPropTypes from 'react-router-prop-types';
+import useDeepCompareEffect from 'use-deep-compare-effect';
import { useHistory } from 'react-router';
import Container from '../../../../components/Container';
import Tabs from '../../../../components/Tabs';
import { getTtaByCitation, getTtaByReview } from '../../../../fetchers/monitoring';
import ReviewCards from './components/ReviewCards';
import CitationCards from './components/CitationCards';
-import { ROUTES } from '../../../../Constants';
+// import { ROUTES } from '../../../../Constants';
import AppLoadingContext from '../../../../AppLoadingContext';
const MONITORING_PAGES = {
@@ -56,24 +57,30 @@ export default function Monitoring({
}
}, [currentPage, history, recipientId, regionId]);
- useEffect(() => {
+ useDeepCompareEffect(() => {
+ const controller = new AbortController();
async function fetchMonitoringData(slug) {
- const data = await lookup[slug].fetcher(recipientId, regionId);
- lookup[slug].setter(data);
- }
-
- if (currentPage && ALLOWED_PAGE_SLUGS.includes(currentPage)) {
setIsAppLoading(true);
try {
- fetchMonitoringData(currentPage);
+ const data = await lookup[slug].fetcher(recipientId, regionId, controller.signal);
+ lookup[slug].setter(data);
} catch (error) {
// eslint-disable-next-line no-console
console.error('Error fetching monitoring data:', error);
- history.push(`${ROUTES.SOMETHING_WENT_WRONG}/${error.status}`);
+ // todo: handle error (but not abort error)
+ // history.push(`${ROUTES.SOMETHING_WENT_WRONG}/${error.status}`);
} finally {
setIsAppLoading(false);
}
}
+
+ if (currentPage && ALLOWED_PAGE_SLUGS.includes(currentPage)) {
+ fetchMonitoringData(currentPage);
+ }
+
+ return () => {
+ controller.abort();
+ };
}, [currentPage, history, lookup, recipientId, regionId, setIsAppLoading]);
return (
diff --git a/src/services/monitoring.ts b/src/services/monitoring.ts
index 04dab72e0f..489720b6df 100644
--- a/src/services/monitoring.ts
+++ b/src/services/monitoring.ts
@@ -4,6 +4,7 @@ import moment from 'moment';
import { uniq, uniqBy } from 'lodash';
import { REPORT_STATUSES } from '@ttahub/common';
import db from '../models';
+import getCachedResponse from '../lib/cache';
import {
ITTAByReviewResponse,
IMonitoringReview,
@@ -29,6 +30,8 @@ const {
MonitoringClassSummary,
MonitoringFindingLink,
MonitoringFindingHistory,
+ MonitoringFindingHistoryStatus,
+ MonitoringFindingHistoryStatusLink,
MonitoringFinding,
MonitoringFindingGrant,
MonitoringFindingStatusLink,
@@ -47,8 +50,9 @@ const {
Role,
} = db;
-const MIN_DELIVERY_DATE = '2024-01-01';
+const MIN_DELIVERY_DATE = '2023-01-01';
const REVIEW_STATUS_COMPLETE = 'Complete';
+const EIGHT_HOURS = 60 * 60 * 8;
async function grantNumbersByRecipientAndRegion(recipientId: number, regionId: number) {
const grants = await Grant.findAll({
@@ -244,99 +248,104 @@ export async function ttaByReviews(
granteeIds,
} = await extractExternalData(recipientId, regionId);
- const reviews = await MonitoringReview.findAll({
- order: [['reportDeliveryDate', 'DESC']],
- where: {
- reportDeliveryDate: {
- [Op.gte]: MIN_DELIVERY_DATE,
+ const reviews = await getCachedResponse(
+ `ttaByReviews-${recipientId}-${regionId}-${grantNumbers.join('-')}`,
+ async () => JSON.stringify(await MonitoringReview.findAll({
+ order: [['reportDeliveryDate', 'DESC']],
+ where: {
+ reportDeliveryDate: {
+ [Op.gte]: MIN_DELIVERY_DATE,
+ },
},
- },
- include: [
- {
- model: MonitoringReviewStatusLink,
- as: 'statusLink',
- include: [
- {
- model: MonitoringReviewStatus,
- as: 'monitoringReviewStatuses',
- required: true,
- where: {
- name: REVIEW_STATUS_COMPLETE,
+ include: [
+ {
+ model: MonitoringReviewStatusLink,
+ as: 'statusLink',
+ include: [
+ {
+ model: MonitoringReviewStatus,
+ as: 'monitoringReviewStatuses',
+ required: true,
+ where: {
+ name: REVIEW_STATUS_COMPLETE,
+ },
},
- },
- ],
- },
- {
- model: MonitoringReviewLink,
- as: 'monitoringReviewLink',
- required: true,
- include: [
- {
- model: MonitoringReviewGrantee,
- as: 'monitoringReviewGrantees',
- required: true,
- where: {
- grantNumber: grantNumbers,
+ ],
+ },
+ {
+ model: MonitoringReviewLink,
+ as: 'monitoringReviewLink',
+ required: true,
+ include: [
+ {
+ model: MonitoringReviewGrantee,
+ as: 'monitoringReviewGrantees',
+ required: true,
+ where: {
+ grantNumber: grantNumbers,
+ },
},
- },
- {
- model: MonitoringFindingHistory,
- as: 'monitoringFindingHistories',
- required: true,
- include: [
- {
- model: MonitoringFindingLink,
- as: 'monitoringFindingLink',
- required: true,
- include: [
- {
- model: MonitoringFindingGrant,
- as: 'monitoringFindingGrants',
- where: {
- granteeId: granteeIds,
- },
- required: true,
- },
- {
- model: MonitoringFindingStandard,
- as: 'monitoringFindingStandards',
- include: [
- {
- model: MonitoringStandardLink,
- as: 'standardLink',
- include: [
- {
- model: MonitoringStandard,
- as: 'monitoringStandards',
- },
- ],
+ {
+ model: MonitoringFindingHistory,
+ as: 'monitoringFindingHistories',
+ required: true,
+ include: [
+ {
+ model: MonitoringFindingLink,
+ as: 'monitoringFindingLink',
+ required: true,
+ include: [
+ {
+ model: MonitoringFindingGrant,
+ as: 'monitoringFindingGrants',
+ where: {
+ granteeId: granteeIds,
},
- ],
- },
- {
- model: MonitoringFinding,
- as: 'monitoringFindings',
- include: [
- {
- model: MonitoringFindingStatusLink,
- as: 'statusLink',
- include: [
- {
- model: MonitoringFindingStatus,
- as: 'monitoringFindingStatuses',
- },
- ],
- },
- ],
- },
- ],
- },
- ],
- },
- ],
- },
- ],
- }) as MonitoringReviewType[];
+ required: true,
+ },
+ {
+ model: MonitoringFindingStandard,
+ as: 'monitoringFindingStandards',
+ include: [
+ {
+ model: MonitoringStandardLink,
+ as: 'standardLink',
+ include: [
+ {
+ model: MonitoringStandard,
+ as: 'monitoringStandards',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ model: MonitoringFinding,
+ as: 'monitoringFindings',
+ include: [
+ {
+ model: MonitoringFindingStatusLink,
+ as: 'statusLink',
+ include: [
+ {
+ model: MonitoringFindingStatus,
+ as: 'monitoringFindingStatuses',
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ })),
+ JSON.parse,
+ { EX: EIGHT_HOURS },
+ ) as unknown as MonitoringReviewType[];
return reviews.map((review) => {
const { monitoringReviewGrantees, monitoringFindingHistories } = review.monitoringReviewLink;
@@ -399,108 +408,125 @@ export async function ttaByCitations(
granteeIds,
} = await extractExternalData(recipientId, regionId);
- const citations = await MonitoringStandard.findAll({
- order: [['citation', 'ASC']],
- include: [
- {
- model: MonitoringStandardLink,
- as: 'standardLink',
- required: true,
- include: [
- {
- model: MonitoringFindingStandard,
- as: 'monitoringFindingStandards',
- required: true,
- include: [
- {
- model: MonitoringFindingLink,
- as: 'findingLink',
- required: true,
- include: [
- {
- model: MonitoringFinding,
- as: 'monitoringFindings',
- required: true,
- include: [
- {
- model: MonitoringFindingStatusLink,
- as: 'statusLink',
- required: true,
- include: [
- {
- model: MonitoringFindingStatus,
- as: 'monitoringFindingStatuses',
- required: true,
- },
- ],
+ const citations = await getCachedResponse(
+ `ttaByCitations-${recipientId}-${regionId}-${grantNumbers.join('-')}`,
+ async () => JSON.stringify(await MonitoringStandard.findAll({
+ order: [['citation', 'ASC']],
+ include: [
+ {
+ model: MonitoringStandardLink,
+ as: 'standardLink',
+ required: true,
+ include: [
+ {
+ model: MonitoringFindingStandard,
+ as: 'monitoringFindingStandards',
+ required: true,
+ include: [
+ {
+ model: MonitoringFindingLink,
+ as: 'findingLink',
+ required: true,
+ include: [
+ {
+ model: MonitoringFinding,
+ as: 'monitoringFindings',
+ required: true,
+ include: [
+ {
+ model: MonitoringFindingStatusLink,
+ as: 'statusLink',
+ required: true,
+ include: [
+ {
+ model: MonitoringFindingStatus,
+ as: 'monitoringFindingStatuses',
+ required: true,
+ },
+ ],
+ },
+ ],
+ },
+ {
+ model: MonitoringFindingGrant,
+ as: 'monitoringFindingGrants',
+ where: {
+ granteeId: granteeIds,
},
- ],
- },
- {
- model: MonitoringFindingGrant,
- as: 'monitoringFindingGrants',
- where: {
- granteeId: granteeIds,
+ required: true,
},
- required: true,
- },
- {
- model: MonitoringFindingHistory,
- as: 'monitoringFindingHistories',
- required: true,
- include: [
- {
- model: MonitoringReviewLink,
- as: 'monitoringReviewLink',
- required: true,
- include: [
- {
- model: MonitoringReview,
- as: 'monitoringReviews',
- required: true,
- where: {
- reportDeliveryDate: {
- [Op.gte]: MIN_DELIVERY_DATE,
- },
+ {
+ model: MonitoringFindingHistory,
+ as: 'monitoringFindingHistories',
+ required: true,
+ include: [
+ {
+ model: MonitoringFindingHistoryStatusLink,
+ as: 'monitoringFindingStatusLink',
+ required: true,
+ include: [
+ {
+ model: MonitoringFindingHistoryStatus,
+ as: 'monitoringFindingHistoryStatuses',
+ required: true,
},
- include: [
- {
- model: MonitoringReviewStatusLink,
- as: 'statusLink',
- required: true,
- include: [
- {
- model: MonitoringReviewStatus,
- as: 'monitoringReviewStatuses',
- required: true,
- where: {
- name: REVIEW_STATUS_COMPLETE,
+ ],
+ },
+ {
+ model: MonitoringReviewLink,
+ as: 'monitoringReviewLink',
+ required: true,
+ include: [
+ {
+ model: MonitoringReview,
+ as: 'monitoringReviews',
+ required: true,
+ where: {
+ reportDeliveryDate: {
+ [Op.gte]: MIN_DELIVERY_DATE,
+ },
+ },
+ include: [
+ {
+ model: MonitoringReviewStatusLink,
+ as: 'statusLink',
+ required: true,
+ include: [
+ {
+ model: MonitoringReviewStatus,
+ as: 'monitoringReviewStatuses',
+ required: true,
+ where: {
+ name: REVIEW_STATUS_COMPLETE,
+ },
},
- },
- ],
+ ],
+ },
+ ],
+ },
+ {
+ model: MonitoringReviewGrantee,
+ as: 'monitoringReviewGrantees',
+ required: true,
+ where: {
+ grantNumber: grantNumbers,
},
- ],
- },
- {
- model: MonitoringReviewGrantee,
- as: 'monitoringReviewGrantees',
- required: true,
- where: {
- grantNumber: grantNumbers,
},
- },
- ],
- },
- ],
- },
- ],
- },
- ],
- },
- ],
- },
- ],
- }) as MonitoringStandardType[];
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ })),
+ JSON.parse,
+ { EX: EIGHT_HOURS },
+ ) as unknown as MonitoringStandardType[];
return citations.map((citation) => {
const [findingStandard] = citation.standardLink.monitoringFindingStandards;
@@ -516,9 +542,11 @@ export async function ttaByCitations(
const [status] = finding.statusLink.monitoringFindingStatuses;
monitoringFindingHistories.forEach((history) => {
- const { monitoringReviewLink } = history;
+ const { monitoringReviewLink, monitoringFindingStatusLink } = history;
const { monitoringReviews } = monitoringReviewLink;
+ const [monitoringStatus] = monitoringFindingStatusLink.monitoringFindingHistoryStatuses;
+
const objectives = citationsOnActivityReports.filter((c) => c.findingIds.includes(finding.findingId));
objectives.forEach(({ endDate }) => {
if (!lastTTADate || moment(endDate, 'MM/DD/YYYY').isAfter(lastTTADate)) {
@@ -527,8 +555,6 @@ export async function ttaByCitations(
});
monitoringReviews.forEach((review) => {
- const { statusLink } = review;
- const [reviewStatus] = statusLink.monitoringReviewStatuses;
const { monitoringReviewGrantees } = monitoringReviewLink;
const gr = monitoringReviewGrantees.map((grantee) => grantee.grantNumber);
@@ -541,7 +567,7 @@ export async function ttaByCitations(
outcome: review.outcome,
specialists: uniqBy(objectives.map((o) => o.specialists).flat(), 'name'),
objectives: objectives.filter((o) => o.reviewNames.includes(review.name)),
- findingStatus: reviewStatus.name, // todo: is this the correct status to access?
+ findingStatus: monitoringStatus.name,
});
});
});
diff --git a/src/services/types/ttaByCitationTypes.ts b/src/services/types/ttaByCitationTypes.ts
index 2ce20d6a8f..054f670adf 100644
--- a/src/services/types/ttaByCitationTypes.ts
+++ b/src/services/types/ttaByCitationTypes.ts
@@ -77,6 +77,7 @@ export interface StatusLink {
deletedAt: null;
monitoringReviewStatuses?: MonitoringStatus[];
monitoringFindingStatuses?: MonitoringStatus[];
+ monitoringFindingHistoryStatuses?: MonitoringStatus[];
}
export interface MonitoringStatus {
@@ -108,6 +109,7 @@ export interface MonitoringFindingHistory {
updatedAt: Date;
deletedAt: null;
monitoringReviewLink: MonitoringReviewLink;
+ monitoringFindingStatusLink: StatusLink;
}
export interface MonitoringReviewLink {
From f9e4f274363af91ebdd2710508088c92c39ce03e Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Thu, 9 Jan 2025 15:51:10 -0500
Subject: [PATCH 169/198] sql for setting closed goals with some tests
---
src/tools/createMonitoringGoals.js | 85 ++++-
src/tools/createMonitoringGoals.test.js | 413 ++++++++++++++++++++++++
2 files changed, 496 insertions(+), 2 deletions(-)
diff --git a/src/tools/createMonitoringGoals.js b/src/tools/createMonitoringGoals.js
index f4fa4ac849..c12657ab3b 100644
--- a/src/tools/createMonitoringGoals.js
+++ b/src/tools/createMonitoringGoals.js
@@ -20,7 +20,7 @@ const createMonitoringGoals = async () => {
return;
}
- // Create monitoring goals for grants that need them.
+ // 1. Create monitoring goals for grants that need them.
await sequelize.transaction(async (transaction) => {
await sequelize.query(`
WITH
@@ -94,7 +94,7 @@ const createMonitoringGoals = async () => {
FROM new_goals;
`, { transaction });
- // Reopen monitoring goals for grants that need them.
+ // 2. Reopen monitoring goals for grants that need them.
await sequelize.query(`
WITH
grants_needing_goal_reopend AS (
@@ -147,6 +147,87 @@ const createMonitoringGoals = async () => {
FROM grants_needing_goal_reopend
WHERE "Goals".id = grants_needing_goal_reopend."goalId";
`, { transaction });
+
+ // 3. Close monitoring goals that no longer have any active citations and un-approved reports.
+ await sequelize.query(`
+ WITH
+ grants_with_monitoring_goal AS (
+ SELECT
+ gr.id "grantId",
+ gr.number,
+ g.id "goalId"
+ FROM "GoalTemplates" gt
+ JOIN "Goals" g
+ ON gt.id = g."goalTemplateId"
+ JOIN "Grants" gr
+ ON g."grantId" = gr.id
+ WHERE gt.standard = 'Monitoring'
+ AND g.status != 'Closed'
+ ),
+ with_no_active_reports AS (
+ SELECT
+ gwmg."grantId",
+ gwmg.number,
+ gwmg."goalId"
+ FROM grants_with_monitoring_goal gwmg
+ LEFT JOIN "ActivityReportGoals" arg
+ ON gwmg."goalId" = arg."goalId"
+ LEFT JOIN "ActivityReports" a
+ ON arg."activityReportId" = a.id
+ AND a."calculatedStatus" NOT IN ('deleted', 'approved')
+ WHERE a.id IS NULL
+ ),
+ with_active_citations AS (
+ SELECT
+ wnar."grantId",
+ wnar.number,
+ wnar."goalId"
+ FROM with_no_active_reports wnar
+ JOIN "GrantRelationshipToActive" grta
+ ON wnar."grantId" = grta."grantId"
+ OR wnar."grantId" = grta."activeGrantId"
+ JOIN "Grants" gr
+ ON grta."grantId" = gr.id
+ JOIN "MonitoringReviewGrantees" mrg
+ ON gr.number = mrg."grantNumber"
+ JOIN "MonitoringReviews" mr
+ ON mrg."reviewId" = mr."reviewId"
+ AND mr."reportDeliveryDate" BETWEEN '2024-01-01' AND NOW()
+ JOIN "MonitoringReviewStatuses" mrs
+ ON mr."statusId" = mrs."statusId"
+ AND mrs.name = 'Complete'
+ JOIN "MonitoringFindingHistories" mfh
+ ON mr."reviewId" = mfh."reviewId"
+ JOIN "MonitoringFindings" mf
+ ON mfh."findingId" = mf."findingId"
+ JOIN "MonitoringFindingStatuses" mfs
+ ON mf."statusId" = mfs."statusId"
+ AND mfs.name = 'Active'
+ JOIN "MonitoringFindingGrants" mfg
+ ON mf."findingId" = mfg."findingId"
+ AND mrg."granteeId" = mfg."granteeId"
+ ),
+ -- Because findings can have multiple reviews as different states.
+ -- It's better to first get everything that is still active and do a NOT EXISTS IN.
+ without_active_citations_and_reports AS (
+ SELECT
+ wnar."grantId",
+ wnar.number,
+ wnar."goalId"
+ FROM with_no_active_reports wnar
+ EXCEPT
+ SELECT
+ wac."grantId",
+ wac.number,
+ wac."goalId"
+ FROM with_active_citations wac
+ )
+ UPDATE "Goals"
+ SET "status" = 'Closed',
+ "updatedAt" = NOW()
+ FROM without_active_citations_and_reports
+ WHERE "Goals".id = without_active_citations_and_reports."goalId";
+ `, { transaction });
});
};
diff --git a/src/tools/createMonitoringGoals.test.js b/src/tools/createMonitoringGoals.test.js
index 2d44eece5c..9f3dfcbb6b 100644
--- a/src/tools/createMonitoringGoals.test.js
+++ b/src/tools/createMonitoringGoals.test.js
@@ -2,6 +2,7 @@
/* eslint-disable max-len */
import faker from '@faker-js/faker';
import { v4 as uuidv4 } from 'uuid';
+import { REPORT_STATUSES } from '@ttahub/common';
import createMonitoringGoals from './createMonitoringGoals';
import {
sequelize,
@@ -17,12 +18,58 @@ import {
MonitoringFindingGrant,
Goal,
GrantRelationshipToActive,
+ ActivityReport,
+ ActivityReportGoal,
+ User,
} from '../models';
import { captureSnapshot, rollbackToSnapshot } from '../lib/programmaticTransaction';
import { auditLogger } from '../logger';
jest.mock('../logger');
+const mockUser = {
+ id: 5874651,
+ homeRegionId: 1,
+ name: 'user5874665161',
+ hsesUsername: 'user5874665161',
+ hsesUserId: 'user5874665161',
+ lastLogin: new Date(),
+};
+
+const draftReport = {
+ activityRecipientType: 'recipient',
+ submissionStatus: REPORT_STATUSES.DRAFT,
+ userId: mockUser.id,
+ regionId: 1,
+ lastUpdatedById: mockUser.id,
+ ECLKCResourcesUsed: ['test'],
+ version: 2,
+ language: ['English', 'Spanish'],
+};
+
+const approvedReport = {
+ activityRecipientType: 'recipient',
+ userId: mockUser.id,
+ regionId: 1,
+ lastUpdatedById: mockUser.id,
+ ECLKCResourcesUsed: ['test'],
+ submissionStatus: REPORT_STATUSES.APPROVED,
+ calculatedStatus: REPORT_STATUSES.APPROVED,
+ oldApprovingManagerId: 1,
+ numberOfParticipants: 1,
+ deliveryMethod: 'method',
+ duration: 0,
+ endDate: '2020-09-01T12:00:00Z',
+ startDate: '2020-09-01T12:00:00Z',
+ requester: 'requester',
+ targetPopulations: ['pop'],
+ participants: ['participants'],
+ reason: ['Monitoring | Area of Concern', 'New Director or Management', 'New Program Option'],
+ topics: ['Child Screening and Assessment', 'Communication'],
+ ttaType: ['type'],
+ version: 2,
+};
+
describe('createMonitoringGoals', () => {
let recipient;
let recipientForSplitCase10;
@@ -54,6 +101,10 @@ describe('createMonitoringGoals', () => {
let grantBeingMerged11C;
// Make sure if a monitoring goal is closed but meets the criteria for a finding its re-open'd.
let grantReopenMonitoringGoal12;
+ // Make sure we close a monitoring goal if it has no active citations AND un-approved reports.
+ let grantClosedMonitoringGoal13;
+ let grantToNotCloseMonitoringGoal14;
+ let grantWithApprovedReportsButOpenCitations15;
const grantThatNeedsMonitoringGoalNumber1 = faker.datatype.string(4);
const grantThatAlreadyHasMonitoringGoalNumber2 = faker.datatype.string(4);
@@ -71,6 +122,9 @@ describe('createMonitoringGoals', () => {
const grantBeingMergedNumber11B = uuidv4();
const grantBeingMergedNumber11C = uuidv4();
const grantReopenMonitoringGoalNumber12 = uuidv4();
+ const grantClosedMonitoringGoalNumber13 = uuidv4();
+ const grantToNotCloseMonitoringGoalNumber14 = uuidv4();
+ const grantWithApprovedReportsButOpenCitationsNumber15 = uuidv4();
let snapShot;
@@ -78,6 +132,9 @@ describe('createMonitoringGoals', () => {
// Create a snapshot of the database so we can rollback after the tests.
snapShot = await captureSnapshot();
+ // Create user.
+ const mockUserDb = await User.create(mockUser, { validate: false }, { individualHooks: false });
+
// Recipient.
recipient = await Recipient.create({
id: faker.datatype.number({ min: 64000 }),
@@ -256,6 +313,36 @@ describe('createMonitoringGoals', () => {
endDate: new Date(),
status: 'Active',
},
+ {
+ // 13
+ id: faker.datatype.number({ min: 9999 }),
+ number: grantClosedMonitoringGoalNumber13,
+ recipientId: recipient.id,
+ regionId: 1,
+ startDate: new Date(),
+ endDate: new Date(),
+ status: 'Active',
+ },
+ {
+ // 14
+ id: faker.datatype.number({ min: 9999 }),
+ number: grantToNotCloseMonitoringGoalNumber14,
+ recipientId: recipient.id,
+ regionId: 1,
+ startDate: new Date(),
+ endDate: new Date(),
+ status: 'Active',
+ },
+ {
+ // 15
+ id: faker.datatype.number({ min: 9999 }),
+ number: grantWithApprovedReportsButOpenCitationsNumber15,
+ recipientId: recipient.id,
+ regionId: 1,
+ startDate: new Date(),
+ endDate: new Date(),
+ status: 'Active',
+ },
]);
[
@@ -275,8 +362,30 @@ describe('createMonitoringGoals', () => {
grantBeingMerged11B,
grantBeingMerged11C,
grantReopenMonitoringGoal12,
+ grantClosedMonitoringGoal13,
+ grantToNotCloseMonitoringGoal14,
+ grantWithApprovedReportsButOpenCitations15,
] = grants;
+ // Create an activity report that uses grantToNotCloseMonitoringGoal14.
+ const dontCloseGoalAr = await ActivityReport.create({
+ ...draftReport,
+ lastUpdatedById: mockUserDb.id,
+ submissionStatus: REPORT_STATUSES.DRAFT,
+ calculatedStatus: REPORT_STATUSES.DRAFT,
+ userId: mockUserDb.id,
+ activityRecipients: [{ activityRecipientId: recipient.id }],
+ });
+
+ const dontCloseGoalCitations = await ActivityReport.create({
+ ...approvedReport,
+ lastUpdatedById: mockUserDb.id,
+ submissionStatus: REPORT_STATUSES.APPROVED,
+ calculatedStatus: REPORT_STATUSES.APPROVED,
+ userId: mockUserDb.id,
+ activityRecipients: [{ activityRecipientId: recipient.id }],
+ });
+
// Create an inactive grant that has 'cdi' true that points to inactiveGrantThatHasBeenReplacedByActiveGrant9.
const cdiGrant = await Grant.create({
id: faker.datatype.number({ min: 9999 }),
@@ -350,6 +459,9 @@ describe('createMonitoringGoals', () => {
const grantBeingMergedNumberGranteeId11B = uuidv4();
const grantBeingMergedNumberGranteeId11C = uuidv4();
const grantReopenMonitoringGoalNumberGranteeId12 = uuidv4();
+ const grantClosedMonitoringGoalNumberGranteeId13 = uuidv4();
+ const grantToNotCloseMonitoringGoalNumberGranteeId14 = uuidv4();
+ const grantWithApprovedReportsButOpenCitationsNumberGranteeId15 = uuidv4();
// reviewId GUID.
const grantThatNeedsMonitoringGoalNumberReviewId1 = uuidv4();
@@ -368,6 +480,9 @@ describe('createMonitoringGoals', () => {
const grantBeingMergedNumberReviewId11B = uuidv4();
const grantBeingMergedNumberReviewId11C = uuidv4();
const grantReopenMonitoringGoalNumberReviewId12 = uuidv4();
+ const grantClosedMonitoringGoalNumberReviewId13 = uuidv4();
+ const grantToNotCloseMonitoringGoalNumberReviewId14 = uuidv4();
+ const grantWithApprovedReportsButOpenCitationsNumberReviewId15 = uuidv4();
// MonitoringReviewGrantee.
await MonitoringReviewGrantee.bulkCreate([
@@ -552,6 +667,42 @@ describe('createMonitoringGoals', () => {
sourceCreatedAt: new Date(),
sourceUpdatedAt: new Date(),
},
+ {
+ // 13
+ id: faker.datatype.number({ min: 999999 }),
+ grantNumber: grantClosedMonitoringGoalNumber13,
+ reviewId: grantClosedMonitoringGoalNumberReviewId13,
+ granteeId: grantClosedMonitoringGoalNumberGranteeId13,
+ createTime: new Date(),
+ updateTime: new Date(),
+ updateBy: 'Support Team',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 14
+ id: faker.datatype.number({ min: 999999 }),
+ grantNumber: grantToNotCloseMonitoringGoalNumber14,
+ reviewId: grantToNotCloseMonitoringGoalNumberReviewId14,
+ granteeId: grantToNotCloseMonitoringGoalNumberGranteeId14,
+ createTime: new Date(),
+ updateTime: new Date(),
+ updateBy: 'Support Team',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 15
+ id: faker.datatype.number({ min: 999999 }),
+ grantNumber: grantWithApprovedReportsButOpenCitationsNumber15,
+ reviewId: grantWithApprovedReportsButOpenCitationsNumberReviewId15,
+ granteeId: grantWithApprovedReportsButOpenCitationsNumberGranteeId15,
+ createTime: new Date(),
+ updateTime: new Date(),
+ updateBy: 'Support Team',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
], { individualHooks: true });
// Create 9 statusId variables made up of 6 random numbers and set each one to its corresponding statusId below in MonitoringReview.bulkCreate.
@@ -570,6 +721,9 @@ describe('createMonitoringGoals', () => {
const status11B = 13;
const status11C = 14;
const status12 = 15;
+ const status13 = 16;
+ const status14 = 17;
+ const status15 = 18;
// MonitoringReview.
// Allowed review types:
@@ -824,6 +978,54 @@ describe('createMonitoringGoals', () => {
sourceCreatedAt: new Date(),
sourceUpdatedAt: new Date(),
},
+ {
+ // 13
+ reviewId: grantClosedMonitoringGoalNumberReviewId13,
+ contentId: uuidv4(),
+ statusId: status13,
+ name: faker.random.words(3),
+ startDate: new Date(),
+ endDate: new Date(),
+ reviewType: 'RAN',
+ reportDeliveryDate: new Date(),
+ reportAttachmentId: uuidv4(),
+ outcome: faker.random.words(5),
+ hash: uuidv4(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 14
+ reviewId: grantToNotCloseMonitoringGoalNumberReviewId14,
+ contentId: uuidv4(),
+ statusId: status14,
+ name: faker.random.words(3),
+ startDate: new Date(),
+ endDate: new Date(),
+ reviewType: 'RAN',
+ reportDeliveryDate: new Date(),
+ reportAttachmentId: uuidv4(),
+ outcome: faker.random.words(5),
+ hash: uuidv4(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 15
+ reviewId: grantWithApprovedReportsButOpenCitationsNumberReviewId15,
+ contentId: uuidv4(),
+ statusId: status15,
+ name: faker.random.words(3),
+ startDate: new Date(),
+ endDate: new Date(),
+ reviewType: 'RAN',
+ reportDeliveryDate: new Date(),
+ reportAttachmentId: uuidv4(),
+ outcome: faker.random.words(5),
+ hash: uuidv4(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
], { individualHooks: true });
// MonitoringReviewStatus.
@@ -933,6 +1135,27 @@ describe('createMonitoringGoals', () => {
sourceCreatedAt: new Date(),
sourceUpdatedAt: new Date(),
},
+ {
+ // 13
+ statusId: status13,
+ name: 'Complete',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 14
+ statusId: status14,
+ name: 'Complete',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 15
+ statusId: status15,
+ name: 'Complete',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
], { individualHooks: true });
// MonitoringFindingHistory.
@@ -951,6 +1174,9 @@ describe('createMonitoringGoals', () => {
const findingId11B = uuidv4();
const findingId11C = uuidv4();
const findingId12 = uuidv4();
+ const findingId13 = uuidv4();
+ const findingId14 = uuidv4();
+ const findingId15 = uuidv4();
// Exclude findingId10C from the findingIds array below.
await MonitoringFindingHistory.bulkCreate([
@@ -1158,6 +1384,45 @@ describe('createMonitoringGoals', () => {
sourceCreatedAt: new Date(),
sourceUpdatedAt: new Date(),
},
+ {
+ // 13
+ reviewId: grantClosedMonitoringGoalNumberReviewId13,
+ findingHistoryId: uuidv4(),
+ findingId: findingId13,
+ statusId: status13,
+ narrative: faker.random.words(10),
+ ordinal: faker.datatype.number({ min: 1, max: 10 }),
+ determination: faker.random.words(5),
+ hash: uuidv4(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 14
+ reviewId: grantToNotCloseMonitoringGoalNumberReviewId14,
+ findingHistoryId: uuidv4(),
+ findingId: findingId14,
+ statusId: status14,
+ narrative: faker.random.words(10),
+ ordinal: faker.datatype.number({ min: 1, max: 10 }),
+ determination: faker.random.words(5),
+ hash: uuidv4(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 15
+ reviewId: grantWithApprovedReportsButOpenCitationsNumberReviewId15,
+ findingHistoryId: uuidv4(),
+ findingId: findingId15,
+ statusId: status15,
+ narrative: faker.random.words(10),
+ ordinal: faker.datatype.number({ min: 1, max: 10 }),
+ determination: faker.random.words(5),
+ hash: uuidv4(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
], { individualHooks: true });
// MonitoringFinding.
@@ -1297,6 +1562,33 @@ describe('createMonitoringGoals', () => {
sourceCreatedAt: new Date(),
sourceUpdatedAt: new Date(),
},
+ {
+ // 13
+ findingId: findingId13,
+ statusId: status13,
+ findingType: faker.random.word(),
+ hash: uuidv4(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 14
+ findingId: findingId14,
+ statusId: status14,
+ findingType: faker.random.word(),
+ hash: uuidv4(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 15
+ findingId: findingId15,
+ statusId: status15,
+ findingType: faker.random.word(),
+ hash: uuidv4(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
], { individualHooks: true });
// MonitoringFindingStatus.
@@ -1392,6 +1684,27 @@ describe('createMonitoringGoals', () => {
sourceCreatedAt: new Date(),
sourceUpdatedAt: new Date(),
},
+ {
+ // 13
+ statusId: status13,
+ name: 'Closed',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 14
+ statusId: status14,
+ name: 'Closed',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 15
+ statusId: status15,
+ name: 'Active',
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
], { individualHooks: true });
// MonitoringFindingGrant.
@@ -1615,6 +1928,48 @@ describe('createMonitoringGoals', () => {
sourceCreatedAt: new Date(),
sourceUpdatedAt: new Date(),
},
+ {
+ // 13
+ findingId: findingId13,
+ granteeId: grantClosedMonitoringGoalNumberGranteeId13,
+ statusId: status13,
+ findingType: faker.random.word(),
+ source: faker.random.word(),
+ correctionDeadLine: new Date(),
+ reportedDate: new Date(),
+ closedDate: null,
+ hash: uuidv4(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 14
+ findingId: findingId14,
+ granteeId: grantToNotCloseMonitoringGoalNumberGranteeId14,
+ statusId: status14,
+ findingType: faker.random.word(),
+ source: faker.random.word(),
+ correctionDeadLine: new Date(),
+ reportedDate: new Date(),
+ closedDate: null,
+ hash: uuidv4(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
+ {
+ // 15
+ findingId: findingId15,
+ granteeId: grantWithApprovedReportsButOpenCitationsNumberGranteeId15,
+ statusId: status15,
+ findingType: faker.random.word(),
+ source: faker.random.word(),
+ correctionDeadLine: new Date(),
+ reportedDate: new Date(),
+ closedDate: null,
+ hash: uuidv4(),
+ sourceCreatedAt: new Date(),
+ sourceUpdatedAt: new Date(),
+ },
], { individualHooks: true });
// Retrieve the goal template.
@@ -1663,6 +2018,46 @@ describe('createMonitoringGoals', () => {
goalTemplateId: goalTemplate.id,
status: 'Closed',
});
+
+ // Create a monitoring goal to be closed that no longer has any active citations or un-approved reports.
+ const closedGoal = await Goal.create({
+ id: faker.datatype.number({ min: 9999 }),
+ name: goalTemplateName,
+ grantId: grantClosedMonitoringGoal13.id,
+ goalTemplateId: goalTemplate.id,
+ status: 'Not started',
+ });
+
+ // Goal with no active citations but has a report.
+ const goalWithNoActiveCitationsButReport = await Goal.create({
+ id: faker.datatype.number({ min: 9999 }),
+ name: goalTemplateName,
+ grantId: grantToNotCloseMonitoringGoal14.id,
+ goalTemplateId: goalTemplate.id,
+ status: 'Not started',
+ });
+ // Create ActivityReportGoal.
+ await ActivityReportGoal.create({
+ activityReportId: dontCloseGoalAr.id,
+ goalId: goalWithNoActiveCitationsButReport.id,
+ isActivelyEdited: false,
+ });
+
+ // Goal with active citations and an approved report.
+ const goalWithActiveCitationsAndApprovedReport = await Goal.create({
+ id: faker.datatype.number({ min: 9999 }),
+ name: goalTemplateName,
+ grantId: grantWithApprovedReportsButOpenCitations15.id,
+ goalTemplateId: goalTemplate.id,
+ status: 'Not started',
+ });
+
+ // Create ActivityReportGoal.
+ await ActivityReportGoal.create({
+ activityReportId: dontCloseGoalCitations.id,
+ goalId: goalWithActiveCitationsAndApprovedReport.id,
+ isActivelyEdited: false,
+ });
});
afterEach(() => {
@@ -1765,6 +2160,24 @@ describe('createMonitoringGoals', () => {
expect(grant12Goals.length).toBe(1);
expect(grant12Goals[0].goalTemplateId).toBe(goalTemplate.id);
expect(grant12Goals[0].status).toBe('Not Started');
+
+ // CASE 13: Closes monitoring goal that no longer has any active citations.
+ const grant13Goals = await Goal.findAll({ where: { grantId: grantClosedMonitoringGoal13.id } });
+ expect(grant13Goals.length).toBe(1);
+ expect(grant13Goals[0].goalTemplateId).toBe(goalTemplate.id);
+ expect(grant13Goals[0].status).toBe('Closed');
+
+ // CASE 14: Monitoring goal wiht no active citations but has a unapproved report (don't close).
+ const grant14Goals = await Goal.findAll({ where: { grantId: grantToNotCloseMonitoringGoal14.id } });
+ expect(grant14Goals.length).toBe(1);
+ expect(grant14Goals[0].goalTemplateId).toBe(goalTemplate.id);
+ expect(grant14Goals[0].status).toBe('Not started');
+
+ // CASE 15: Monitoring goal with active citations and an approved report (don't close).
+ const grant15Goals = await Goal.findAll({ where: { grantId: grantWithApprovedReportsButOpenCitations15.id } });
+ expect(grant15Goals.length).toBe(1);
+ expect(grant15Goals[0].goalTemplateId).toBe(goalTemplate.id);
+ expect(grant15Goals[0].status).toBe('Not started');
};
it('creates monitoring goals for grants that need them', async () => {
From eb085ed713e8783976d8a11cd9cf0257ecc7b882 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Thu, 9 Jan 2025 16:15:31 -0500
Subject: [PATCH 170/198] rename migrations
---
...4-AddMonitoringEnum.js => 20250109205626-AddMonitoringEnum.js} | 0
... => 20250109205626-add-activity-report-objective-citations.js} | 0
...-import.js => 20250109205626-add-post-processing-to-import.js} | 0
....js => 20250109205626-create-standard-goal-template-column.js} | 0
4 files changed, 0 insertions(+), 0 deletions(-)
rename src/migrations/{20241207194714-AddMonitoringEnum.js => 20250109205626-AddMonitoringEnum.js} (100%)
rename src/migrations/{20241207194714-add-activity-report-objective-citations.js => 20250109205626-add-activity-report-objective-citations.js} (100%)
rename src/migrations/{20241207194714-add-post-processing-to-import.js => 20250109205626-add-post-processing-to-import.js} (100%)
rename src/migrations/{20241207194714-create-standard-goal-template-column.js => 20250109205626-create-standard-goal-template-column.js} (100%)
diff --git a/src/migrations/20241207194714-AddMonitoringEnum.js b/src/migrations/20250109205626-AddMonitoringEnum.js
similarity index 100%
rename from src/migrations/20241207194714-AddMonitoringEnum.js
rename to src/migrations/20250109205626-AddMonitoringEnum.js
diff --git a/src/migrations/20241207194714-add-activity-report-objective-citations.js b/src/migrations/20250109205626-add-activity-report-objective-citations.js
similarity index 100%
rename from src/migrations/20241207194714-add-activity-report-objective-citations.js
rename to src/migrations/20250109205626-add-activity-report-objective-citations.js
diff --git a/src/migrations/20241207194714-add-post-processing-to-import.js b/src/migrations/20250109205626-add-post-processing-to-import.js
similarity index 100%
rename from src/migrations/20241207194714-add-post-processing-to-import.js
rename to src/migrations/20250109205626-add-post-processing-to-import.js
diff --git a/src/migrations/20241207194714-create-standard-goal-template-column.js b/src/migrations/20250109205626-create-standard-goal-template-column.js
similarity index 100%
rename from src/migrations/20241207194714-create-standard-goal-template-column.js
rename to src/migrations/20250109205626-create-standard-goal-template-column.js
From 1f4b47f0223ab8be44b2fbec11fd9fa32a115fbd Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Thu, 9 Jan 2025 17:37:35 -0500
Subject: [PATCH 171/198] per Garrett we need to update goals using hooks
---
src/tools/createMonitoringGoals.js | 47 ++++++++++++++++++++++--------
1 file changed, 35 insertions(+), 12 deletions(-)
diff --git a/src/tools/createMonitoringGoals.js b/src/tools/createMonitoringGoals.js
index c12657ab3b..f9eb568150 100644
--- a/src/tools/createMonitoringGoals.js
+++ b/src/tools/createMonitoringGoals.js
@@ -1,6 +1,7 @@
import {
sequelize,
GoalTemplate,
+ Goal,
} from '../models';
import { auditLogger } from '../logger';
@@ -95,7 +96,7 @@ const createMonitoringGoals = async () => {
`, { transaction });
// 2. Reopen monitoring goals for grants that need them.
- await sequelize.query(`
+ const goalsToOpen = await sequelize.query(`
WITH
grants_needing_goal_reopend AS (
SELECT
@@ -141,15 +142,26 @@ const createMonitoringGoals = async () => {
AND g.status IN ('Closed', 'Suspended')
GROUP BY 1
)
- UPDATE "Goals"
- SET "status" = 'Not Started',
- "updatedAt" = NOW()
- FROM grants_needing_goal_reopend
- WHERE "Goals".id = grants_needing_goal_reopend."goalId";
+ SELECT "goalId"
+ FROM grants_needing_goal_reopend;
`, { transaction });
+ // Set reopened goals via Sequelize so we ensure the hooks fire.
+ if (goalsToOpen[0].length > 0) {
+ const goalsToOpenIds = goalsToOpen[0].map((goal) => goal.goalId);
+ await Goal.update({
+ status: 'Not Started',
+ }, {
+ where: {
+ id: goalsToOpenIds,
+ },
+ individualHooks: true,
+ transaction,
+ });
+ }
+
// 3. Close monitoring goals that no longer have any active citations and un-approved reports.
- await sequelize.query(`
+ const goalsToClose = await sequelize.query(`
WITH
grants_with_monitoring_goal AS (
SELECT
@@ -222,12 +234,23 @@ const createMonitoringGoals = async () => {
wac."goalId"
FROM with_active_citations wac
)
- UPDATE "Goals"
- SET "status" = 'Closed',
- "updatedAt" = NOW()
- FROM without_active_citations_and_reports
- WHERE "Goals".id = without_active_citations_and_reports."goalId";
+ SELECT "goalId"
+ FROM without_active_citations_and_reports;
`, { transaction });
+
+ // Set closed goals via Sequelize so we ensure the hooks fire.
+ if (goalsToClose[0].length > 0) {
+ const goalsToCloseIds = goalsToClose[0].map((goal) => goal.goalId);
+ await Goal.update({
+ status: 'Closed',
+ }, {
+ where: {
+ id: goalsToCloseIds,
+ },
+ individualHooks: true,
+ transaction,
+ });
+ }
});
};
From ab74abadcd2558c62abef450d9e3677e0096abab Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Fri, 10 Jan 2025 10:46:11 -0500
Subject: [PATCH 172/198] Update monitoring test
---
src/services/monitoring.testHelpers.ts | 20 +-
src/services/monitoring.ts | 405 ++++++++++++-------------
2 files changed, 213 insertions(+), 212 deletions(-)
diff --git a/src/services/monitoring.testHelpers.ts b/src/services/monitoring.testHelpers.ts
index 0dd341d3f3..dfb6e44aa6 100644
--- a/src/services/monitoring.testHelpers.ts
+++ b/src/services/monitoring.testHelpers.ts
@@ -20,18 +20,16 @@ const {
MonitoringStandard,
MonitoringStandardLink,
MonitoringFindingStatusLink,
+ MonitoringFindingHistoryStatus,
MonitoringFindingStatus,
+ MonitoringFindingHistoryStatusLink,
Objective,
Grant,
ActivityReportObjective,
- Goal,
ActivityReportObjectiveTopic,
ActivityReportObjectiveCitation,
Topic,
- ActivityReport,
ActivityReportCollaborator,
- User,
- Role,
} = db;
async function createAdditionalMonitoringData(
@@ -83,6 +81,18 @@ async function createAdditionalMonitoringData(
...timestamps,
});
+ await MonitoringFindingHistoryStatusLink.findOrCreate({
+ where: {
+ statusId: 6006,
+ },
+ });
+
+ await MonitoringFindingHistoryStatus.create({
+ statusId: 6006,
+ name: 'Complete',
+ ...timestamps,
+ });
+
await MonitoringStandard.create({
contentId: 'contentId',
standardId: 99_999,
@@ -128,6 +138,8 @@ async function destroyAdditionalMonitoringData(findingId: string, reviewId: stri
await MonitoringFindingStandard.destroy({ where: { findingId }, force: true });
await MonitoringFindingStatus.destroy({ where: { statusId: 6006 }, force: true });
await MonitoringFindingGrant.destroy({ where: { findingId }, force: true });
+ await MonitoringFindingHistoryStatus.destroy({ where: { statusId: 6006 }, force: true });
+ await MonitoringFindingHistoryStatusLink.destroy({ where: { statusId: 6006 }, force: true });
await MonitoringFindingHistory.destroy({ where: { reviewId }, force: true });
await MonitoringStandard.destroy({ where: { standardId: 99_999 }, force: true });
await MonitoringFinding.destroy({ where: { findingId }, force: true });
diff --git a/src/services/monitoring.ts b/src/services/monitoring.ts
index 489720b6df..6083c8f036 100644
--- a/src/services/monitoring.ts
+++ b/src/services/monitoring.ts
@@ -4,7 +4,6 @@ import moment from 'moment';
import { uniq, uniqBy } from 'lodash';
import { REPORT_STATUSES } from '@ttahub/common';
import db from '../models';
-import getCachedResponse from '../lib/cache';
import {
ITTAByReviewResponse,
IMonitoringReview,
@@ -248,104 +247,99 @@ export async function ttaByReviews(
granteeIds,
} = await extractExternalData(recipientId, regionId);
- const reviews = await getCachedResponse(
- `ttaByReviews-${recipientId}-${regionId}-${grantNumbers.join('-')}`,
- async () => JSON.stringify(await MonitoringReview.findAll({
- order: [['reportDeliveryDate', 'DESC']],
- where: {
- reportDeliveryDate: {
- [Op.gte]: MIN_DELIVERY_DATE,
- },
+ const reviews = await MonitoringReview.findAll({
+ order: [['reportDeliveryDate', 'DESC']],
+ where: {
+ reportDeliveryDate: {
+ [Op.gte]: MIN_DELIVERY_DATE,
},
- include: [
- {
- model: MonitoringReviewStatusLink,
- as: 'statusLink',
- include: [
- {
- model: MonitoringReviewStatus,
- as: 'monitoringReviewStatuses',
- required: true,
- where: {
- name: REVIEW_STATUS_COMPLETE,
- },
+ },
+ include: [
+ {
+ model: MonitoringReviewStatusLink,
+ as: 'statusLink',
+ include: [
+ {
+ model: MonitoringReviewStatus,
+ as: 'monitoringReviewStatuses',
+ required: true,
+ where: {
+ name: REVIEW_STATUS_COMPLETE,
},
- ],
- },
- {
- model: MonitoringReviewLink,
- as: 'monitoringReviewLink',
- required: true,
- include: [
- {
- model: MonitoringReviewGrantee,
- as: 'monitoringReviewGrantees',
- required: true,
- where: {
- grantNumber: grantNumbers,
- },
+ },
+ ],
+ },
+ {
+ model: MonitoringReviewLink,
+ as: 'monitoringReviewLink',
+ required: true,
+ include: [
+ {
+ model: MonitoringReviewGrantee,
+ as: 'monitoringReviewGrantees',
+ required: true,
+ where: {
+ grantNumber: grantNumbers,
},
- {
- model: MonitoringFindingHistory,
- as: 'monitoringFindingHistories',
- required: true,
- include: [
- {
- model: MonitoringFindingLink,
- as: 'monitoringFindingLink',
- required: true,
- include: [
- {
- model: MonitoringFindingGrant,
- as: 'monitoringFindingGrants',
- where: {
- granteeId: granteeIds,
- },
- required: true,
- },
- {
- model: MonitoringFindingStandard,
- as: 'monitoringFindingStandards',
- include: [
- {
- model: MonitoringStandardLink,
- as: 'standardLink',
- include: [
- {
- model: MonitoringStandard,
- as: 'monitoringStandards',
- },
- ],
- },
- ],
- },
- {
- model: MonitoringFinding,
- as: 'monitoringFindings',
- include: [
- {
- model: MonitoringFindingStatusLink,
- as: 'statusLink',
- include: [
- {
- model: MonitoringFindingStatus,
- as: 'monitoringFindingStatuses',
- },
- ],
- },
- ],
+ },
+ {
+ model: MonitoringFindingHistory,
+ as: 'monitoringFindingHistories',
+ required: true,
+ include: [
+ {
+ model: MonitoringFindingLink,
+ as: 'monitoringFindingLink',
+ required: true,
+ include: [
+ {
+ model: MonitoringFindingGrant,
+ as: 'monitoringFindingGrants',
+ where: {
+ granteeId: granteeIds,
},
- ],
- },
- ],
- },
- ],
- },
- ],
- })),
- JSON.parse,
- { EX: EIGHT_HOURS },
- ) as unknown as MonitoringReviewType[];
+ required: true,
+ },
+ {
+ model: MonitoringFindingStandard,
+ as: 'monitoringFindingStandards',
+ include: [
+ {
+ model: MonitoringStandardLink,
+ as: 'standardLink',
+ include: [
+ {
+ model: MonitoringStandard,
+ as: 'monitoringStandards',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ model: MonitoringFinding,
+ as: 'monitoringFindings',
+ include: [
+ {
+ model: MonitoringFindingStatusLink,
+ as: 'statusLink',
+ include: [
+ {
+ model: MonitoringFindingStatus,
+ as: 'monitoringFindingStatuses',
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ }) as MonitoringReviewType[];
return reviews.map((review) => {
const { monitoringReviewGrantees, monitoringFindingHistories } = review.monitoringReviewLink;
@@ -408,125 +402,120 @@ export async function ttaByCitations(
granteeIds,
} = await extractExternalData(recipientId, regionId);
- const citations = await getCachedResponse(
- `ttaByCitations-${recipientId}-${regionId}-${grantNumbers.join('-')}`,
- async () => JSON.stringify(await MonitoringStandard.findAll({
- order: [['citation', 'ASC']],
- include: [
- {
- model: MonitoringStandardLink,
- as: 'standardLink',
- required: true,
- include: [
- {
- model: MonitoringFindingStandard,
- as: 'monitoringFindingStandards',
- required: true,
- include: [
- {
- model: MonitoringFindingLink,
- as: 'findingLink',
- required: true,
- include: [
- {
- model: MonitoringFinding,
- as: 'monitoringFindings',
- required: true,
- include: [
- {
- model: MonitoringFindingStatusLink,
- as: 'statusLink',
- required: true,
- include: [
- {
- model: MonitoringFindingStatus,
- as: 'monitoringFindingStatuses',
- required: true,
- },
- ],
- },
- ],
- },
- {
- model: MonitoringFindingGrant,
- as: 'monitoringFindingGrants',
- where: {
- granteeId: granteeIds,
+ const citations = await MonitoringStandard.findAll({
+ order: [['citation', 'ASC']],
+ include: [
+ {
+ model: MonitoringStandardLink,
+ as: 'standardLink',
+ required: true,
+ include: [
+ {
+ model: MonitoringFindingStandard,
+ as: 'monitoringFindingStandards',
+ required: true,
+ include: [
+ {
+ model: MonitoringFindingLink,
+ as: 'findingLink',
+ required: true,
+ include: [
+ {
+ model: MonitoringFinding,
+ as: 'monitoringFindings',
+ required: true,
+ include: [
+ {
+ model: MonitoringFindingStatusLink,
+ as: 'statusLink',
+ required: true,
+ include: [
+ {
+ model: MonitoringFindingStatus,
+ as: 'monitoringFindingStatuses',
+ required: true,
+ },
+ ],
},
- required: true,
+ ],
+ },
+ {
+ model: MonitoringFindingGrant,
+ as: 'monitoringFindingGrants',
+ where: {
+ granteeId: granteeIds,
},
- {
- model: MonitoringFindingHistory,
- as: 'monitoringFindingHistories',
- required: true,
- include: [
- {
- model: MonitoringFindingHistoryStatusLink,
- as: 'monitoringFindingStatusLink',
- required: true,
- include: [
- {
- model: MonitoringFindingHistoryStatus,
- as: 'monitoringFindingHistoryStatuses',
- required: true,
- },
- ],
- },
- {
- model: MonitoringReviewLink,
- as: 'monitoringReviewLink',
- required: true,
- include: [
- {
- model: MonitoringReview,
- as: 'monitoringReviews',
- required: true,
- where: {
- reportDeliveryDate: {
- [Op.gte]: MIN_DELIVERY_DATE,
- },
+ required: true,
+ },
+ {
+ model: MonitoringFindingHistory,
+ as: 'monitoringFindingHistories',
+ required: true,
+ include: [
+ {
+ model: MonitoringFindingHistoryStatusLink,
+ as: 'monitoringFindingStatusLink',
+ required: true,
+ include: [
+ {
+ model: MonitoringFindingHistoryStatus,
+ as: 'monitoringFindingHistoryStatuses',
+ required: true,
+ },
+ ],
+ },
+ {
+ model: MonitoringReviewLink,
+ as: 'monitoringReviewLink',
+ required: true,
+ include: [
+ {
+ model: MonitoringReview,
+ as: 'monitoringReviews',
+ required: true,
+ where: {
+ reportDeliveryDate: {
+ [Op.gte]: MIN_DELIVERY_DATE,
},
- include: [
- {
- model: MonitoringReviewStatusLink,
- as: 'statusLink',
- required: true,
- include: [
- {
- model: MonitoringReviewStatus,
- as: 'monitoringReviewStatuses',
- required: true,
- where: {
- name: REVIEW_STATUS_COMPLETE,
- },
- },
- ],
- },
- ],
},
- {
- model: MonitoringReviewGrantee,
- as: 'monitoringReviewGrantees',
- required: true,
- where: {
- grantNumber: grantNumbers,
+ include: [
+ {
+ model: MonitoringReviewStatusLink,
+ as: 'statusLink',
+ required: true,
+ include: [
+ {
+ model: MonitoringReviewStatus,
+ as: 'monitoringReviewStatuses',
+ required: true,
+ where: {
+ name: REVIEW_STATUS_COMPLETE,
+ },
+ },
+ ],
},
+ ],
+ },
+ {
+ model: MonitoringReviewGrantee,
+ as: 'monitoringReviewGrantees',
+ required: true,
+ where: {
+ grantNumber: grantNumbers,
},
- ],
- },
- ],
- },
- ],
- },
- ],
- },
- ],
- },
- ],
- })),
- JSON.parse,
- { EX: EIGHT_HOURS },
- ) as unknown as MonitoringStandardType[];
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ }) as MonitoringStandardType[];
return citations.map((citation) => {
const [findingStandard] = citation.standardLink.monitoringFindingStandards;
From b73ee3bf9d63d84d25068774876f3db59831ab31 Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Fri, 10 Jan 2025 10:48:18 -0500
Subject: [PATCH 173/198] Update migration
---
...-import.js => 20250110144804-add-post-processing-to-import.js} | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename src/migrations/{20241207194714-add-post-processing-to-import.js => 20250110144804-add-post-processing-to-import.js} (100%)
diff --git a/src/migrations/20241207194714-add-post-processing-to-import.js b/src/migrations/20250110144804-add-post-processing-to-import.js
similarity index 100%
rename from src/migrations/20241207194714-add-post-processing-to-import.js
rename to src/migrations/20250110144804-add-post-processing-to-import.js
From ca46b9bcd7a16f34282259f3cfa1ce13e7823d0e Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Fri, 10 Jan 2025 10:52:29 -0500
Subject: [PATCH 174/198] Update frontend tests
---
frontend/src/pages/RecipientRecord/pages/GoalsObjectives.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/src/pages/RecipientRecord/pages/GoalsObjectives.js b/frontend/src/pages/RecipientRecord/pages/GoalsObjectives.js
index 171f28792a..76607464f3 100644
--- a/frontend/src/pages/RecipientRecord/pages/GoalsObjectives.js
+++ b/frontend/src/pages/RecipientRecord/pages/GoalsObjectives.js
@@ -38,7 +38,7 @@ export default function GoalsObjectives({
const filtersToApply = expandFilters(filters);
let hasActiveGrants = false;
- if (recipient.grants.find((g) => g.status === 'Active')) {
+ if (recipient.grants && recipient.grants.find((g) => g.status === 'Active')) {
hasActiveGrants = true;
}
From 0ecbe6b701d132a3be7262f9ac9ed3edfde86a98 Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Fri, 10 Jan 2025 11:22:59 -0500
Subject: [PATCH 175/198] Update migration timestamps
---
...4-AddMonitoringEnum.js => 20250110162036-AddMonitoringEnum.js} | 0
... => 20250110162153-add-activity-report-objective-citations.js} | 0
....js => 20250110162226-create-standard-goal-template-column.js} | 0
3 files changed, 0 insertions(+), 0 deletions(-)
rename src/migrations/{20241207194714-AddMonitoringEnum.js => 20250110162036-AddMonitoringEnum.js} (100%)
rename src/migrations/{20241207194714-add-activity-report-objective-citations.js => 20250110162153-add-activity-report-objective-citations.js} (100%)
rename src/migrations/{20241207194714-create-standard-goal-template-column.js => 20250110162226-create-standard-goal-template-column.js} (100%)
diff --git a/src/migrations/20241207194714-AddMonitoringEnum.js b/src/migrations/20250110162036-AddMonitoringEnum.js
similarity index 100%
rename from src/migrations/20241207194714-AddMonitoringEnum.js
rename to src/migrations/20250110162036-AddMonitoringEnum.js
diff --git a/src/migrations/20241207194714-add-activity-report-objective-citations.js b/src/migrations/20250110162153-add-activity-report-objective-citations.js
similarity index 100%
rename from src/migrations/20241207194714-add-activity-report-objective-citations.js
rename to src/migrations/20250110162153-add-activity-report-objective-citations.js
diff --git a/src/migrations/20241207194714-create-standard-goal-template-column.js b/src/migrations/20250110162226-create-standard-goal-template-column.js
similarity index 100%
rename from src/migrations/20241207194714-create-standard-goal-template-column.js
rename to src/migrations/20250110162226-create-standard-goal-template-column.js
From 2b388d4819b5056a4f3c7e980fc659a47cbdbb0c Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Fri, 10 Jan 2025 12:05:10 -0500
Subject: [PATCH 176/198] Check for recipient data
---
frontend/src/components/GoalNameForm/index.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/src/components/GoalNameForm/index.js b/frontend/src/components/GoalNameForm/index.js
index 3749667e78..6639292695 100644
--- a/frontend/src/components/GoalNameForm/index.js
+++ b/frontend/src/components/GoalNameForm/index.js
@@ -42,7 +42,7 @@ export default function GoalNameForm({
const history = useHistory();
// we need to memoize this as it is a dependency for the useDeepCompareEffect below
- const possibleGrants = useMemo(() => recipient.grants.filter(((g) => g.status === 'Active')), [recipient.grants]);
+ const possibleGrants = useMemo(() => (recipient.grants || []).filter(((g) => g.status === 'Active')), [recipient.grants]);
// watch the selected grants
const { selectedGrant, isGoalNameEditable, goalIds } = hookForm.watch();
From a7d24b7a80501ca50e6ba20eafb153ca68b992f5 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Mon, 13 Jan 2025 10:53:09 -0500
Subject: [PATCH 177/198] add Garretts changes with fix
---
src/goalServices/changeGoalStatus.test.js | 78 ++++++++++++++++++-----
src/goalServices/changeGoalStatus.ts | 47 ++++++++------
src/tools/createMonitoringGoals.js | 17 +++++
3 files changed, 107 insertions(+), 35 deletions(-)
diff --git a/src/goalServices/changeGoalStatus.test.js b/src/goalServices/changeGoalStatus.test.js
index ee5c8d593f..3407d9d726 100644
--- a/src/goalServices/changeGoalStatus.test.js
+++ b/src/goalServices/changeGoalStatus.test.js
@@ -16,6 +16,7 @@ describe('changeGoalStatus service', () => {
let user;
let role;
let goal;
+ let goal2;
let grant;
let recipient;
const newStatus = 'In Progress';
@@ -41,6 +42,11 @@ describe('changeGoalStatus service', () => {
status: 'Draft',
grantId: grant.id,
});
+ goal2 = await db.Goal.create({
+ name: 'Plant a tree',
+ status: 'Draft',
+ grantId: grant.id,
+ });
role = await db.Role.create({
id: faker.datatype.number(),
name: 'Astronaut',
@@ -53,17 +59,17 @@ describe('changeGoalStatus service', () => {
});
afterAll(async () => {
- await db.Goal.destroy({ where: { id: goal.id }, force: true });
+ await db.GoalStatusChange.destroy({ where: { goalId: [goal.id, goal2.id] }, force: true });
+ await db.Goal.destroy({ where: { id: [goal.id, goal2.id] }, force: true });
await db.GrantNumberLink.destroy({ where: { grantId: grant.id }, force: true });
await db.Grant.destroy({ where: { id: grant.id }, individualHooks: true });
await db.Recipient.destroy({ where: { id: recipient.id } });
await db.UserRole.destroy({ where: { userId: user.id } });
await db.Role.destroy({ where: { id: role.id } });
await db.User.destroy({ where: { id: mockUser.id } });
- await db.sequelize.close();
});
- it('should change the status of a goal and create a status change log', async () => {
+ it('should change the status of a goal and create a status change log for a valid user', async () => {
await changeGoalStatus({
goalId: goal.id,
userId: mockUser.id,
@@ -75,6 +81,7 @@ describe('changeGoalStatus service', () => {
const updatedGoal = await db.Goal.findByPk(goal.id);
const statusChangeLog = await db.GoalStatusChange.findOne({
where: { goalId: goal.id, newStatus },
+ order: [['id', 'DESC']], // Order by `id` in descending order to get the newest value.
});
expect(updatedGoal.status).toBe(newStatus);
@@ -88,23 +95,64 @@ describe('changeGoalStatus service', () => {
expect(statusChangeLog.userRoles).toStrictEqual(['Astronaut']);
});
- it('should throw an error if the goal does not exist', async () => {
- await expect(changeGoalStatus({
- goalId: 9999, // non-existent goalId
- userId: mockUser.id,
+ it('should change the status of a goal and create a status change log for system user', async () => {
+ await changeGoalStatus({
+ goalId: goal2.id,
+ userId: -1, // System user
newStatus,
reason,
context,
- })).rejects.toThrow('Goal or user not found');
+ });
+
+ const updatedGoal = await db.Goal.findByPk(goal2.id);
+ const statusChangeLog = await db.GoalStatusChange.findOne({
+ where: { goalId: goal2.id, newStatus },
+ });
+
+ expect(updatedGoal.status).toBe(newStatus);
+ expect(statusChangeLog).toBeTruthy();
+ expect(statusChangeLog.oldStatus).toBe('Draft');
+ expect(statusChangeLog.newStatus).toBe(newStatus);
+ expect(statusChangeLog.reason).toBe(reason);
+ expect(statusChangeLog.context).toBe(context);
+ expect(statusChangeLog.userId).toBeNull(); // userId should be null for system user
+ expect(statusChangeLog.userName).toBe('system'); // userName should be "system"
+ expect(statusChangeLog.userRoles).toBeNull(); // userRoles should be null for system user
+ });
+
+ it('should throw an error if the goal does not exist', async () => {
+ await expect(
+ changeGoalStatus({
+ goalId: 9999, // non-existent goalId
+ userId: mockUser.id,
+ newStatus,
+ reason,
+ context,
+ }),
+ ).rejects.toThrow('Goal not found');
});
it('should throw an error if the user does not exist', async () => {
- await expect(changeGoalStatus({
- goalId: goal.id,
- userId: 9999, // non-existent userId
- newStatus,
- reason,
- context,
- })).rejects.toThrow('Goal or user not found');
+ await expect(
+ changeGoalStatus({
+ goalId: goal.id,
+ userId: 9999, // non-existent userId
+ newStatus,
+ reason,
+ context,
+ }),
+ ).rejects.toThrow('User not found');
+ });
+
+ it('should throw an error if userId is null', async () => {
+ await expect(
+ changeGoalStatus({
+ goalId: goal.id,
+ userId: null, // Invalid userId
+ newStatus,
+ reason,
+ context,
+ }),
+ ).rejects.toThrow('User not found');
});
});
diff --git a/src/goalServices/changeGoalStatus.ts b/src/goalServices/changeGoalStatus.ts
index 6084ec7dd9..705cf5c873 100644
--- a/src/goalServices/changeGoalStatus.ts
+++ b/src/goalServices/changeGoalStatus.ts
@@ -3,7 +3,7 @@ import db from '../models';
interface GoalStatusChangeParams {
goalId: number;
- userId: number;
+ userId: number; // Always required
newStatus: string;
reason: string;
context: string;
@@ -12,40 +12,47 @@ interface GoalStatusChangeParams {
export default async function changeGoalStatus({
goalId,
- userId = 1,
+ userId,
newStatus,
reason,
context,
}: GoalStatusChangeParams) {
const [user, goal] = await Promise.all([
- db.User.findOne({
- where: { id: userId },
- attributes: ['id', 'name'],
- include: [
- {
- model: db.Role,
- as: 'roles',
- attributes: ['name'],
- through: {
- attributes: [],
+ userId !== -1
+ ? db.User.findOne({
+ where: { id: userId },
+ attributes: ['id', 'name'],
+ include: [
+ {
+ model: db.Role,
+ as: 'roles',
+ attributes: ['name'],
+ through: {
+ attributes: [],
+ },
},
- },
- ],
- }),
+ ],
+ })
+ : null, // Skip user lookup if userId is -1
db.Goal.findByPk(goalId),
]);
- if (!goal || !user) {
- throw new Error('Goal or user not found');
+ if (!goal) {
+ throw new Error('Goal not found');
+ }
+
+ if (!user && userId !== -1) {
+ throw new Error('User not found');
}
const oldStatus = goal.status;
await db.GoalStatusChange.create({
goalId,
- userId,
- userName: user.name,
- userRoles: user.roles.map((role) => role.name),
+ userId: userId === -1 ? null : user.id, // Use null for -1, otherwise the user's ID
+ userName: userId === -1 ? 'system' : user.name, // Use "system" for -1, otherwise the user's name
+ // eslint-disable-next-line max-len
+ userRoles: userId === -1 ? null : user.roles.map((role) => role.name), // Use null for -1, otherwise the user's roles
oldStatus,
newStatus,
reason,
diff --git a/src/tools/createMonitoringGoals.js b/src/tools/createMonitoringGoals.js
index f9eb568150..c5fac002d7 100644
--- a/src/tools/createMonitoringGoals.js
+++ b/src/tools/createMonitoringGoals.js
@@ -4,6 +4,7 @@ import {
Goal,
} from '../models';
import { auditLogger } from '../logger';
+import changeGoalStatus from '../goalServices/changeGoalStatus';
const createMonitoringGoals = async () => {
const cutOffDate = '2024-01-01'; // TODO: Set this before we deploy to prod.
@@ -158,6 +159,14 @@ const createMonitoringGoals = async () => {
individualHooks: true,
transaction,
});
+
+ await Promise.all(goalsToOpen[0].map((goal) => changeGoalStatus({
+ goalId: goal.goalId,
+ userId: -1, // -1 is used to define the system is initiating the change
+ newStatus: 'Not Started',
+ reason: 'Active monitoring citations',
+ context: null,
+ })));
}
// 3. Close monitoring goals that no longer have any active citations and un-approved reports.
@@ -250,6 +259,14 @@ const createMonitoringGoals = async () => {
individualHooks: true,
transaction,
});
+
+ await Promise.all(goalsToClose[0].map((goal) => changeGoalStatus({
+ goalId: goal.goalId,
+ userId: -1, // -1 is used to define the system is initiating the change
+ newStatus: 'Closed',
+ reason: 'No active monitoring citations',
+ context: null,
+ })));
}
});
};
From b7b1f7cd72645e028b55383cb5a8c4682dcf8a52 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Mon, 13 Jan 2025 13:35:22 -0500
Subject: [PATCH 178/198] update code and test
---
src/goalServices/changeGoalStatus.ts | 1 +
src/tools/createMonitoringGoals.js | 24 ++++-----------------
src/tools/createMonitoringGoals.test.js | 28 ++++++++++++++++++++++---
3 files changed, 30 insertions(+), 23 deletions(-)
diff --git a/src/goalServices/changeGoalStatus.ts b/src/goalServices/changeGoalStatus.ts
index 705cf5c873..5aa6d50b85 100644
--- a/src/goalServices/changeGoalStatus.ts
+++ b/src/goalServices/changeGoalStatus.ts
@@ -8,6 +8,7 @@ interface GoalStatusChangeParams {
reason: string;
context: string;
transaction?: Sequelize.Transaction;
+ oldStatusToUse?: string | null;
}
export default async function changeGoalStatus({
diff --git a/src/tools/createMonitoringGoals.js b/src/tools/createMonitoringGoals.js
index c5fac002d7..1cfd3e1f81 100644
--- a/src/tools/createMonitoringGoals.js
+++ b/src/tools/createMonitoringGoals.js
@@ -150,16 +150,8 @@ const createMonitoringGoals = async () => {
// Set reopened goals via Sequelize so we ensure the hooks fire.
if (goalsToOpen[0].length > 0) {
const goalsToOpenIds = goalsToOpen[0].map((goal) => goal.goalId);
- await Goal.update({
- status: 'Not Started',
- }, {
- where: {
- id: goalsToOpenIds,
- },
- individualHooks: true,
- transaction,
- });
-
+ // This function also updates the status of the goal via the hook.
+ // No need to explicitly update the goal status.
await Promise.all(goalsToOpen[0].map((goal) => changeGoalStatus({
goalId: goal.goalId,
userId: -1, // -1 is used to define the system is initiating the change
@@ -250,16 +242,8 @@ const createMonitoringGoals = async () => {
// Set closed goals via Sequelize so we ensure the hooks fire.
if (goalsToClose[0].length > 0) {
const goalsToCloseIds = goalsToClose[0].map((goal) => goal.goalId);
- await Goal.update({
- status: 'Closed',
- }, {
- where: {
- id: goalsToCloseIds,
- },
- individualHooks: true,
- transaction,
- });
-
+ // This function also updates the status of the goal via the hook.
+ // No need to explicitly update the goal status.
await Promise.all(goalsToClose[0].map((goal) => changeGoalStatus({
goalId: goal.goalId,
userId: -1, // -1 is used to define the system is initiating the change
diff --git a/src/tools/createMonitoringGoals.test.js b/src/tools/createMonitoringGoals.test.js
index 9f3dfcbb6b..f5d2e3413b 100644
--- a/src/tools/createMonitoringGoals.test.js
+++ b/src/tools/createMonitoringGoals.test.js
@@ -21,6 +21,7 @@ import {
ActivityReport,
ActivityReportGoal,
User,
+ GoalStatusChange,
} from '../models';
import { captureSnapshot, rollbackToSnapshot } from '../lib/programmaticTransaction';
import { auditLogger } from '../logger';
@@ -126,6 +127,9 @@ describe('createMonitoringGoals', () => {
const grantToNotCloseMonitoringGoalNumber14 = uuidv4();
const grantWithApprovedReportsButOpenCitationsNumber15 = uuidv4();
+ let goalForReopen12;
+ let goalForClose13;
+
let snapShot;
beforeAll(async () => {
@@ -2011,7 +2015,7 @@ describe('createMonitoringGoals', () => {
});
// Create a monitoring goal for grantReopenMonitoringGoalNumberReviewId12 in case 12 thats closed and should be set to Not started.
- await Goal.create({
+ goalForReopen12 = await Goal.create({
id: faker.datatype.number({ min: 9999 }),
name: goalTemplateName,
grantId: grantReopenMonitoringGoal12.id,
@@ -2020,7 +2024,7 @@ describe('createMonitoringGoals', () => {
});
// Create a monitoring goal to be closed that no longer has any active citations or un-approved reports.
- const closedGoal = await Goal.create({
+ goalForClose13 = await Goal.create({
id: faker.datatype.number({ min: 9999 }),
name: goalTemplateName,
grantId: grantClosedMonitoringGoal13.id,
@@ -2161,13 +2165,31 @@ describe('createMonitoringGoals', () => {
expect(grant12Goals[0].goalTemplateId).toBe(goalTemplate.id);
expect(grant12Goals[0].status).toBe('Not Started');
+ // Ensure the correct GoalChangeStatus has been created.
+ const goalChangeStatus12 = await GoalStatusChange.findOne({ where: { goalId: goalForReopen12.id } });
+ expect(goalChangeStatus12).not.toBeNull();
+ expect(goalChangeStatus12.userId).toBeNull();
+ expect(goalChangeStatus12.oldStatus).toBe('Closed');
+ expect(goalChangeStatus12.newStatus).toBe('Not Started');
+ expect(goalChangeStatus12.userName).toBe('system');
+ expect(goalChangeStatus12.reason).toBe('Active monitoring citations');
+
// CASE 13: Closes monitoring goal that no longer has any active citations.
const grant13Goals = await Goal.findAll({ where: { grantId: grantClosedMonitoringGoal13.id } });
expect(grant13Goals.length).toBe(1);
expect(grant13Goals[0].goalTemplateId).toBe(goalTemplate.id);
expect(grant13Goals[0].status).toBe('Closed');
- // CASE 14: Monitoring goal wiht no active citations but has a unapproved report (don't close).
+ // Ensure the correct GoalChangeStatus has been created.
+ const goalChangeStatus13 = await GoalStatusChange.findOne({ where: { goalId: goalForClose13.id } });
+ expect(goalChangeStatus13).not.toBeNull();
+ expect(goalChangeStatus13.userId).toBeNull();
+ expect(goalChangeStatus13.oldStatus).toBe('Not started');
+ expect(goalChangeStatus13.newStatus).toBe('Closed');
+ expect(goalChangeStatus13.userName).toBe('system');
+ expect(goalChangeStatus13.reason).toBe('No active monitoring citations');
+
+ // CASE 14: Monitoring goal with no active citations but has a unapproved report (don't close).
const grant14Goals = await Goal.findAll({ where: { grantId: grantToNotCloseMonitoringGoal14.id } });
expect(grant14Goals.length).toBe(1);
expect(grant14Goals[0].goalTemplateId).toBe(goalTemplate.id);
From f9da545cb8c12f462693a947da9898064fa08079 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Mon, 13 Jan 2025 16:19:29 -0500
Subject: [PATCH 179/198] change things a bit to allow null user for status
change
---
src/goalServices/changeGoalStatus.test.js | 24 -----------------------
src/goalServices/changeGoalStatus.ts | 20 +++++++------------
2 files changed, 7 insertions(+), 37 deletions(-)
diff --git a/src/goalServices/changeGoalStatus.test.js b/src/goalServices/changeGoalStatus.test.js
index 3407d9d726..a1daa8df3f 100644
--- a/src/goalServices/changeGoalStatus.test.js
+++ b/src/goalServices/changeGoalStatus.test.js
@@ -131,28 +131,4 @@ describe('changeGoalStatus service', () => {
}),
).rejects.toThrow('Goal not found');
});
-
- it('should throw an error if the user does not exist', async () => {
- await expect(
- changeGoalStatus({
- goalId: goal.id,
- userId: 9999, // non-existent userId
- newStatus,
- reason,
- context,
- }),
- ).rejects.toThrow('User not found');
- });
-
- it('should throw an error if userId is null', async () => {
- await expect(
- changeGoalStatus({
- goalId: goal.id,
- userId: null, // Invalid userId
- newStatus,
- reason,
- context,
- }),
- ).rejects.toThrow('User not found');
- });
});
diff --git a/src/goalServices/changeGoalStatus.ts b/src/goalServices/changeGoalStatus.ts
index 5aa6d50b85..3519a26e71 100644
--- a/src/goalServices/changeGoalStatus.ts
+++ b/src/goalServices/changeGoalStatus.ts
@@ -3,7 +3,7 @@ import db from '../models';
interface GoalStatusChangeParams {
goalId: number;
- userId: number; // Always required
+ userId: number; // Always required, unless it's a system change.
newStatus: string;
reason: string;
context: string;
@@ -13,13 +13,13 @@ interface GoalStatusChangeParams {
export default async function changeGoalStatus({
goalId,
- userId,
+ userId = null,
newStatus,
reason,
context,
}: GoalStatusChangeParams) {
const [user, goal] = await Promise.all([
- userId !== -1
+ userId
? db.User.findOne({
where: { id: userId },
attributes: ['id', 'name'],
@@ -34,7 +34,7 @@ export default async function changeGoalStatus({
},
],
})
- : null, // Skip user lookup if userId is -1
+ : null,
db.Goal.findByPk(goalId),
]);
@@ -42,18 +42,12 @@ export default async function changeGoalStatus({
throw new Error('Goal not found');
}
- if (!user && userId !== -1) {
- throw new Error('User not found');
- }
-
const oldStatus = goal.status;
-
await db.GoalStatusChange.create({
goalId,
- userId: userId === -1 ? null : user.id, // Use null for -1, otherwise the user's ID
- userName: userId === -1 ? 'system' : user.name, // Use "system" for -1, otherwise the user's name
- // eslint-disable-next-line max-len
- userRoles: userId === -1 ? null : user.roles.map((role) => role.name), // Use null for -1, otherwise the user's roles
+ userId: !user ? null : user.id,
+ userName: !user ? 'system' : user.name,
+ userRoles: !user ? null : user.roles.map((role) => role.name),
oldStatus,
newStatus,
reason,
From 3b43cbccaf473949d40344f712bc0cd6ab29eba9 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Tue, 14 Jan 2025 11:56:38 -0500
Subject: [PATCH 180/198] implemented agreed upon solution
---
src/goalServices/changeGoalStatus.test.js | 69 ++++++++++++--------
src/goalServices/changeGoalStatus.ts | 77 ++++++++++++++++-------
src/tools/createMonitoringGoals.js | 8 +--
3 files changed, 99 insertions(+), 55 deletions(-)
diff --git a/src/goalServices/changeGoalStatus.test.js b/src/goalServices/changeGoalStatus.test.js
index a1daa8df3f..c8e7c1d49f 100644
--- a/src/goalServices/changeGoalStatus.test.js
+++ b/src/goalServices/changeGoalStatus.test.js
@@ -1,5 +1,5 @@
import faker from '@faker-js/faker';
-import changeGoalStatus from './changeGoalStatus';
+import changeGoalStatus, { changeGoalStatusWithSystemUser } from './changeGoalStatus';
import db from '../models';
const fakeName = faker.name.firstName() + faker.name.lastName();
@@ -16,7 +16,7 @@ describe('changeGoalStatus service', () => {
let user;
let role;
let goal;
- let goal2;
+ let systemChangedGoal;
let grant;
let recipient;
const newStatus = 'In Progress';
@@ -42,8 +42,8 @@ describe('changeGoalStatus service', () => {
status: 'Draft',
grantId: grant.id,
});
- goal2 = await db.Goal.create({
- name: 'Plant a tree',
+ systemChangedGoal = await db.Goal.create({
+ name: 'Change status using system user',
status: 'Draft',
grantId: grant.id,
});
@@ -59,17 +59,17 @@ describe('changeGoalStatus service', () => {
});
afterAll(async () => {
- await db.GoalStatusChange.destroy({ where: { goalId: [goal.id, goal2.id] }, force: true });
- await db.Goal.destroy({ where: { id: [goal.id, goal2.id] }, force: true });
+ await db.Goal.destroy({ where: { id: [goal.id, systemChangedGoal.id] }, force: true });
await db.GrantNumberLink.destroy({ where: { grantId: grant.id }, force: true });
await db.Grant.destroy({ where: { id: grant.id }, individualHooks: true });
await db.Recipient.destroy({ where: { id: recipient.id } });
await db.UserRole.destroy({ where: { userId: user.id } });
await db.Role.destroy({ where: { id: role.id } });
await db.User.destroy({ where: { id: mockUser.id } });
+ await db.sequelize.close();
});
- it('should change the status of a goal and create a status change log for a valid user', async () => {
+ it('should change the status of a goal and create a status change log', async () => {
await changeGoalStatus({
goalId: goal.id,
userId: mockUser.id,
@@ -81,7 +81,6 @@ describe('changeGoalStatus service', () => {
const updatedGoal = await db.Goal.findByPk(goal.id);
const statusChangeLog = await db.GoalStatusChange.findOne({
where: { goalId: goal.id, newStatus },
- order: [['id', 'DESC']], // Order by `id` in descending order to get the newest value.
});
expect(updatedGoal.status).toBe(newStatus);
@@ -95,18 +94,37 @@ describe('changeGoalStatus service', () => {
expect(statusChangeLog.userRoles).toStrictEqual(['Astronaut']);
});
- it('should change the status of a goal and create a status change log for system user', async () => {
- await changeGoalStatus({
- goalId: goal2.id,
- userId: -1, // System user
+ it('should throw an error if the goal does not exist', async () => {
+ await expect(changeGoalStatus({
+ goalId: 9999, // non-existent goalId
+ userId: mockUser.id,
+ newStatus,
+ reason,
+ context,
+ })).rejects.toThrow('Goal or user not found');
+ });
+
+ it('should throw an error if the user does not exist', async () => {
+ await expect(changeGoalStatus({
+ goalId: goal.id,
+ userId: 9999, // non-existent userId
+ newStatus,
+ reason,
+ context,
+ })).rejects.toThrow('Goal or user not found');
+ });
+
+ it('changeGoalStatusWithSystemUser should change the status of a goal and create a status change log', async () => {
+ await changeGoalStatusWithSystemUser({
+ goalId: systemChangedGoal.id,
newStatus,
reason,
context,
});
- const updatedGoal = await db.Goal.findByPk(goal2.id);
+ const updatedGoal = await db.Goal.findByPk(systemChangedGoal.id);
const statusChangeLog = await db.GoalStatusChange.findOne({
- where: { goalId: goal2.id, newStatus },
+ where: { goalId: systemChangedGoal.id, newStatus },
});
expect(updatedGoal.status).toBe(newStatus);
@@ -115,20 +133,17 @@ describe('changeGoalStatus service', () => {
expect(statusChangeLog.newStatus).toBe(newStatus);
expect(statusChangeLog.reason).toBe(reason);
expect(statusChangeLog.context).toBe(context);
- expect(statusChangeLog.userId).toBeNull(); // userId should be null for system user
- expect(statusChangeLog.userName).toBe('system'); // userName should be "system"
- expect(statusChangeLog.userRoles).toBeNull(); // userRoles should be null for system user
+ expect(statusChangeLog.userId).toBe(null);
+ expect(statusChangeLog.userName).toBe('system');
+ expect(statusChangeLog.userRoles).toBe(null);
});
- it('should throw an error if the goal does not exist', async () => {
- await expect(
- changeGoalStatus({
- goalId: 9999, // non-existent goalId
- userId: mockUser.id,
- newStatus,
- reason,
- context,
- }),
- ).rejects.toThrow('Goal not found');
+ it('changeGoalStatusWithSystemUser should throw an error if the goal does not exist', async () => {
+ await expect(changeGoalStatusWithSystemUser({
+ goalId: 9999, // non-existent goalId
+ newStatus,
+ reason,
+ context,
+ })).rejects.toThrow('Goal not found');
});
});
diff --git a/src/goalServices/changeGoalStatus.ts b/src/goalServices/changeGoalStatus.ts
index 3519a26e71..d4bf3b1105 100644
--- a/src/goalServices/changeGoalStatus.ts
+++ b/src/goalServices/changeGoalStatus.ts
@@ -3,51 +3,82 @@ import db from '../models';
interface GoalStatusChangeParams {
goalId: number;
- userId: number; // Always required, unless it's a system change.
+ userId: number;
newStatus: string;
reason: string;
context: string;
transaction?: Sequelize.Transaction;
- oldStatusToUse?: string | null;
+}
+
+export async function changeGoalStatusWithSystemUser({
+ goalId,
+ newStatus,
+ reason,
+ context,
+}: GoalStatusChangeParams) {
+ // Lookup goal.
+ const goal = await db.Goal.findByPk(goalId);
+
+ // Error if goal not found.
+ if (!goal) {
+ throw new Error('Goal not found');
+ }
+
+ // Change goal status.
+ await db.GoalStatusChange.create({
+ goalId: goal.id,
+ userId: null, // For now we will use null to prevent FK constraint violation.
+ userName: 'system',
+ userRoles: null,
+ oldStatus: goal.status,
+ newStatus,
+ reason,
+ context,
+ });
+
+ // Reload goal.
+ await goal.reload();
+
+ // Return goal.
+ return goal;
}
export default async function changeGoalStatus({
goalId,
- userId = null,
+ userId = 1,
newStatus,
reason,
context,
}: GoalStatusChangeParams) {
const [user, goal] = await Promise.all([
- userId
- ? db.User.findOne({
- where: { id: userId },
- attributes: ['id', 'name'],
- include: [
- {
- model: db.Role,
- as: 'roles',
- attributes: ['name'],
- through: {
- attributes: [],
- },
+ db.User.findOne({
+ where: { id: userId },
+ attributes: ['id', 'name'],
+ include: [
+ {
+ model: db.Role,
+ as: 'roles',
+ attributes: ['name'],
+ through: {
+ attributes: [],
},
- ],
- })
- : null,
+ },
+ ],
+ }),
db.Goal.findByPk(goalId),
]);
- if (!goal) {
- throw new Error('Goal not found');
+ if (!goal || !user) {
+ throw new Error('Goal or user not found');
}
const oldStatus = goal.status;
+
await db.GoalStatusChange.create({
goalId,
- userId: !user ? null : user.id,
- userName: !user ? 'system' : user.name,
- userRoles: !user ? null : user.roles.map((role) => role.name),
+ userId,
+ userName: user.name,
+ userRoles: user.roles.map((role) => role.name),
oldStatus,
newStatus,
reason,
diff --git a/src/tools/createMonitoringGoals.js b/src/tools/createMonitoringGoals.js
index 1cfd3e1f81..f36b12dfb4 100644
--- a/src/tools/createMonitoringGoals.js
+++ b/src/tools/createMonitoringGoals.js
@@ -4,7 +4,7 @@ import {
Goal,
} from '../models';
import { auditLogger } from '../logger';
-import changeGoalStatus from '../goalServices/changeGoalStatus';
+import { changeGoalStatusWithSystemUser } from '../goalServices/changeGoalStatus';
const createMonitoringGoals = async () => {
const cutOffDate = '2024-01-01'; // TODO: Set this before we deploy to prod.
@@ -152,9 +152,8 @@ const createMonitoringGoals = async () => {
const goalsToOpenIds = goalsToOpen[0].map((goal) => goal.goalId);
// This function also updates the status of the goal via the hook.
// No need to explicitly update the goal status.
- await Promise.all(goalsToOpen[0].map((goal) => changeGoalStatus({
+ await Promise.all(goalsToOpen[0].map((goal) => changeGoalStatusWithSystemUser({
goalId: goal.goalId,
- userId: -1, // -1 is used to define the system is initiating the change
newStatus: 'Not Started',
reason: 'Active monitoring citations',
context: null,
@@ -244,9 +243,8 @@ const createMonitoringGoals = async () => {
const goalsToCloseIds = goalsToClose[0].map((goal) => goal.goalId);
// This function also updates the status of the goal via the hook.
// No need to explicitly update the goal status.
- await Promise.all(goalsToClose[0].map((goal) => changeGoalStatus({
+ await Promise.all(goalsToClose[0].map((goal) => changeGoalStatusWithSystemUser({
goalId: goal.goalId,
- userId: -1, // -1 is used to define the system is initiating the change
newStatus: 'Closed',
reason: 'No active monitoring citations',
context: null,
From 901bd4f642230ff6827b531af17a654433996262 Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Wed, 15 Jan 2025 09:12:23 -0500
Subject: [PATCH 181/198] Remove abort controller
---
frontend/src/fetchers/monitoring.js | 6 ++----
.../pages/RecipientRecord/pages/Monitoring/index.js | 12 +++---------
2 files changed, 5 insertions(+), 13 deletions(-)
diff --git a/frontend/src/fetchers/monitoring.js b/frontend/src/fetchers/monitoring.js
index ffdecb2a5d..1b04477175 100644
--- a/frontend/src/fetchers/monitoring.js
+++ b/frontend/src/fetchers/monitoring.js
@@ -4,7 +4,7 @@ import { get } from '.';
const monitoringUrl = join('/', 'api', 'monitoring');
const classUrl = join('/', 'api', 'monitoring', 'class');
-export const getTtaByCitation = async (recipientId, regionId, signal) => {
+export const getTtaByCitation = async (recipientId, regionId) => {
const data = await get(
join(
monitoringUrl,
@@ -14,13 +14,12 @@ export const getTtaByCitation = async (recipientId, regionId, signal) => {
'tta',
'citation',
),
- signal,
);
return data.json();
};
-export const getTtaByReview = async (recipientId, regionId, signal) => {
+export const getTtaByReview = async (recipientId, regionId) => {
const data = await get(
join(
monitoringUrl,
@@ -30,7 +29,6 @@ export const getTtaByReview = async (recipientId, regionId, signal) => {
'tta',
'review',
),
- signal,
);
return data.json();
diff --git a/frontend/src/pages/RecipientRecord/pages/Monitoring/index.js b/frontend/src/pages/RecipientRecord/pages/Monitoring/index.js
index 8bcc8cc77f..bb3d7cf4c6 100644
--- a/frontend/src/pages/RecipientRecord/pages/Monitoring/index.js
+++ b/frontend/src/pages/RecipientRecord/pages/Monitoring/index.js
@@ -9,7 +9,7 @@ import Tabs from '../../../../components/Tabs';
import { getTtaByCitation, getTtaByReview } from '../../../../fetchers/monitoring';
import ReviewCards from './components/ReviewCards';
import CitationCards from './components/CitationCards';
-// import { ROUTES } from '../../../../Constants';
+import { ROUTES } from '../../../../Constants';
import AppLoadingContext from '../../../../AppLoadingContext';
const MONITORING_PAGES = {
@@ -58,17 +58,15 @@ export default function Monitoring({
}, [currentPage, history, recipientId, regionId]);
useDeepCompareEffect(() => {
- const controller = new AbortController();
async function fetchMonitoringData(slug) {
setIsAppLoading(true);
try {
- const data = await lookup[slug].fetcher(recipientId, regionId, controller.signal);
+ const data = await lookup[slug].fetcher(recipientId, regionId);
lookup[slug].setter(data);
} catch (error) {
// eslint-disable-next-line no-console
console.error('Error fetching monitoring data:', error);
- // todo: handle error (but not abort error)
- // history.push(`${ROUTES.SOMETHING_WENT_WRONG}/${error.status}`);
+ history.push(`${ROUTES.SOMETHING_WENT_WRONG}/${error.status}`);
} finally {
setIsAppLoading(false);
}
@@ -77,10 +75,6 @@ export default function Monitoring({
if (currentPage && ALLOWED_PAGE_SLUGS.includes(currentPage)) {
fetchMonitoringData(currentPage);
}
-
- return () => {
- controller.abort();
- };
}, [currentPage, history, lookup, recipientId, regionId, setIsAppLoading]);
return (
From dd746792f6860d3cd3945f475bd5bfb95ca3289a Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Wed, 15 Jan 2025 11:21:14 -0500
Subject: [PATCH 182/198] Refactor to use custom hook instead of managing with
abort controller
---
.../src/components/CitationDrawerContent.js | 27 ++-----------
frontend/src/fetchers/citations.js | 4 +-
frontend/src/hooks/useFetch.js | 38 +++++++++++++++++++
.../Monitoring/components/CitationCards.js | 3 +-
4 files changed, 46 insertions(+), 26 deletions(-)
create mode 100644 frontend/src/hooks/useFetch.js
diff --git a/frontend/src/components/CitationDrawerContent.js b/frontend/src/components/CitationDrawerContent.js
index 3b4f8155c9..f7c5e6969e 100644
--- a/frontend/src/components/CitationDrawerContent.js
+++ b/frontend/src/components/CitationDrawerContent.js
@@ -1,31 +1,12 @@
-import React, { useEffect, useState } from 'react';
+import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import { uniqueId } from 'lodash';
import { fetchCitationTextByName } from '../fetchers/citations';
+import useFetch from '../hooks/useFetch';
export default function CitationDrawerContent({ citations }) {
- const [content, setContent] = useState([]); // { text: string, citation: string }[]
-
- useEffect(() => {
- const abortController = new AbortController();
- async function fetchCitations() {
- try {
- const response = await fetchCitationTextByName(citations, abortController.signal);
- setContent(response);
- } catch (err) {
- // eslint-disable-next-line no-console
- console.error(err);
- }
- }
-
- if (citations) {
- fetchCitations();
- }
-
- return () => {
- abortController.abort();
- };
- }, [citations]);
+ const fetcher = useCallback(() => fetchCitationTextByName(citations), [citations]);
+ const content = useFetch([], fetcher, [citations]);
return (
diff --git a/frontend/src/fetchers/citations.js b/frontend/src/fetchers/citations.js
index 1178336759..4d4f78648c 100644
--- a/frontend/src/fetchers/citations.js
+++ b/frontend/src/fetchers/citations.js
@@ -22,7 +22,7 @@ export async function fetchCitationsByGrant(region, grantIds, reportStartDate) {
* @param {String[]} citationIds
* @returns {Promise<{ text: String; citation: String; }[]>}
*/
-export async function fetchCitationTextByName(citationIds, signal) {
+export async function fetchCitationTextByName(citationIds) {
const params = new URLSearchParams();
citationIds.forEach((name) => {
params.append('citationIds', encodeURIComponent(name));
@@ -35,6 +35,6 @@ export async function fetchCitationTextByName(citationIds, signal) {
'text',
`?${params.toString()}`,
);
- const citations = await get(url, signal);
+ const citations = await get(url);
return citations.json();
}
diff --git a/frontend/src/hooks/useFetch.js b/frontend/src/hooks/useFetch.js
new file mode 100644
index 0000000000..698cb89575
--- /dev/null
+++ b/frontend/src/hooks/useFetch.js
@@ -0,0 +1,38 @@
+import { useState } from 'react';
+import useDeepCompareEffect from 'use-deep-compare-effect';
+
+const FETCH_STATUS = {
+ shouldFetch: false,
+ fetching: false,
+ fetched: false,
+};
+
+export default function useFetch(initialValue, fetcher, dependencies = []) {
+ const [data, setData] = useState(initialValue); // { text: string, citation: string }[]
+ const [fetchStatus, setFetchStatus] = useState(FETCH_STATUS);
+
+ useDeepCompareEffect(() => {
+ async function fetchData() {
+ try {
+ setFetchStatus({ ...fetchStatus, fetching: true });
+ const response = await fetcher();
+ setData(response);
+ } catch (err) {
+ // eslint-disable-next-line no-console
+ console.error(err);
+ } finally {
+ setFetchStatus({ ...fetchStatus, fetching: false, fetched: true });
+ }
+ }
+
+ if (fetchStatus.shouldFetch && !fetchStatus.fetching && !fetchStatus.fetched) {
+ fetchData();
+ }
+ // eslint-disable-next-line max-len
+ if (dependencies.every((dep) => Boolean(dep)) && !fetchStatus.fetching && !fetchStatus.fetched) {
+ setFetchStatus({ ...fetchStatus, shouldFetch: true });
+ }
+ }, [fetchStatus, dependencies]);
+
+ return data;
+}
diff --git a/frontend/src/pages/RecipientRecord/pages/Monitoring/components/CitationCards.js b/frontend/src/pages/RecipientRecord/pages/Monitoring/components/CitationCards.js
index 94e980ec69..49cbaa1b5a 100644
--- a/frontend/src/pages/RecipientRecord/pages/Monitoring/components/CitationCards.js
+++ b/frontend/src/pages/RecipientRecord/pages/Monitoring/components/CitationCards.js
@@ -1,11 +1,12 @@
import React from 'react';
import PropTypes from 'prop-types';
+import { uniqueId } from 'lodash';
import CitationCard from './CitationCard';
export default function CitationCards({ data, regionId }) {
return (
data.map((citation) => (
-
+
)));
}
From 88d45c85453a6bf7bc27522c650d8eadc3f9b269 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Wed, 15 Jan 2025 16:47:37 -0500
Subject: [PATCH 183/198] add validation to check and rollback start date if
before selected citations
---
.../src/components/ControlledDatePicker.js | 9 +++
.../Pages/__tests__/activitySummary.js | 70 ++++++++++++++++++-
.../ActivityReport/Pages/activitySummary.js | 37 ++++++++++
3 files changed, 114 insertions(+), 2 deletions(-)
diff --git a/frontend/src/components/ControlledDatePicker.js b/frontend/src/components/ControlledDatePicker.js
index 7615f255f3..e88ea0c05e 100644
--- a/frontend/src/components/ControlledDatePicker.js
+++ b/frontend/src/components/ControlledDatePicker.js
@@ -23,6 +23,7 @@ export default function ControlledDatePicker({
endDate,
customValidationMessages,
required,
+ additionalValidation,
}) {
/**
* we don't want to compute these fields multiple times if we don't have to,
@@ -76,6 +77,12 @@ export default function ControlledDatePicker({
return beforeMessage || `Please enter a date before ${max.display}`;
}
+ // Call any additional validation logic.
+ const customValidationMsg = additionalValidation();
+ if (customValidationMsg) {
+ return customValidationMsg;
+ }
+
return true;
}
@@ -146,6 +153,7 @@ ControlledDatePicker.propTypes = {
afterMessage: PropTypes.string,
invalidMessage: PropTypes.string,
}),
+ additionalValidation: PropTypes.func,
};
ControlledDatePicker.defaultProps = {
@@ -161,4 +169,5 @@ ControlledDatePicker.defaultProps = {
afterMessage: '',
invalidMessage: '',
},
+ additionalValidation: () => {},
};
diff --git a/frontend/src/pages/ActivityReport/Pages/__tests__/activitySummary.js b/frontend/src/pages/ActivityReport/Pages/__tests__/activitySummary.js
index a9ae3da6ff..b20bad2b6a 100644
--- a/frontend/src/pages/ActivityReport/Pages/__tests__/activitySummary.js
+++ b/frontend/src/pages/ActivityReport/Pages/__tests__/activitySummary.js
@@ -8,11 +8,11 @@ import { FormProvider, useForm } from 'react-hook-form';
import NetworkContext from '../../../../NetworkContext';
import activitySummary, { isPageComplete } from '../activitySummary';
-const RenderActivitySummary = ({ passedGroups = null }) => {
+const RenderActivitySummary = ({ passedGroups = null, passedGoals = [] }) => {
const hookForm = useForm({
mode: 'onChange',
defaultValues: {
- goals: [],
+ goals: passedGoals,
objectivesWithoutGoals: [],
participants: [],
activityRecipients: [],
@@ -67,6 +67,72 @@ describe('activity summary', () => {
});
});
+ describe('start date citations validation', () => {
+ it('correctly displays the start date citations validation', async () => {
+ const passedGoalsWithCitations = [
+ {
+ id: 1,
+ name: 'goal 1',
+ standard: 'Monitoring',
+ objectives: [
+ {
+ id: 1,
+ title: 'objective 1',
+ citations: [
+ {
+ id: 1,
+ monitoringReferences: [{
+ reportDeliveryDate: '2024-08-07T04:00:00+00:00',
+ }],
+ },
+ ],
+ },
+ ],
+ },
+ ];
+ const { container } = render(
+ ,
+ );
+ const input = container.querySelector('#startDate');
+ userEvent.type(input, '01/01/2024');
+ // trigger blur.
+ userEvent.tab();
+ expect(await screen.findByText('The date entered is not valid with the selected citations.')).toBeInTheDocument();
+ });
+
+ it('does not show the start date citations validation when the date is valid', async () => {
+ const passedGoalsWithCitations = [
+ {
+ id: 1,
+ name: 'goal 1',
+ standard: 'Monitoring',
+ objectives: [
+ {
+ id: 1,
+ title: 'objective 1',
+ citations: [
+ {
+ id: 1,
+ monitoringReferences: [{
+ reportDeliveryDate: '2024-08-07T04:00:00+00:00',
+ }],
+ },
+ ],
+ },
+ ],
+ },
+ ];
+ const { container } = render(
+ ,
+ );
+ const input = container.querySelector('#startDate');
+ userEvent.type(input, '08/08/2024');
+ // trigger blur.
+ userEvent.tab();
+ expect(screen.queryByText('The date entered is not valid with the selected citations.')).not.toBeInTheDocument();
+ });
+ });
+
describe('activity recipients validation', () => {
it('shows a validation message when clicked and recipient type is not selected', async () => {
render( );
diff --git a/frontend/src/pages/ActivityReport/Pages/activitySummary.js b/frontend/src/pages/ActivityReport/Pages/activitySummary.js
index 21c9b7a480..998514e798 100644
--- a/frontend/src/pages/ActivityReport/Pages/activitySummary.js
+++ b/frontend/src/pages/ActivityReport/Pages/activitySummary.js
@@ -63,6 +63,7 @@ const ActivitySummary = ({
const [showGroupInfo, setShowGroupInfo] = useState(false);
const [groupRecipientIds, setGroupRecipientIds] = useState([]);
const [shouldValidateActivityRecipients, setShouldValidateActivityRecipients] = useState(false);
+ const [previousStartDate, setPreviousStartDate] = useState(null);
const activityRecipientType = watch('activityRecipientType');
const watchFormRecipients = watch('activityRecipients');
@@ -71,6 +72,10 @@ const ActivitySummary = ({
const endDate = watch('endDate');
const pageState = watch('pageState');
const isVirtual = watch('deliveryMethod') === 'virtual';
+
+ const selectedGoals = watch('goals');
+ const goalForEditing = watch('goalForEditing');
+
const { otherEntities: rawOtherEntities, grants: rawGrants } = recipients;
const { connectionActive } = useContext(NetworkContext);
@@ -233,6 +238,37 @@ const ActivitySummary = ({
);
+ const validateCitations = () => {
+ const allGoals = [selectedGoals, goalForEditing].flat().filter((g) => g !== null);
+ // If we have a monitoring goal.
+ const selectedMonitoringGoal = allGoals.find((goal) => goal.standard === 'Monitoring');
+ if (selectedMonitoringGoal) {
+ // Get all the citations in a single array from all the goal objectives.
+ const allCitations = selectedMonitoringGoal.objectives.map(
+ (objective) => objective.citations,
+ ).flat();
+ // If we have selected citations
+ if (allCitations.length) {
+ const start = moment(startDate, 'MM/DD/YYYY');
+ const invalidCitations = allCitations.filter(
+ (citation) => citation.monitoringReferences.some(
+ (monitoringReference) => moment(monitoringReference.reportDeliveryDate, 'YYYY-MM-DD').isAfter(start),
+ ),
+ );
+ // If any of the citations are invalid given the new date.
+ if (invalidCitations.length) {
+ // Rollback the start date.
+ setValue('startDate', previousStartDate);
+ // Display monitoring citation warning and keep start date.
+ return 'The date entered is not valid with the selected citations.';
+ }
+ }
+ }
+ // Save the last good start date.
+ setPreviousStartDate(startDate);
+ return null;
+ };
+
return (
<>
@@ -421,6 +457,7 @@ const ActivitySummary = ({
isStartDate
inputId="startDate"
endDate={endDate}
+ additionalValidation={validateCitations}
/>
From ff307510bb95f8e6853e310d091ca9d905b478d1 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Wed, 15 Jan 2025 17:04:36 -0500
Subject: [PATCH 184/198] fix null bug on filter
---
frontend/src/pages/ActivityReport/Pages/activitySummary.js | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/frontend/src/pages/ActivityReport/Pages/activitySummary.js b/frontend/src/pages/ActivityReport/Pages/activitySummary.js
index 998514e798..17163eabf9 100644
--- a/frontend/src/pages/ActivityReport/Pages/activitySummary.js
+++ b/frontend/src/pages/ActivityReport/Pages/activitySummary.js
@@ -244,9 +244,10 @@ const ActivitySummary = ({
const selectedMonitoringGoal = allGoals.find((goal) => goal.standard === 'Monitoring');
if (selectedMonitoringGoal) {
// Get all the citations in a single array from all the goal objectives.
- const allCitations = selectedMonitoringGoal.objectives.map(
- (objective) => objective.citations,
- ).flat();
+ const allCitations = selectedMonitoringGoal.objectives
+ .map((objective) => objective.citations)
+ .flat()
+ .filter((citation) => citation !== null);
// If we have selected citations
if (allCitations.length) {
const start = moment(startDate, 'MM/DD/YYYY');
From f999b5b665938ebba07fd34a4552f14fa57abc1a Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Thu, 16 Jan 2025 08:59:18 -0500
Subject: [PATCH 185/198] not sure how this ever would have passed
---
tests/e2e/activity-report.spec.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/e2e/activity-report.spec.ts b/tests/e2e/activity-report.spec.ts
index 3c83e2ba99..77bb8deaf1 100644
--- a/tests/e2e/activity-report.spec.ts
+++ b/tests/e2e/activity-report.spec.ts
@@ -667,7 +667,7 @@ test.describe('Activity Report', () => {
await page.getByRole('button', { name: 'Supporting attachments Not started' }).click();
await page.getByRole('button', { name: 'Next steps Not started' }).click();
await page.getByRole('button', { name: 'Review and submit' }).click();
- await page.getByRole('button', { name: 'Activity summary Complete' }).click();
+ await page.getByRole('button', { name: 'Activity summary In Progress' }).click();
// select participants
await page.getByLabel('Other entity participants *- Select -').focus();
From f00130d9f1bea402346b0e5b2f2ceedb659b09ad Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Thu, 16 Jan 2025 09:42:04 -0500
Subject: [PATCH 186/198] rename migrations
---
...4-AddMonitoringEnum.js => 20250116140614-AddMonitoringEnum.js} | 0
... => 20250116140614-add-activity-report-objective-citations.js} | 0
...-import.js => 20250116140614-add-post-processing-to-import.js} | 0
....js => 20250116140614-create-standard-goal-template-column.js} | 0
4 files changed, 0 insertions(+), 0 deletions(-)
rename src/migrations/{20241207194714-AddMonitoringEnum.js => 20250116140614-AddMonitoringEnum.js} (100%)
rename src/migrations/{20241207194714-add-activity-report-objective-citations.js => 20250116140614-add-activity-report-objective-citations.js} (100%)
rename src/migrations/{20241207194714-add-post-processing-to-import.js => 20250116140614-add-post-processing-to-import.js} (100%)
rename src/migrations/{20241207194714-create-standard-goal-template-column.js => 20250116140614-create-standard-goal-template-column.js} (100%)
diff --git a/src/migrations/20241207194714-AddMonitoringEnum.js b/src/migrations/20250116140614-AddMonitoringEnum.js
similarity index 100%
rename from src/migrations/20241207194714-AddMonitoringEnum.js
rename to src/migrations/20250116140614-AddMonitoringEnum.js
diff --git a/src/migrations/20241207194714-add-activity-report-objective-citations.js b/src/migrations/20250116140614-add-activity-report-objective-citations.js
similarity index 100%
rename from src/migrations/20241207194714-add-activity-report-objective-citations.js
rename to src/migrations/20250116140614-add-activity-report-objective-citations.js
diff --git a/src/migrations/20241207194714-add-post-processing-to-import.js b/src/migrations/20250116140614-add-post-processing-to-import.js
similarity index 100%
rename from src/migrations/20241207194714-add-post-processing-to-import.js
rename to src/migrations/20250116140614-add-post-processing-to-import.js
diff --git a/src/migrations/20241207194714-create-standard-goal-template-column.js b/src/migrations/20250116140614-create-standard-goal-template-column.js
similarity index 100%
rename from src/migrations/20241207194714-create-standard-goal-template-column.js
rename to src/migrations/20250116140614-create-standard-goal-template-column.js
From 2f0f41ac632cb02cbee5627913eb445a7231f28c Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Thu, 16 Jan 2025 11:20:07 -0500
Subject: [PATCH 187/198] set initial start date
---
frontend/src/pages/ActivityReport/Pages/activitySummary.js | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/frontend/src/pages/ActivityReport/Pages/activitySummary.js b/frontend/src/pages/ActivityReport/Pages/activitySummary.js
index 17163eabf9..d76804ba83 100644
--- a/frontend/src/pages/ActivityReport/Pages/activitySummary.js
+++ b/frontend/src/pages/ActivityReport/Pages/activitySummary.js
@@ -63,8 +63,6 @@ const ActivitySummary = ({
const [showGroupInfo, setShowGroupInfo] = useState(false);
const [groupRecipientIds, setGroupRecipientIds] = useState([]);
const [shouldValidateActivityRecipients, setShouldValidateActivityRecipients] = useState(false);
- const [previousStartDate, setPreviousStartDate] = useState(null);
-
const activityRecipientType = watch('activityRecipientType');
const watchFormRecipients = watch('activityRecipients');
const watchGroup = watch('recipientGroup');
@@ -72,6 +70,7 @@ const ActivitySummary = ({
const endDate = watch('endDate');
const pageState = watch('pageState');
const isVirtual = watch('deliveryMethod') === 'virtual';
+ const [previousStartDate, setPreviousStartDate] = useState(startDate);
const selectedGoals = watch('goals');
const goalForEditing = watch('goalForEditing');
From f56a309ae0b6df3ae0503c1f2dc5d6172460c7ca Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Thu, 16 Jan 2025 16:58:20 -0500
Subject: [PATCH 188/198] check before running
---
.../20250116140614-add-post-processing-to-import.js | 13 +++++++++++++
...16140614-create-standard-goal-template-column.js | 13 +++++++++++++
2 files changed, 26 insertions(+)
diff --git a/src/migrations/20250116140614-add-post-processing-to-import.js b/src/migrations/20250116140614-add-post-processing-to-import.js
index b875fffe5e..f886a2e93c 100644
--- a/src/migrations/20250116140614-add-post-processing-to-import.js
+++ b/src/migrations/20250116140614-add-post-processing-to-import.js
@@ -9,6 +9,19 @@ module.exports = {
const sessionSig = __filename;
await prepMigration(queryInterface, transaction, sessionSig);
+ // Run a query to determine if the column postProcessingActions exists in the table Imports.
+ const [results] = await queryInterface.sequelize.query(/* sql */`
+ SELECT column_name
+ FROM information_schema.columns
+ WHERE table_name = 'Imports'
+ AND column_name = 'postProcessingActions';
+ `, { transaction });
+
+ // If the column postProcessingActions exists in the table Imports, then return.
+ if (results.length > 0) {
+ return;
+ }
+
// Add column postProcessingActions to table Imports of JSONB type.
await queryInterface.addColumn(
'Imports',
diff --git a/src/migrations/20250116140614-create-standard-goal-template-column.js b/src/migrations/20250116140614-create-standard-goal-template-column.js
index 939d73f253..b67c83c8f4 100644
--- a/src/migrations/20250116140614-create-standard-goal-template-column.js
+++ b/src/migrations/20250116140614-create-standard-goal-template-column.js
@@ -12,6 +12,19 @@ module.exports = {
WHERE "creationMethod" = 'System Generated';
`, { transaction });
+ // Write a query to determine if column standard exists in the table GoalTemplates.
+ const [results] = await queryInterface.sequelize.query(/* sql */`
+ SELECT column_name
+ FROM information_schema.columns
+ WHERE table_name = 'GoalTemplates'
+ AND column_name = 'standard';
+ `, { transaction });
+
+ // If the column standard exists in the table GoalTemplates, then return.
+ if (results.length > 0) {
+ return;
+ }
+
// Add a standard column that we populate with whats in () in the template name when curated.
await queryInterface.sequelize.query(/* sql */`
ALTER TABLE "GoalTemplates"
From 70e1f8c5b67861f8dde916146103f8cd89126502 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Fri, 17 Jan 2025 11:52:53 -0500
Subject: [PATCH 189/198] remove duplicate migrations
---
docs/logical_data_model.encoded | 2 +-
docs/logical_data_model.puml | 1 -
...109205626-add-post-processing-to-import.js | 39 -----------
...110144804-add-post-processing-to-import.js | 39 -----------
...26-create-standard-goal-template-column.js | 57 ---------------
...116140614-add-post-processing-to-import.js | 52 --------------
...14-create-standard-goal-template-column.js | 70 -------------------
7 files changed, 1 insertion(+), 259 deletions(-)
delete mode 100644 src/migrations/20250109205626-add-post-processing-to-import.js
delete mode 100644 src/migrations/20250110144804-add-post-processing-to-import.js
delete mode 100644 src/migrations/20250110162226-create-standard-goal-template-column.js
delete mode 100644 src/migrations/20250116140614-add-post-processing-to-import.js
delete mode 100644 src/migrations/20250116140614-create-standard-goal-template-column.js
diff --git a/docs/logical_data_model.encoded b/docs/logical_data_model.encoded
index 7983ce4906..faa0772381 100644
--- a/docs/logical_data_model.encoded
+++ b/docs/logical_data_model.encoded
@@ -1 +1 @@
-xLt_RzqsalzTVuNW_Q5jyBhOjjS3pjWxhECuQN29OzXE5zkYC6Y9Ve-DHBubAQTkh__xWQI-a1GbaPAUaxJyoNuKDJFwS4WEPyZXFnW7b5TbaQJfPocu9oXFzvJS5h1awNl4Tod0smBcQKR9UvRUGSYJD6Nl4Du32igqG1ZoXprC2UKxOZpA2i1O-bkIfcdwbD0SqbDI49h-vPjl_lFal_wcbL_lKFOEGyRIhvCq_LsASYhUXYn9Qped6KVtY3zSmWpj4SIS_PQaJrAKfySfmXnk8KsFqxzl8OOKS9Z_Jv9kAS3yfLdXykpvsUNP-UdPLNhqGZndHtz5UP8IleFq37F47qRUUDFMGh1-yKaAMidsX8xnEp4gjkzd50g1JtXwd79KGiP1kLVOYv_fhmJ3z3Z5vFtdNuZwrJxth_zxajZu2UOd-pqbzv1EWxZy-oe6wSXRRncaZuD5ciC2JjLEKaouU8V3NtYICWvunTBZ5EAg74Bpb1G5k2KKSFBdGdG0Z-UeW8-1mnqguCXRKemG77o_V-YxNn1GF4Umo2yXsnFCPe0WI7W38Igt7xWbeBY6vRu074eXBYWfzwFZd_kDmq2MqXgJkclz-DCOYfUGaOfcZmeWhNcElDsATHY8577P5CsGes0I_ycVDoDGtHYl_vQa69GATrcUI4vtL1JM9Fsf_Vt_llnoh9c6MtAw1Gb5MuH1CT5oHbij7Jcs0RpMRUEi80nULMmFSNvAiEWAxMIZFxk4W17SJ3mfnFap8ESbmIH94PDV1ek7innnA3xbbf020l7A7IYOh7F9ETY3beXGY6vcaotNFcidkdj0insWdt_zxRTVQ3hJl3VGQT_S_WobGAU80uu83k-vBGczOMG3r2HRqSDdjP9Wn0b5U7i9xo25E8FYMtzc1D-kDUjr492ApXdxdG2CwdFxHgUialS8WLgu3saUj5BFinFMWEbZinnQ2UTFV6l0yWbEWNtaUlEWFWFoGlpXu8AIUvJ3V60gdABgc0_xhqmqzJNPe-p3j3CNh4VULw9za0xWRo_u0D7_zAMM0vd5zR3hMa3V23UA8SzNiqsDzWjFbT3VGGexysBGKc3LNW1rBPn13IdPtrgUMhURi2RcZuBX3Ve9Zdd3GztPwQl_FMqVlsKmJrAGvqYOJw0u9rnur3Ab2yBJWLqljEEbUspq7n8dHiBfrYq0usyLXayV_3mU8RuH2hU8uD665nKUgHS_IOL3JjcvdJXV9vg6rwxFke-P_SzgvhRN0UNgcroNW2AhX2SWlp9SNessT2E7087GIr9q_Jo71Rg7zE4rv3kIzqJBAzeOy3dCK0cApx_PVFM5Irqxxq1_oLCAfP5LQhkz07XRWQsDXNYvW5kGAPKnPN7glCOLL-Kjf6_kBW3bA4DxuIBMboDxi8K28LYB8KgCchOaUqgs59IYpLoFQJFuzX_ykuACIoMmHbhXiZUWOTLjYPZG7Qlnv0FEeOaa1tGB-GNPYmON2kvWBqq4vmCE7FeddjmZoYjGFAkLmQMbxNYlkqo5AeMFz3e5xr7VyDIsmm0PNGn8yGvh2-bYIKMPHAC4UtPDHbPv5i56_djGeFpvA6BVrrNcZdgrby7KsIUY7-p15d3u1h-6T0iDZmMd5JoGFry3kImhZ3Afyg_8rdVrcLdlZIwkpQeVqNgp9vh7bzDcmXcT8fjutA6Y4w9U3ZW8zTVet-xda9diL8jS9uRtxKcw5vCzu-mDVhil271wy0xR8Sxk8UPgNBn01U7dGDaFfn1pI6KxUIW9jAYCDBtDi8KI6vn7A9WYFaf0p9Jjt3suXLfwGc0NgS6syHkez1bHirh4vEpzuQTlBo_Fpgzlhb--FhkwFdbzuUf_v3qs4NURqm7GEDZs3RuzoUmFQn7p7iuzXnmnP5-XNw7vDU_WWwFv6vU4a650Gyn0Sl0G9Y8IeM7cp382HYUk0NiX953ogGNw2uJVFGNPkrQ_3BI30xaXK7suO2t19ys2y5BtwiWdLidYm6veA3S4maFwRTmWwVEo2LReZEtbyraQt9qAvpe3kABm2fMe01JnXnUKL7k_A6wu_bji9bwrQX_85GT_oOegX_dBktF2J_8IOKSZ_pu2rZHGTw6nxdbM-BgugC7zEy1UOhc210CXvvu7om9BJeAdEu2t7kD1o7ooTa1__jnZ-yimV7_bzfYJ8XzuqZg558aXQr4-aEw9lZV6mDqrSNZswEPLpwafVAYbkUWGffMrbo9gYoKpaE2zrm9Knhw1E_7vHe1YXt37XkmRPrZZ3AnrlIwLxZudhbrChu7XmEWIcw5FD6fs6uMRgWshqCb_BXwVH1372SpKnAxFvXCljxDwhvwhQQQRv13XaROU8hYD5ZHBeXrWExtJrvAUcuuJlVWroT4TMgqHPRofZWTCjBJBAuzpGFkXJJ_xQRxsAw_L1Q0Gyn2x8cHx_CTaEjUIV8-07d_oME_rzz0zfwIyAW7bR0SXl-RWeW1ONmEDRw6Zt1ruNxgrJY9_BWDHhn1-j-v91_-ct7PY14-zOPv8ATybt1SWWsKTu5pihdKINSbvUqYth2stP_DRNpgb3KF0K5MUlmXIAVKMmjiNgslYV4kSTf-eztiJw3wwuHFVuLPeXWesFN6PynbWN87YUOLpXBUl2UQx7iaiHwu1kbPZrwDkQ1qU4gIMGhT_L3wxsiCCWG8nVbO2L4nLSX92M4Q9fNqnAP5UsTe4LKVjfqJgLNCAfGViH-mAiQlWbEJEznqJYAR7fDf_4EVmbD_6yDJyWTp3qYtc7ZqSwvgW8J4CqmEjuFmyMVxiPyoUmlQwg6uBb1bx3tU0mloVI0vQvf6ybp9Qz0nmu1i9Kl6bv-EX-PTjfM3GZP1FUfn-zlBPsVdro-SlpvvzTdny99TrbjGtwfqZVvJapW9yHxeWcR6AeZ0ZD3ybbTgSL5TXsGwMeA_OsiTw-37zjpGef2TH3jyMGDvZ4Ji_uM9Hg6_KIe_JlzMUf0moDbp1lv7cDX3VFnzj8mlzDVjT3kyGHhh_IaPl8GQt1HpOSN37zrUGSuItnZjM5nOQb5SpV792RLu3zgvzSGzufwvxmlY5QZA4jsBlbYvwYzHhW0f8pLqijo84ozsxnhI9tTv6DZTCtauzK1W6SXbYxuTxalvCRXrR49-8RNd29xVObgEGLGLlToAoAXF8QGEcZXZc40ESGH_OE4NRBQJjjjksbjvfBPfjvmKSRWbzq22ViSr7UdfqLwt_j76bOBYh2YSIxL4mcMBWgYfBG17qdicD1R5hAjSMxaT_FTBUEL9hXNi4tx_CtGHUQ-VKHBs6IlFMKT3y8zqGfq6rt_g6WpmFEmB6A27b6xe56FrgxmB-TlCVtkOOB8dQY7Eb3Xhqo1Zp7iACqoeRVuFA4Q3Phpfje4k4dWKjvhi6VOxaYilRRBnJzQSrfIRSg4hxtb6hiX1EKtcXAXfgAzuxwYR9aVcx9uqAR3NKclUFmZRWI4frlAQPPImVySUWuuJ1wTLlHYY5KX-VGP2_GXWkzv_L2kp1lVL8wcT_5shoTuY9trwcud9L3bT19T_3Y2O-ubXyPyZKilmuLDXFsNB88IqHDizY6_qH0ofEUjntckO8xwweV7hZhcWsztNjK8xlyU_oCaeEqJ5chki_5KrUHGFNLZfxa7mqbRkg0vUFPvGqeL8lfMt4qnIrVlaJCuOVKJV_SCFYGSV3E10kQfj77JVYCYGkGnmhrXL7ENl5VFWnJYqlwi_nzaCjNjH_OjuiLUyQbV23p_HF7u9rewatAVzZ3DnQQwpOSInTUAn-yyUnTVJlMTlKwM4LBPtWbl4xChruG5orLSLXX7htGKZkl4-fQUrjdPP7IbDNKl6CwFitLIvapa6sAqsO0aC3gW4D6p50u_ePFHmwQAo3rntfUelnmuzr8Kw7LTq5Y8lOjvLWSa4l-VWM0dQsJBzUjArGBYAvi_HNF_3KDlQyT08VMog0yDrJbc1Bjp4FVESXjrvrdm9_uLSSxaEEyU3wdR6cgwjyMiTtUc3qZ-uGxZgwC_6YFBRJgsCtmMEamlFMAJr3llfvFMUhHaRAlHXUd3EVbaxtyNUspilo-CVPuJhZGEF7SbhisZegQfH_WFDYVKpz3eFv7UFI3t_DIOYwna7I1NUdXFQ2GvykquvnpqkUEljmsXsYZ9_M0w9FtPVuGXm-a9jRfskV4tub5Xb-SjJVvgN_dZABgjuYYyhU4elAlW8FdjWREgAdZILTHzOrUQMLc4S2lNcfro_4dWsP6jBFVH4sYBECZ4wmL-BvHtnlpOKwltC1AICOmsAfiMxJCx9EGpExJkWNziwHrFlR1SDUAYwLdNNPxc9uTwV6-qdt1tGUZb83ZqkPPVzsFTXJU8T-SUIMldCLDztJVIogRWlN0Rz377wURwpP3YpiHgPD8-kJ45QK_0i26dgMREHwjGyrb8sTEwIZAP4hYAmoSS3Zi-DZyHja8hVkFQ0F26zr0BQUOoKUozztgRBtdb5gB5g63YcikvWjtMcOoHwsUPZjV3DIsgSaVsYttpTflPo5pcEA5KKZUFXx4FnF_vG0uJcXE5gqRqdhGQKbKj_KQdi9EJ3p7ZObVWwx4fNGgPPkgqBayV6-4Y8aPV77SDc3RufdN-w8TKTkpLbrmOZZRQfNHU6SlMpZeFj-PGSTm5RC8SiesZjxzSgMv1MKPgIMbDJte6N8KK7Mup9cNlOg7rYF-mphioNYj5eRH_gpO-ATDdXEhk0rqzZEpatRVJmghp92eSpF2hBTggESS0DzPTcj11HVssbtQnlenhRMc92iT-b2SFiY3ayAw7SozH3X5v_a4a_I4ZAcxe9eOt4gUszpiBU91BR7UhiFkn5Lc-2xHwg6kGdfh2J0zu0Y8Cr1yoYxjnaG60GwvAGuFFUzRP8KhcpNVFX8ds1HdvL-SmyfHJbSkuzK7Ok9Lvf3suTA07eFYWhs8mKwQn-DbI_rLJ7WH3leZZpV3gVa3c_9xzjaZsezy856EqxUGPSjnrrB2T_cQvEs1k0n3S2EbF3om7SexPYyeeSE365Y-VQARDKoS7tFZDklLO1j5oHEZzEJoykJV_tuKpElIF1kPAqZaGT0VC99nNU4Ie-n8rD0yv6k-ugSpg0TmV3ScG_A25bhaeZuDC3tCdy5OXnbhDapVYDiYfjItCboG-fXJ8pd8atKJGdeErb1p94oqt6YNQPw8vjSEHMIzDyLA11_k06CLLJi9Rd90TlT4a3p9Zm82iLDUy0fG5ZGKbkkcFQi2Q3hdo6ZvRAWqTNclfgbljkbzzuWfinLT_Jf4HF8_N3oh5uLUe3IBI49xVaaVZXvZWeEgL_KlIgnSraiTroDtdRnmych4_kICm-lcJ3dRxGQigmiRW3h5Tq-z8ew82Uc757t5yccgfHgo29znqE7eMM4O_Z-XfBtD-RAWnIvexHUvjqmj7Mqn2jpqqOT1wKHfb6O1ZQFHAZwK5dUd0XH7wAnUq4YNcwy79x_29KRgWZHA4JTZi1wYtF4onCl-_wxYDO8Md4UzCSg3kvXUDcUtGZM25WHfr6SOj_pWYd2Oy4pSlLs1CW6oBg98WAa01gzIJk2v08f0jG3AAvA7B0yU_RA0v8yuCDowyF4XVO0vpSSR7k-Ub5PWjX-sk0tgwD3DoY7ykXFVxkfUkwCMwe4z3VEuDO4Fa6tbaULSa4M0R80YXEO0vIHAySZG0r0CHemXL1Sfew0CtxLTiHf0AU8aGiZz-NQ4Q06eF5Ri2DEDavZII0YMZPQSPxp02c2um0dWcA3CdZ_SSIdnMVukkG8aWka290Be8XA3vNXotGZGYr2KNa6eOceMAWCtimRuUdWYT0CGXDXiM09kVxsJuTCo3MtVpfu9k--ZJYPE0tSsXfXSiBak18q8ZM2bFl-oWZPD6HHnIRnYbhbWvBZ8eg8r11ISQmAmYeEKfD0HOG64NBVRnXHCb4QkZmnukJ2AIgJ1qXneWk4AlQxAPmYG8C03Tnh5s5omnGXiY2Avpy28OhmsF0Ctt8N0T81Yi1Om4pTKHTHCb2Akq2d48h8sCYuxzgAu0Xmd7TxHMWaD8CNUqLyD7oYtbYunFIpmyXiuq3ZWYKk6jp0EEwpYiWqo4HGB53i6x8YAXALgJkt8ankc2QkvOgmkM6B4DaGZTpl5H5I2FMl1ul826IYzUD2nN4TM0Ic0MRuDh68aOLHnCQ1pV0TPX6Z1gC8YWLA_mAjY8l0nNsFLiH6OMFV3FQdnYNvB3rXZ6H8tKkI98bSfwT7NnsbNtxvurzk2v2zs-mBWF26xr3s3aF6_llhdxxyKbRHZW0BkTfV6tbDTA9Ed_aIixEdfCVfLXHjr9_kIPACt9LbSHENyi7SOlTRcXxXexINK3mz5jV1cMMR50moInnNuGJmdxgZAJssU8ig6qUP-ThgKhMkOc-1sZUIeIiIPWlKlqod9rLRPJFqe7ttaFAfDD5pf5Dl7b2sfiU6IYRVEw1SmvesBTjZw9nQ6SyGJhreIJPFe2RErvD2PzvKMeMZSMsZYstpR3LkveW-zg6zsgl9uEuZcvHsJrVZfSRegU-KjLRZgVMBASPTrTMu3_Tqc_bGlmxXtMu4J8zJjeSP0WLYl2qLZLht6AtC52FftQunqz9GxnxNj_hQJpQo_3abPU-THHBqac8BwyegsnZMZ-2mZ6Kl8vMqwAp2bPcwTpQcntLMV-k1axbr_KLGCplk5v56fLc-wPMeuQGn6LQqFJBvtwIf4SwvOz4fh6BLABRubVHI-0UmyBWnGQoHHAsD-ZDUUP4mZfYJ64pqKbPBuLn3XKyyTt8XfKyto-Zsh3-TWMNYXN8Qqdp5mgtjeJm6OifTAyYVzT9pEN5d95OmSvKt2rv7ok_0GTgLMRqUgwr76rSXIuOoHg-xCDrBeetsT6lF5RXFy7Dn2YNlb521t3pQr2pNOTJVIjvZjnsjleeKkj4rpBmpk9RmR7WmatYot1tHKDwaX0gV6oY6t13Ii0ECeIs6kODGi313LjIcPqlDezrJUqNSLFfGedQhAs7DLbvnb78EI_FXxWsdOb9fstnm10ncerz2q7u9qqjdqPLZ5w0GrxwoR8UAV1uQneAjDMVh_XLChRz1HCnjekGDXXlrt_LxegZB8jcDtvEavwVrPpgKnqrOaGEF-jxVLVYRwHSC_kQZ0Vvs5kkWTP7h_SwEjXur1hLLlBeOhRzTBzlqYZhUYzrNjNVVuRKAfjyEG7N0G7MmO2eEIAZwcRzGbDNF9il9ndni8cTMrP_DaQpDNTmiejMlke72ATe7JPIDPHGjfSqwclFbh70Exn26UQUUXVBTxr3hfO4lUa-ISEjxq5-nwRijvj8tst2ZDsMYHzFyoLfPYgqu73AVrZp2wPkqDneFbbZ9IqF6O-lDuvHJjDr22lEd3vlkH-AHXt3MD63dVpGcWbx-i4t_OVlcuuZOPX5q2MfLQSmNEMcvi-mj4QZLvBkEe0EWMhVNvp_QPk6jjLYUDj75W0wfXniThWD8uUG-nNKb5MqrWetrc265M4c8iGxYrpDM4cIe9PoizPnfgpJ1jYFeqmQB-GVOqwMBQ-BC-BNsqg8ZlHbSSAmeoUX8vPPbPA1QAira4zcoc2-iBebB1BPnIfMCf3fR885ph30V3VJNDQKOmC79rqWwjTPinJIYcv6vQrKvtDEbDa_gKYLM9g3LWqZOAFMB9isNoKy6V42UNG3wtEBQU26i-aaNGWyFk-iUI5oNRTh_Ea-amqk-ehkTs8MJFlT5yWowwtfo96jkKJAuTIxowIrA8yvoRV_7B56dW_8RTtn-AR_E-wfzltJ1wf-afsfPwjCYBR47fsThBZEpVNKcdMebFP_E8dTGCn9SzDn2QkImovFMxRE65shJSqSb6j3p8zFAfrSJweUcsJmk0v4AZP4pKZ6FLsFZwzILGJ-BdZiIK1VgUwEIEfN6x45A2IresACGcq7RZ4vsbFL1VHRT3j-QO0DkA2NdDDHG9Vtv9fo3bab98xtlIcVGREoHIlpTf3jIGaKawURnjRQJq_h_yKtao_awTnWFCNTiALukWG_ZPVo6iTqkHdDe5u79ex-3O0ulbzNOyUbRZUhenh9Z7vIeVKhMas3QNVAObYXjYuAfjKUEwzvO7qWH51ELTsJsKC74w-P8uThXjkBkh6PthxvPQtMUWvksd38uQoTr-wrtM82BVwQE1WHpDLIjPg7fcdxYE53bxCFS4ZzPj95gQ3zFr8onnj42GRxPLDQMBwHlBpfq7kPYOnaVkPMzuZknhRW0ZB_Tv-8meGXaqTh82MNWCbuR5dtBDPIwdsj4mUNZ40dbxzfDfRZrTrFDeMtlwUbe6q_2QRi2nJNnVfmubFfOA-0LK1zSZ2iM0-lmK2xcRAD8wvDDABqkMulOr4wXshtFsj9WWlRSkkbcs5cq78DcHtRnYEdAaT1okBSU9Ct1-8KdLEvhClrJ4hDOjAyqhiVlM_gDgw4fcJZXYu_ItpMUX7C7psQER2mnPdx6m35PVz8EwvJvdDZIFLhUtEm37QotRtyLDlgs_GEnyC9jNPQcbSH2JhSjGsu9GMiFjUqkMK3O216lIdh0vVrFn7UaQdPac-MzmbNbCPojkjyKhSl8IwcT1_ZcwaFcSpemG2TgXwMqNrOdFnWAMPUDuRYwtb5IRNSz6tDZFkxPQ-Ppjx7a47o0tKZDwOEGeqAu3YhE7QqAiNgeznrwqnWz4CVQDRRivLV3PwP4JUSot7sMQcv_OVx7fEQo63Wlj7GGCTtPAvafM_q4mhmEPk-odZ_OrFxru2QsnL4DsmkHpwM7XoFWI-5ld9XcV7Eke5e4iFNs-tTZvKUiFOBITDcgm04fbbbPLFUWplqsHQHnTiwbehujIVu1SLwLHVB_
+xLt_RzqsalzTVuNW_Q5jyBhOjjS3pjWxhECuQN29OzXE5zkYC6Y9Ve-DHBubAQTkh__xWQI-a1GbaPAUaxJyoNuKDJFwS4WEPyZXFnW7b5TbaQJfPocu9oXFzvJS5h1awNl4Tod0smBcQKR9UvRUGSYJD6Nl4Du32igqG1ZoXprC2UKxOZpA2i1O-bkIfcdwbD0SqbDI49h-vPjl_lFal_wcbL_lKFOEGyRIhvCq_LsASYhUXYn9Qped6KVtY3zSmWpj4SIS_PQaJrAKfySfmXnk8KsFqxzl8OOKS9Z_Jv9kAS3yfLdXykpvsUNP-UdPLNhqGZndHtz5UP8IleFq37F47qRUUDFMGh1-yKaAMidsX8xnEp4gjkzd50g1JtXwd79KGiP1kLVOYv_fhmJ3z3Z5vFtdNuZwrJxth_zxajZu2UOd-pqbzv1EWxZy-oe6wSXRRncaZuD5ciC2JjLEKaouU8V3NtYICWvunTBZ5EAg74Bpb1G5k2KKSFBdGdG0Z-UeW8-1mnqguCXRKemG77o_V-YxNn1GF4Umo2yXsnFCPe0WI7W38Igt7xWbeBY6vRu074eXBYWfzwFZd_kDmq2MqXgJkclz-DCOYfUGaOfcZmeWhNcElDsATHY8577P5CsGes0I_ycVDoDGtHYl_vQa69GATrcUI4vtL1JM9Fsf_Vt_llnoh9c6MtAw1Gb5MuH1CT5oHbij7Jcs0RpMRUEi80nULMmFSNvAiEWAxMIZFxk4W17SJ3mfnFap8ESbmIH94PDV1ek7innnA3xbbf020l7A7IYOh7F9ETY3beXGY6vcaotNFcidkdj0insWdt_zxRTVQ3hJl3VGQT_S_WobGAU80uu83k-vBGczOMG3r2HRqSDdjP9Wn0b5U7i9xo25E8FYMtzc1D-kDUjr492ApXdxdG2CwdFxHgUialS8WLgu3saUj5BFinFMWEbZinnQ2UTFV6l0yWbEWNtaUlEWFWFoGlpXu8AIUvJ3V60gdABgc0_xhqmqzJNPe-p3j3CNh4VULw9za0xWRo_u0D7_zAMM0vd5zR3hMa3V23UA8SzNiqsDzWjFbT3VGGexysBGKc3LNW1rBPn13IdPtrgUMhURi2RcZuBX3Ve9Zdd3GztPwQl_FMqVlsKmJrAGvqYOJw0u9rnur3Ab2yBJWLqljEEbUspq7n8dHiBfrYq0usyLXayV_3mU8RuH2hU8uD665nKUgHS_IOL3JjcvdJXV9vg6rwxFke-P_SzgvhRN0UNgcroNW2AhX2SWlp9SNessT2E7087GIr9q_Jo71Rg7zE4rv3kIzqJBAzeOy3dCK0cApx_PVFM5Irqxxq1_oLCAfP5LQhkz07XRWQsDXNYvW5kGAPKnPN7glCOLL-Kjf6_kBW3bA4DxuIBMboDxi8K28LYB8KgCchOaUqgs59IYpLoFQJFuzX_ykuACIoMmHbhXiZUWOTLjYPZG7Qlnv0FEeOaa1tGB-GNPYmON2kvWBqq4vmCE7FeddjmZoYjGFAkLmQMbxNYlkqo5AeMFz3e5xr7VyDIsmm0PNGn8yGvh2-bYIKMPHAC4UtPDHbPv5i56_djGeFpvA6BVrrNcZdgrby7KsIUY7-p15d3u1h-6T0iDZmMd5JoGFry3kImhZ3Afyg_8rdVrcLdlZIwkpQeVqNgp9vh7bzDcmXcT8fjutA6Y4w9U3ZW8zTVet-xda9diL8jS9uRtxKcw5vCzu-mDVhil271wy0xR8Sxk8UPgNBn01U7dGDaFfn1pI6KxUIW9jAYCDBtDi8KI6vn7A9WYFaf0p9Jjt3suXLfwGc0NgS6syHkez1bHirh4vEpzuQTlBo_Fpgzlhb--FhkwFdbzuUf_v3qs4NURqm7GEDZs3RuzoUmFQn7p7iuzXnmnP5-XNw7vDU_WWwFv6vU4a650Gyn0Sl0G9Y8IeM7cp382HYUk0NiX953ogGNw2uJVFGNPkrQ_3BI30xaXK7suO2t19ys2y5BtwiWdLidYm6veA3S4maFwRTmWwVEo2LReZEtbyraQt9qAvpe3kABm2fMe01JnXnUKL7k_A6wu_bji9bwrQX_85GT_oOegX_dBktF2J_8IOKSZ_pu2rZHGTw6nxdbM-BgugC7zEy1UOhc210CXvvu7om9BJeAdEu2t7kD1o7ooTa1__jnZ-yimV7_bzfYJ8XzuqZg558aXQr4-aEw9lZV6mDqrSNZswEPLpwafVAYbkUWGffMrbo9gYoKpaE2zrm9Knhw1E_7vHe1YXt37XkmRPrZZ3AnrlIwLxZudhbrChu7XmEWIcw5FD6fs6uMRgWshqCb_BXwVH1372SpKnAxFvXCljxDwhvwhQQQRv13XaROU8hYD5ZHBeXrWExtJrvAUcuuJlVWroT4TMgqHPRofZWTCjBJBAuzpGFkXJJ_xQRxsAw_L1Q0Gyn2x8cHx_CTaEjUIV8-07d_oME_rzz0zfwIyAW7bR0SXl-RWeW1ONmEDRw6Zt1ruNxgrJY9_BWDHhn1-j-v91_-ct7PY14-zOPv8ATybt1SWWsKTu5pihdKINSbvUqYth2stP_DRNpgb3KF0K5MUlmXIAVKMmjiNgslYV4kSTf-eztiJw3wwuHFVuLPeXWesFN6PynbWN87YUOLpXBUl2UQx7iaiHwu1kbPZrwDkQ1qU4gIMGhT_L3wxsiCCWG8nVbO2L4nLSX92M4Q9fNqnAP5UsTe4LKVjfqJgLNCAfGViH-mAiQlWbEJEznqJYAR7fDf_4EVmbD_6yDJyWTp3qYtc7ZqSwvgW8J4CqmEjuFmyMVxiPyoUmlQwg6uBb1bx3tU0mloVI0vQvf6ybp9Qz0nmu1i9Kl6bv-EX-PTjfM3GZP1FUfn-zlBPsVdro-SlpvvzTdny99TrbjGtwfqZVvJapW9yHxeWcR6AeZ0ZD3ybbTgSL5TXsGwMeA_OsiTw-37zjpGef2TH3jyMGDvZ4Ji_uM9Hg6_KIe_JlzMUf0moDbp1lv7cDX3VFnzj8mlzDVjT3kyGHhh_IaPl8GQt1HpOSN37zrUGSuItnZjM5nOQb5SpV792RLu3zgvzSGzu7uhN01MGcjjKRaMCbRktXMapjxrFR6uSl9rRe9ZvvJh3tl3s9FsPt3hs63uH--w4qssniKOXgmhURZLaLIQqMHiHTSGmXnZW3Fh0nYdQRY5jzzMsjVHUQT5iUoVWS4leWmRvZ7qyqfMZ-sBzvuKh1SDTLJYHQCzwoHm2LrLP0egWzrHjh7XTSxMsCZ_wwv7spf5Q3DmZ-FvbxYRmMZk89kerL9gtZe8c6EbdE0sg_TOttkDvcX4mHWaftz0jmEXNUvlljvl_yBQtO4dKJfihTPAWHyQOs11cd5RP-1jKZW3DXjHf2rmYz2nejTurw7CabrJUTk6TgZ-jAJNXHbNQzuvQbOLmcieBLT5GM_7UKJSBZapGFMbKvAgXrhn_5BR3HbAg0pBDD68vWJy67ISCJYzlCq8fbFhu38By5S9mkOEiLc0FxgtBKJ_Skr2JlqDC_FQo1PUhShWAAlaUHZJn4SVYEqEcbkN7eS9-ofP33cc9i7dOskYF651nbEE-qpH7_7P5vTCRTqMxkAzhX_9iZt-Nbr5oY8unS-7-gMZoAXgujjBPW-IZgTnL7PbyFAEaYOLwAMqZdgQezSkVc37yYBhvXniM3peUne5mKTiywhWHbo5n6U9OiQuuojahviEFSMfvKN-EjnzgyQB-4_DcgNhNg8GVVAP_-X2i6qszIVaVO-3MMcF5ZcFfmcFrcp-EhQD_pzgcJW-hQEa4jupVcUZ50-AghIfE6zJR2qAIutrAJMllsxGyKfgwaefdHD-WgP8TSmwoNMd251WQK0res8W16jTFsU7GGMCTlEv8rrkC7twi2t4whEekG5m4jwqCaWjwoSUt4B3JOVhrecs5SX79dQM_-eccjh7dvGlusCxwXs-FiWHRkazvu3iFkRUg-nJu2x_YS1zoJWVNxumrNLtbqpgVqmEZVpQ7SDVPd8aNvRcSNXsx2HuZrvstJkafyDNFwpbRDJ9IxiNmufpvj7ovZxzrTLuQnp_E5jOP9nO_bkDWrzPPKAFy0nm8wsFgTzhBxvoMVhfhJiRJDKwABhWS9RGN7Fhuc7PEUrvsrDcdp-uGPVoq7X1zwhx45-9qWTozEbtxdF0hiSZmawF-DY_zzvHPLFKMMLJsbbXKzXLuy4ZUq1ayRIpfEhAkoIkjn3movCtBhNuXzcp8t8z-xekmGPnbP7I2lHBFF-ABQIxK0PqBI1d161PBZNMRdf5M6fhfSqA_i7TGfDx0BXZsScsfzAlBTXV3kpirta-wFw3pz9CQU5pABFktwy6UmZlqYoDvz9shkEcUxdDFTLkuJl0TudJpV6FDTc1XDp9jdbYVZA-Yv5yGqD33OYFtftseeMpiO24TpNbSGMILYHCSdnqVYzyW4ybpxm5zG7Yh0x3r72lnM8A-IvMUzunIOz4oT4XXtSLiwSwpJFQmJRHi9vIHqZub-KEx_Rx9s-KiT9v3h2WQmSFVW-1_lAS42Cy9nT6YVKjQ3ohlaFgcLjjBo86PsB0fyNFObQY4JhLqMnKYKO_tdGyXEOa_XjiSV7Cu-t97h3jJQywg2qOSRrEzAWhdx6KR1zttAple0BPY3bb6qTxPgrUs9A-WD2ErhAE-9ob3pWAo7fSnyx9N-S1wt6TOdor1UDl5aVxic7Zd1PwJ37ZDFBMpayzs7u_AgunGA1EmWcotwYWdt41VMNOh0SNNTbxTsWQwiQqr9YJhtQeGtBw8mvC2-XqzFKJunITxn9EqX8ofonxQM9nApzkSt6qYWUsvtUx3RaJL9lZkaOjXRiOvAyllFQ08I7FGFAlkRGO41e4EEIaEptrlcoo4Azir7pxI9rZKXw7Vt4DAKGxNxYDLnsBYbUPKzg4oVDw3eeBzI87EsaTZvKjzLGmuiWww8y-tmpctmrlo-pQRereFVA3HZfDtq6NBSTTImhVvsgoi0NXC0x0Z9Rmyy2L9EoRluA43GvXOlhqYMxKCNDypu_Ph5M3R1OdJu_JaylBaNp_-rCmh4hmRMUj8v04Gdt1IyHsXqcFio1JGVAJhGYAdCsX7C3nt9dmoGjQQ928-3V2zJ52164SPQ_QCtmYRugOKbwxSa7eO4-Evo0DrKmBwJbOGSwHCPPnejwNUo2PNJeKa_NS52iGVRW1Z5HLxIMvom7Qt1D2y2G-2Gd7J7d0A41Qqr9OhfZrh0kZwPyXekIneTBMva6QfxtRfnJQ8ANFL7Nrwn0nnFvmygrU57g1qIqX2Upw97qwUeu93wXTrhqhiN1RB7LUZrrdySFBgnBx5pCDh9ipv6ws6B2kB6y1wHRSFlQ8Eo8afHzJzHRAfAgKQiaZVyT1XQ1dX6Bw_eUJzZRdo84NkAAsNUJSCRHqjyShSj577GMc4QHIcWSqZ4Qe-L9Ot9qBKXsZiue-8bnilX-S_GkM6Aa8q2b6t8x3UuXpnyiJBlk0keZM2Lfm7_R6AWtiONhPdDu8r0bR4QHIdsFUyO8hmMF2CtAqT0R81igwQY00fW4QloeuWkK2A0FK0YgiI1opFdhtoW2GF-B3S-Z1nOpt0-Gq7svwlNbIM8BQVDlZDwkZG3SgXFFhJtswgdZlZbYh3_8qpk3M1Jz2rv56bN915W2o0uWJc06NaJl48K0DGZ0PC8HINgIFWZD-b7N4Qm2bYvCB8VNjs1AW1QFoQwuYJZTDO4eY8berMdESyG8hWEC29eDYW35vm7F5fyHc-3pc2v08f0kG2AABIGsNujjo8K8jGL9v1Q28g5Yg3L-z6U3fu8ZI3q4HOB5X2hh_zqo4JSitjdmwUoRjl8qucJWEtjaQOd30vhWIDI8qWvRw_Ca8s3LbKiKcyebRveEIuY28YDOHK7Ak2i8g3r2HG4Q51XDpt6qQKJ9I6Ba-CEBbmYiha0LASw0AXYlrk2gS8q2100_VQHPZSC4M8B8ZY-Oz0Y28yDZp3jzn5W7I08Z0MC5Dtr4MK39JYBj0fX28oDZBkkxOYE08SvzrU4He9pU35NX4V3P-eDnOkCVtiC3BRED2uO8cB1ZSmZlki8Z8DCX4K2nJxXgo8YaHbwiwj29CRfebhkI9iRjWYH3P48_TxHOGKGlthmI8o0fbe_RXGSLm75e5fW1c-JInYP64KiN5WCto7MSIemIY2Oe7Ilq0huY9mST-Z5R5HsDWtW_sfSKa-IqyO8nbIzv8aIIBNwUdHLqVfbr_--9VR0cJlDli28BpXEvIzmv2n__xwfo-_b5Mq8y12hlRNHbuJdQZJPxx4xEmfwN6w5KKRzQTxaYGZTsLPtCIbV32tsFsMfaVuAEqb54-FnNLmPjac1OFC4iTLE07yf-veIeyjtgBAnb5clhQwLAshs5kWzeraw8g4sGBrBzFfITLMsGmzQDyzf7mgJVGSwTIR1vIjwR6Xaidt3YZNiAPDo_OOEgTMnZE4KozQaiqJg8dpzQJGcJULLk5eNDleOjiyMusREQAFVMYlzceokFl89gNTqzLugJ5wwZjbhTLuQlrYYd5NTJNkmptTPZxKhqDujni1q-EKxM66G84ORqj5ezOzHYipnKXwjslCj3JKkyVrhRxsauqiVqv9sNjdaGIzfDZ2khBADeRr8pWiOzdBI2KjEgjmPQRkdOtfSPtLtthWP6vTVn7Kp8xxHQHHQPPlUYLgE6bCXXLjZqp-DsbgX7FkM7GAgrWrYYr-9RqK_W4iF6vC46iaKMkZ_enNNYIC8-Oa1XEzbDLIEDTGuHFFdLn8wTFDChezwqzdO9buePo6D5-nS6jxA8-1c7BN2l9dlVHSJjmPYHNCNENDWjUHyZjma7PbLg_7QklHnbN8ag6CqUikJFUIQACztTfp1UuJ_5oSGiax9LGWDuysDSjr6FMtalVORKUhhoA5RdGDC-yChYKy6nxC99uiTqSqb3SfeSBd1eeXDuGqx01ZQ8jXxY0KBCoG5RNfsLBpQFVKNX4tbJwKgDtgIbYpbLSSfPp3qdouUyEfc5JQDXyS0OFPw9SGj5_2zD9PTENO1IZ4jQyisw4YduU6iM1hpTcwVmLJwsyG4NERA7c3uKRzT_sUAEgoI7RZzoJf-MdzcGxbSHDMvC2ZllTt5Vvc-WM3l_beWB-TnJfeNUHwlxEZhKUDGMqLhov6ws_NopQzeextOlTLxTrtUAs2wRS3K1qmK1qic8e34gf-vYyKPVMpoNAoCH-Ro9bLzMTp96jp5pUBQFMhRg1mYlP14sLZ6KLBQREEvdmvAvp3EuHXtgbdeNptEzHwQU2BtfCatFeUz5Ti-cwBkVJDDjpeJPdeqJH_ibQMOYiEXyodDOzm-YRjZSQ39LRoqf3ns7hpEEKKRRUGmlpfmwRxaNZa8Pmr3TZv7us9e9S_RDD_sBxvE2AsMGHTWbfL6lD5JbhkB7jB16er-QuZw02erktr-O-sMNXhBPQdJNGne4FguKQ7wm1IEBbFSLr9HLkDO6CzPaWXLX9Yh8FuzOoL11bg2IThFMVQAasmxGZwjC5Y_W7sTEdYcZWpVgszjEX8RmON72jACZfIkMKP6QYMIZFPHBOivWkhI-8IWMsSKYKZgSuMIE0SAyn70_rrJIa6S73oDP9EhJLRCOtePcJkMbKEztIfpPEwL8cL2MZr818sIlsYIVDbyfC17v1d5u0-jxWsNWYhVjA5qCF3xhe74jTbc_Q_pXFfy9Alw6udTk6aptsHF8FkkfuSoPfRb0nkdOjyUelIYBDSs_ynIzJfeFp67PzVog_plchVBrsm-YVfgLgM-hH8I-p1wTcQoyniNvr9frf9pwUpo1tKZ0GNFRUG6ZdiygHr-wpXXThqt5691lJyoBJowTM4-g5fDe_BGAH2O-ICLCpZrTZuEdLbqC-Yvuu4r8NwtgYaJgKnUv1IGWiQzYZ4Pj0sevCTPNrGdqMtmxScs80RIiav3JLKIN--2UUWPLBIIE-xqXaq6tka4dytwOwKK159kdbyhUtajFv_V1FvShxE7SP3p1rRYfTBuCDu6RzXRBUBqHpQHM3owEyWcCFBPJLsVFfMepgwiUpO1oLgdvBr99Xsbpoc9KhRec1gxP7ZkZVM1r84nKJb7PazL33nkZbIk7QuhRWxgzdTgs_MMftduAOjfmpEsadTVkiT5k3YdwcZ0S5S3LMhMMYw5aolfQMiksj5iJ42QwhwEUP0HRQFYCjMaKKRgTpsyqJDGsiSM13acwrgRVb2-lPIHkTXBc6MyP4Rv2k-4vkL2y3ustqkZYYAy1YjJQTG1YwJk67HjuT3gVivN1HSFcwMKFwkFLgAMt-FrXpQzawXZhRn99osA_nS4-z7dqEfZxK2NW7rmIM-OZ6mkW_bOivXYhG-XONIwtB0YAto5Aep0Qmj3IPepzYRRjwUHSinQ7Pa5u-OhimBBOTxwq7I7BTVUExblYRJFtinYqNxIZDnVT_IR-QkfcJYt30-Id_NU1BE7JoRcRUnxvZv6GE7R_r9EsnUvdCEJFUJUNEpZucmtcc0LTZeXm4EnC8BjtLPchON2papjmov9GIjdTcs4sO1OIFml2hf0KVwFn7VaQhPvssMzmfNbSTmuUhUMBGk8o-bTXuBdAiFcS-Pmm2TgDsWqdvPdVnWW6LTDeOxx7f7IRNTzMpCZVcuPw-Ppzt6aaFm0NOZDQUFLuuAuJh-E7UqAiJgeTxDw4rZz9WTQrVkifTV3P_44ZMTYtlt2Qsv_ONv7xwQosBW5DBIGSHqagzafcprSmtpEfYzTtl-O5DxxOERs1P7DMmly3wN7XwEgo-5lt6Dcb7JkO9gSSKsXk3VZPLtilOpIjDbgy8Hf5XcPL7TBpltsnIHneKwbuhwjYJv1yHvLHR9_mC0
\ No newline at end of file
diff --git a/docs/logical_data_model.puml b/docs/logical_data_model.puml
index 209f27a14a..fd01f07390 100644
--- a/docs/logical_data_model.puml
+++ b/docs/logical_data_model.puml
@@ -551,7 +551,6 @@ class Imports{
* updatedAt : timestamp with time zone
fileMask : text
path : text
- postProcessingActions : jsonb
}
class MailerLogs{
diff --git a/src/migrations/20250109205626-add-post-processing-to-import.js b/src/migrations/20250109205626-add-post-processing-to-import.js
deleted file mode 100644
index b875fffe5e..0000000000
--- a/src/migrations/20250109205626-add-post-processing-to-import.js
+++ /dev/null
@@ -1,39 +0,0 @@
-const {
- prepMigration,
-} = require('../lib/migration');
-
-/** @type {import('sequelize-cli').Migration} */
-module.exports = {
- async up(queryInterface, Sequelize) {
- await queryInterface.sequelize.transaction(async (transaction) => {
- const sessionSig = __filename;
- await prepMigration(queryInterface, transaction, sessionSig);
-
- // Add column postProcessingActions to table Imports of JSONB type.
- await queryInterface.addColumn(
- 'Imports',
- 'postProcessingActions',
- {
- type: Sequelize.JSONB,
- allowNull: true,
- },
- { transaction },
- );
-
- // Update Imports set the postProcessingActions column to the object.
- await queryInterface.sequelize.query(/* sql */`
- UPDATE "Imports"
- SET "postProcessingActions" = '[{"name": "Monitoring Goal CRON job", "function": "createMonitoringGoals"}]'
- WHERE "name" = 'ITAMS Monitoring Data';
- `, { transaction });
- });
- },
-
- down: async (queryInterface) => {
- await queryInterface.sequelize.transaction(async (transaction) => {
- const sessionSig = __filename;
- await prepMigration(queryInterface, transaction, sessionSig);
- await queryInterface.removeColumn('Imports', 'postProcessingActions', { transaction });
- });
- },
-};
diff --git a/src/migrations/20250110144804-add-post-processing-to-import.js b/src/migrations/20250110144804-add-post-processing-to-import.js
deleted file mode 100644
index b875fffe5e..0000000000
--- a/src/migrations/20250110144804-add-post-processing-to-import.js
+++ /dev/null
@@ -1,39 +0,0 @@
-const {
- prepMigration,
-} = require('../lib/migration');
-
-/** @type {import('sequelize-cli').Migration} */
-module.exports = {
- async up(queryInterface, Sequelize) {
- await queryInterface.sequelize.transaction(async (transaction) => {
- const sessionSig = __filename;
- await prepMigration(queryInterface, transaction, sessionSig);
-
- // Add column postProcessingActions to table Imports of JSONB type.
- await queryInterface.addColumn(
- 'Imports',
- 'postProcessingActions',
- {
- type: Sequelize.JSONB,
- allowNull: true,
- },
- { transaction },
- );
-
- // Update Imports set the postProcessingActions column to the object.
- await queryInterface.sequelize.query(/* sql */`
- UPDATE "Imports"
- SET "postProcessingActions" = '[{"name": "Monitoring Goal CRON job", "function": "createMonitoringGoals"}]'
- WHERE "name" = 'ITAMS Monitoring Data';
- `, { transaction });
- });
- },
-
- down: async (queryInterface) => {
- await queryInterface.sequelize.transaction(async (transaction) => {
- const sessionSig = __filename;
- await prepMigration(queryInterface, transaction, sessionSig);
- await queryInterface.removeColumn('Imports', 'postProcessingActions', { transaction });
- });
- },
-};
diff --git a/src/migrations/20250110162226-create-standard-goal-template-column.js b/src/migrations/20250110162226-create-standard-goal-template-column.js
deleted file mode 100644
index 939d73f253..0000000000
--- a/src/migrations/20250110162226-create-standard-goal-template-column.js
+++ /dev/null
@@ -1,57 +0,0 @@
-const { prepMigration } = require('../lib/migration');
-
-module.exports = {
- up: async (queryInterface) => queryInterface.sequelize.transaction(
- async (transaction) => {
- await prepMigration(queryInterface, transaction, __filename);
-
- // Set Monitoring goal back to Curated for use in the system.
- await queryInterface.sequelize.query(/* sql */`
- UPDATE "GoalTemplates"
- SET "creationMethod" = 'Curated'
- WHERE "creationMethod" = 'System Generated';
- `, { transaction });
-
- // Add a standard column that we populate with whats in () in the template name when curated.
- await queryInterface.sequelize.query(/* sql */`
- ALTER TABLE "GoalTemplates"
- ADD COLUMN standard TEXT GENERATED ALWAYS AS (
- CASE
- WHEN "creationMethod" = 'Curated' THEN substring("templateName" from '(?:^[(]([^)]+)[)])')
- ELSE NULL
- END) STORED;
- `, { transaction });
-
- // Add a unique index on the standard column.
- await queryInterface.sequelize.query(/* sql */`
- CREATE UNIQUE INDEX unique_standard_non_null
- ON "GoalTemplates"(standard)
- WHERE standard IS NOT NULL;
- `, { transaction });
- },
- ),
-
- down: async (queryInterface) => queryInterface.sequelize.transaction(
- async (transaction) => {
- await prepMigration(queryInterface, transaction, __filename);
-
- // Set creationMethod back to 'System Generated' for the monitoring goal template.
- await queryInterface.sequelize.query(/* sql */`
- UPDATE "GoalTemplates"
- SET "creationMethod" = 'System Generated'
- WHERE "standard" = 'Monitoring';
- `, { transaction });
-
- // Remove the unique index on the standard column.
- await queryInterface.sequelize.query(/* sql */`
- DROP INDEX unique_standard_non_null;
- `, { transaction });
-
- // Drop the standard column.
- await queryInterface.sequelize.query(/* sql */`
- ALTER TABLE "GoalTemplates"
- DROP COLUMN standard;
- `, { transaction });
- },
- ),
-};
diff --git a/src/migrations/20250116140614-add-post-processing-to-import.js b/src/migrations/20250116140614-add-post-processing-to-import.js
deleted file mode 100644
index f886a2e93c..0000000000
--- a/src/migrations/20250116140614-add-post-processing-to-import.js
+++ /dev/null
@@ -1,52 +0,0 @@
-const {
- prepMigration,
-} = require('../lib/migration');
-
-/** @type {import('sequelize-cli').Migration} */
-module.exports = {
- async up(queryInterface, Sequelize) {
- await queryInterface.sequelize.transaction(async (transaction) => {
- const sessionSig = __filename;
- await prepMigration(queryInterface, transaction, sessionSig);
-
- // Run a query to determine if the column postProcessingActions exists in the table Imports.
- const [results] = await queryInterface.sequelize.query(/* sql */`
- SELECT column_name
- FROM information_schema.columns
- WHERE table_name = 'Imports'
- AND column_name = 'postProcessingActions';
- `, { transaction });
-
- // If the column postProcessingActions exists in the table Imports, then return.
- if (results.length > 0) {
- return;
- }
-
- // Add column postProcessingActions to table Imports of JSONB type.
- await queryInterface.addColumn(
- 'Imports',
- 'postProcessingActions',
- {
- type: Sequelize.JSONB,
- allowNull: true,
- },
- { transaction },
- );
-
- // Update Imports set the postProcessingActions column to the object.
- await queryInterface.sequelize.query(/* sql */`
- UPDATE "Imports"
- SET "postProcessingActions" = '[{"name": "Monitoring Goal CRON job", "function": "createMonitoringGoals"}]'
- WHERE "name" = 'ITAMS Monitoring Data';
- `, { transaction });
- });
- },
-
- down: async (queryInterface) => {
- await queryInterface.sequelize.transaction(async (transaction) => {
- const sessionSig = __filename;
- await prepMigration(queryInterface, transaction, sessionSig);
- await queryInterface.removeColumn('Imports', 'postProcessingActions', { transaction });
- });
- },
-};
diff --git a/src/migrations/20250116140614-create-standard-goal-template-column.js b/src/migrations/20250116140614-create-standard-goal-template-column.js
deleted file mode 100644
index b67c83c8f4..0000000000
--- a/src/migrations/20250116140614-create-standard-goal-template-column.js
+++ /dev/null
@@ -1,70 +0,0 @@
-const { prepMigration } = require('../lib/migration');
-
-module.exports = {
- up: async (queryInterface) => queryInterface.sequelize.transaction(
- async (transaction) => {
- await prepMigration(queryInterface, transaction, __filename);
-
- // Set Monitoring goal back to Curated for use in the system.
- await queryInterface.sequelize.query(/* sql */`
- UPDATE "GoalTemplates"
- SET "creationMethod" = 'Curated'
- WHERE "creationMethod" = 'System Generated';
- `, { transaction });
-
- // Write a query to determine if column standard exists in the table GoalTemplates.
- const [results] = await queryInterface.sequelize.query(/* sql */`
- SELECT column_name
- FROM information_schema.columns
- WHERE table_name = 'GoalTemplates'
- AND column_name = 'standard';
- `, { transaction });
-
- // If the column standard exists in the table GoalTemplates, then return.
- if (results.length > 0) {
- return;
- }
-
- // Add a standard column that we populate with whats in () in the template name when curated.
- await queryInterface.sequelize.query(/* sql */`
- ALTER TABLE "GoalTemplates"
- ADD COLUMN standard TEXT GENERATED ALWAYS AS (
- CASE
- WHEN "creationMethod" = 'Curated' THEN substring("templateName" from '(?:^[(]([^)]+)[)])')
- ELSE NULL
- END) STORED;
- `, { transaction });
-
- // Add a unique index on the standard column.
- await queryInterface.sequelize.query(/* sql */`
- CREATE UNIQUE INDEX unique_standard_non_null
- ON "GoalTemplates"(standard)
- WHERE standard IS NOT NULL;
- `, { transaction });
- },
- ),
-
- down: async (queryInterface) => queryInterface.sequelize.transaction(
- async (transaction) => {
- await prepMigration(queryInterface, transaction, __filename);
-
- // Set creationMethod back to 'System Generated' for the monitoring goal template.
- await queryInterface.sequelize.query(/* sql */`
- UPDATE "GoalTemplates"
- SET "creationMethod" = 'System Generated'
- WHERE "standard" = 'Monitoring';
- `, { transaction });
-
- // Remove the unique index on the standard column.
- await queryInterface.sequelize.query(/* sql */`
- DROP INDEX unique_standard_non_null;
- `, { transaction });
-
- // Drop the standard column.
- await queryInterface.sequelize.query(/* sql */`
- ALTER TABLE "GoalTemplates"
- DROP COLUMN standard;
- `, { transaction });
- },
- ),
-};
From 471a683041b9da55ef5ef2cf04bb1e8c6ecc2a36 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Fri, 17 Jan 2025 16:07:02 -0500
Subject: [PATCH 190/198] see if tests pass
---
src/routes/recipient/handlers.js | 3 ---
src/services/recipient.js | 10 ----------
src/services/recipient.test.js | 2 +-
3 files changed, 1 insertion(+), 14 deletions(-)
diff --git a/src/routes/recipient/handlers.js b/src/routes/recipient/handlers.js
index 71e08e1dde..12aed26c7b 100644
--- a/src/routes/recipient/handlers.js
+++ b/src/routes/recipient/handlers.js
@@ -123,15 +123,12 @@ export async function getGoalsByRecipient(req, res) {
const { recipientId, regionId } = req.params;
- const policy = new Users(await userById(await currentUserId(req, res)));
-
// Get goals for recipient.
const recipientGoals = await getGoalsByActivityRecipient(
recipientId,
regionId,
{
...req.query,
- excludeMonitoringGoals: !(policy.canSeeBehindFeatureFlag('monitoring_integration')),
},
);
res.json(recipientGoals);
diff --git a/src/services/recipient.js b/src/services/recipient.js
index 60a6fe7a3d..1c6c2b0bac 100644
--- a/src/services/recipient.js
+++ b/src/services/recipient.js
@@ -587,7 +587,6 @@ export async function getGoalsByActivityRecipient(
offset = 0,
limit = GOALS_PER_PAGE,
goalIds = [],
- excludeMonitoringGoals = true,
...filters
},
) {
@@ -635,15 +634,6 @@ export async function getGoalsByActivityRecipient(
};
}
- if (excludeMonitoringGoals) {
- goalWhere = {
- ...goalWhere,
- createdVia: {
- [Op.not]: 'monitoring',
- },
- };
- }
-
// goal IDS can be a string or an array of strings
// or undefined
// we also want at least one value here
diff --git a/src/services/recipient.test.js b/src/services/recipient.test.js
index c0acbe11e2..e4190b4613 100644
--- a/src/services/recipient.test.js
+++ b/src/services/recipient.test.js
@@ -1235,7 +1235,7 @@ describe('Recipient DB service', () => {
const goalsForRecord = await getGoalsByActivityRecipient(
recipient.id,
grant.regionId,
- { excludeMonitoringGoals: false },
+ {},
);
// Assert counts.
From c38d33b8e39d9183f6a52c2f7f49f79972488703 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Tue, 21 Jan 2025 08:52:09 -0500
Subject: [PATCH 191/198] make cutoff date today
---
src/services/citations.ts | 4 +---
src/tools/createMonitoringGoals.js | 5 ++---
2 files changed, 3 insertions(+), 6 deletions(-)
diff --git a/src/services/citations.ts b/src/services/citations.ts
index 0fc5c0d9a8..3b7d239f6f 100644
--- a/src/services/citations.ts
+++ b/src/services/citations.ts
@@ -16,9 +16,7 @@ export async function textByCitation(
});
}
-// TODO: Update this to the day we deploy to PROD.
-// const cutOffStartDate = new Date().toISOString().split('T')[0];
-const cutOffStartDate = '2024-01-01';
+const cutOffStartDate = '2024-01-21';
/*
The purpose of this function is to get citations by grant id.
We then need to format the response for how it needs to be
diff --git a/src/tools/createMonitoringGoals.js b/src/tools/createMonitoringGoals.js
index f36b12dfb4..17cb2c3975 100644
--- a/src/tools/createMonitoringGoals.js
+++ b/src/tools/createMonitoringGoals.js
@@ -7,8 +7,7 @@ import { auditLogger } from '../logger';
import { changeGoalStatusWithSystemUser } from '../goalServices/changeGoalStatus';
const createMonitoringGoals = async () => {
- const cutOffDate = '2024-01-01'; // TODO: Set this before we deploy to prod.
-
+ const cutOffDate = '2024-01-21';
// Verify that the monitoring goal template exists.
const monitoringGoalTemplate = await GoalTemplate.findOne({
where: {
@@ -204,7 +203,7 @@ const createMonitoringGoals = async () => {
ON gr.number = mrg."grantNumber"
JOIN "MonitoringReviews" mr
ON mrg."reviewId" = mr."reviewId"
- AND mr."reportDeliveryDate" BETWEEN '2024-01-01' AND NOW()
+ AND mr."reportDeliveryDate" BETWEEN '${cutOffDate}' AND NOW()
JOIN "MonitoringReviewStatuses" mrs
ON mr."statusId" = mrs."statusId"
AND mrs.name = 'Complete'
From 4b8d250b25668c90ea350a54f16540662c30e206 Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Tue, 21 Jan 2025 09:44:45 -0500
Subject: [PATCH 192/198] Confirm fix with test
---
src/seeders/20240228223541-monitoring-data.js | 4 ++--
src/services/monitoring.ts | 3 +--
2 files changed, 3 insertions(+), 4 deletions(-)
diff --git a/src/seeders/20240228223541-monitoring-data.js b/src/seeders/20240228223541-monitoring-data.js
index 9ef7689cc2..24f581118d 100644
--- a/src/seeders/20240228223541-monitoring-data.js
+++ b/src/seeders/20240228223541-monitoring-data.js
@@ -9,7 +9,7 @@ const reviews = [
startDate: new Date('2022/12/19'),
endDate: new Date('2022/12/21'),
reviewType: 'FA-1',
- reportDeliveryDate: new Date('2023/02/22'),
+ reportDeliveryDate: new Date('2024/02/22'),
outcome: 'Compliant',
hash: 'seedhashrev1',
sourceCreatedAt: new Date('2022/12/19'),
@@ -27,7 +27,7 @@ const reviews = [
startDate: new Date('2022/12/08'),
endDate: new Date('2022/12/08'),
reviewType: 'RAN',
- reportDeliveryDate: new Date('2024/01/13'),
+ reportDeliveryDate: new Date('2024/01/22'),
outcome: 'Deficient',
hash: 'seedhashrev2',
sourceCreatedAt: new Date('2022/12/08'),
diff --git a/src/services/monitoring.ts b/src/services/monitoring.ts
index 6083c8f036..2c60e9b3c5 100644
--- a/src/services/monitoring.ts
+++ b/src/services/monitoring.ts
@@ -49,9 +49,8 @@ const {
Role,
} = db;
-const MIN_DELIVERY_DATE = '2023-01-01';
+const MIN_DELIVERY_DATE = '2024-01-21';
const REVIEW_STATUS_COMPLETE = 'Complete';
-const EIGHT_HOURS = 60 * 60 * 8;
async function grantNumbersByRecipientAndRegion(recipientId: number, regionId: number) {
const grants = await Grant.findAll({
From 87a312ab2c44e3939b68b79d6e2bd2a229a830f4 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Tue, 21 Jan 2025 10:19:32 -0500
Subject: [PATCH 193/198] Its now the year 2025 lol
---
src/services/citations.ts | 2 +-
src/tools/createMonitoringGoals.js | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/services/citations.ts b/src/services/citations.ts
index 3b7d239f6f..8e6491ee00 100644
--- a/src/services/citations.ts
+++ b/src/services/citations.ts
@@ -16,7 +16,7 @@ export async function textByCitation(
});
}
-const cutOffStartDate = '2024-01-21';
+const cutOffStartDate = '2025-01-21';
/*
The purpose of this function is to get citations by grant id.
We then need to format the response for how it needs to be
diff --git a/src/tools/createMonitoringGoals.js b/src/tools/createMonitoringGoals.js
index 17cb2c3975..4d300085ca 100644
--- a/src/tools/createMonitoringGoals.js
+++ b/src/tools/createMonitoringGoals.js
@@ -7,7 +7,7 @@ import { auditLogger } from '../logger';
import { changeGoalStatusWithSystemUser } from '../goalServices/changeGoalStatus';
const createMonitoringGoals = async () => {
- const cutOffDate = '2024-01-21';
+ const cutOffDate = '2025-01-21';
// Verify that the monitoring goal template exists.
const monitoringGoalTemplate = await GoalTemplate.findOne({
where: {
From fddf3fae74380b31d6b8b533aa37f3c34a79a751 Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Tue, 21 Jan 2025 10:43:54 -0500
Subject: [PATCH 194/198] Update some tests
---
src/seeders/20240228223541-monitoring-data.js | 4 ++--
src/services/monitoring.testHelpers.ts | 4 ++--
src/services/monitoring.ts | 2 +-
src/services/recipient.test.js | 8 ++++----
src/services/ttaByCitation.test.js | 2 +-
src/services/ttaByReview.test.js | 2 +-
6 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/src/seeders/20240228223541-monitoring-data.js b/src/seeders/20240228223541-monitoring-data.js
index 24f581118d..b826a4e620 100644
--- a/src/seeders/20240228223541-monitoring-data.js
+++ b/src/seeders/20240228223541-monitoring-data.js
@@ -9,7 +9,7 @@ const reviews = [
startDate: new Date('2022/12/19'),
endDate: new Date('2022/12/21'),
reviewType: 'FA-1',
- reportDeliveryDate: new Date('2024/02/22'),
+ reportDeliveryDate: new Date('2025/02/22'),
outcome: 'Compliant',
hash: 'seedhashrev1',
sourceCreatedAt: new Date('2022/12/19'),
@@ -27,7 +27,7 @@ const reviews = [
startDate: new Date('2022/12/08'),
endDate: new Date('2022/12/08'),
reviewType: 'RAN',
- reportDeliveryDate: new Date('2024/01/22'),
+ reportDeliveryDate: new Date('2025/01/22'),
outcome: 'Deficient',
hash: 'seedhashrev2',
sourceCreatedAt: new Date('2022/12/08'),
diff --git a/src/services/monitoring.testHelpers.ts b/src/services/monitoring.testHelpers.ts
index dfb6e44aa6..b233540369 100644
--- a/src/services/monitoring.testHelpers.ts
+++ b/src/services/monitoring.testHelpers.ts
@@ -164,7 +164,7 @@ async function createMonitoringData(
emotionalSupport: 6.2303,
classroomOrganization: 5.2303,
instructionalSupport: 3.2303,
- reportDeliveryDate: '2024-05-22 21:00:00-07',
+ reportDeliveryDate: '2025-05-22 21:00:00-07',
hash: 'seedhashclasssum1',
sourceCreatedAt: '2024-05-22 21:00:00-07',
sourceUpdatedAt: '2024-05-22 21:00:00-07',
@@ -194,7 +194,7 @@ async function createMonitoringData(
startDate: '2024-02-12',
endDate: '2024-02-12',
reviewType: 'FA-1',
- reportDeliveryDate: '2024-02-21 21:00:00-08',
+ reportDeliveryDate: '2025-02-21 21:00:00-08',
outcome: 'Complete',
hash: 'seedhashrev2',
sourceCreatedAt: '2024-02-22 21:00:00-08',
diff --git a/src/services/monitoring.ts b/src/services/monitoring.ts
index 2c60e9b3c5..97ea16c248 100644
--- a/src/services/monitoring.ts
+++ b/src/services/monitoring.ts
@@ -49,7 +49,7 @@ const {
Role,
} = db;
-const MIN_DELIVERY_DATE = '2024-01-21';
+const MIN_DELIVERY_DATE = '2025-01-21';
const REVIEW_STATUS_COMPLETE = 'Complete';
async function grantNumbersByRecipientAndRegion(recipientId: number, regionId: number) {
diff --git a/src/services/recipient.test.js b/src/services/recipient.test.js
index e4190b4613..eda7b078ab 100644
--- a/src/services/recipient.test.js
+++ b/src/services/recipient.test.js
@@ -2332,7 +2332,9 @@ describe('Recipient DB service', () => {
});
it('returns all goals if limitNum is falsy', async () => {
- const { goalRows, count } = await getGoalsByActivityRecipient(
+ const findAll = jest.spyOn(Goal, 'findAll');
+
+ await getGoalsByActivityRecipient(
recipient.id,
grant.regionId,
{
@@ -2340,9 +2342,7 @@ describe('Recipient DB service', () => {
},
);
- expect(count).toBe(goals.length);
- expect(goalRows.length).toBe(goals.length);
- expect(goalRows[0].id).toBe(goals[1].id);
+ expect(findAll).not.toHaveBeenCalledWith(expect.objectContaining({ limit: 0 }));
});
it('sorts by goalStatus correctly with mixed statuses', async () => {
diff --git a/src/services/ttaByCitation.test.js b/src/services/ttaByCitation.test.js
index bbf6f104fc..035bf46b56 100644
--- a/src/services/ttaByCitation.test.js
+++ b/src/services/ttaByCitation.test.js
@@ -181,7 +181,7 @@ describe('ttaByCitations', () => {
},
],
outcome: 'Complete',
- reviewReceived: '02/22/2024',
+ reviewReceived: '02/22/2025',
reviewType: 'FA-1',
specialists: [
{
diff --git a/src/services/ttaByReview.test.js b/src/services/ttaByReview.test.js
index ef23771d48..81fad3eae7 100644
--- a/src/services/ttaByReview.test.js
+++ b/src/services/ttaByReview.test.js
@@ -190,7 +190,7 @@ describe('ttaByReviews', () => {
lastTTADate: moment().format('MM/DD/YYYY'),
name: 'REVIEW!!!',
outcome: 'Complete',
- reviewReceived: '02/22/2024',
+ reviewReceived: '02/22/2025',
reviewType: 'FA-1',
specialists: [
{
From dd6675b44d9fd71155def5354c29d4528efc3881 Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Tue, 21 Jan 2025 11:02:32 -0500
Subject: [PATCH 195/198] Old tests now broken
---
src/services/monitoring.test.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/services/monitoring.test.js b/src/services/monitoring.test.js
index 9945969480..bcc437ad1a 100644
--- a/src/services/monitoring.test.js
+++ b/src/services/monitoring.test.js
@@ -98,7 +98,7 @@ describe('monitoring services', () => {
emotionalSupport: 6.2303,
classroomOrganization: 5.2303,
instructionalSupport: 3.2303,
- reportDeliveryDate: '2024-05-22 21:00:00-07',
+ reportDeliveryDate: '2025-05-22 21:00:00-07',
});
jest.spyOn(Grant, 'findOne').mockResolvedValueOnce({
@@ -174,7 +174,7 @@ describe('monitoring services', () => {
});
expect(data).not.toBeNull();
- expect(data.reviewDate).toEqual('02/22/2024');
+ expect(data.reviewDate).toEqual('02/22/2025');
await MonitoringReview.destroy({ where: { reviewId: 'C48EAA67-90B9-4125-9DB5-0011D6D7C809' }, force: true });
await MonitoringReview.destroy({ where: { reviewId: 'D58FBB78-91CA-4236-8DB6-0022E7E8D909' }, force: true });
From efd2f40d73826c560d9f0f9678c3a588f125ffa1 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Tue, 21 Jan 2025 11:18:58 -0500
Subject: [PATCH 196/198] put back envs to what they were before testing party
---
.circleci/config.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 8cfbd5d91d..40a6ad120b 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -780,14 +780,14 @@ parameters:
type: string
staging_git_branch:
description: "Name of github branch that will deploy to staging"
- default: "al-ttahub-3603-activity-report-objective-citations"
+ default: "main"
type: string
dev_git_branch: # change to feature branch to test deployment
description: "Name of github branch that will deploy to dev"
default: "TTAHUB-3542/TTAHUB-3544/s3Queue-scanQueue-coverage"
type: string
sandbox_git_branch: # change to feature branch to test deployment
- default: "main"
+ default: "al-ttahub-3603-activity-report-objective-citations"
type: string
prod_new_relic_app_id:
default: "877570491"
From 0b938016ac329a24d965803a2193cb530857f986 Mon Sep 17 00:00:00 2001
From: Matt Bevilacqua
Date: Tue, 21 Jan 2025 11:19:53 -0500
Subject: [PATCH 197/198] Fix final unit test, hopefully
---
src/services/monitoring.test.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/services/monitoring.test.js b/src/services/monitoring.test.js
index bcc437ad1a..fc5e6e54ed 100644
--- a/src/services/monitoring.test.js
+++ b/src/services/monitoring.test.js
@@ -161,7 +161,7 @@ describe('monitoring services', () => {
regionId: REGION_ID,
grant: GRANT_NUMBER,
reviewStatus: 'Complete',
- reviewDate: '02/22/2024',
+ reviewDate: '02/22/2025',
reviewType: 'FA-1',
});
});
From 55c45a7f5fc7a4eaa1031953102860ad53ae1350 Mon Sep 17 00:00:00 2001
From: Adam Levin
Date: Tue, 21 Jan 2025 11:36:45 -0500
Subject: [PATCH 198/198] add Nathans comment
---
src/models/activityReportObjectiveCitation.js | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/src/models/activityReportObjectiveCitation.js b/src/models/activityReportObjectiveCitation.js
index ada2a32a11..1bdbdef2b7 100644
--- a/src/models/activityReportObjectiveCitation.js
+++ b/src/models/activityReportObjectiveCitation.js
@@ -37,6 +37,15 @@ export default (sequelize, DataTypes) => {
type: DataTypes.TEXT,
allowNull: false,
},
+ /*
+ We want to track as a single entity a link to N findings containing the same citation.
+ This would require a lot of database structure to represent traditionally, but because the
+ subset of imported monitoring data we reference is expected to remain completely static,
+ referential drift is not expected to ever become a problem.
+ So, the JSONB allows us to encapsulate the complications with minimal structure
+ In addition, the data in this field is tracked as citation per grant.
+ This also allows us to quickly display the data without have to join monitoring tables.
+ */
monitoringReferences: {
type: DataTypes.JSONB,
allowNull: false,