forked from HHS/Head-Start-TTADP
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' into legacy-comment-file-upload
- Loading branch information
Showing
9 changed files
with
349 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
import reconcileLegacyReports, | ||
{ | ||
reconcileAuthors, | ||
reconcileCollaborators, | ||
reconcileApprovingManagers, | ||
} from './legacyreports'; | ||
import db, { User, ActivityReport, ActivityReportCollaborator } from '../models'; | ||
import { REPORT_STATUSES } from '../constants'; | ||
|
||
const report1 = { | ||
activityRecipientType: 'grantee', | ||
status: REPORT_STATUSES.DRAFT, | ||
regionId: 1, | ||
ECLKCResourcesUsed: ['test'], | ||
legacyId: 'legacy-1', | ||
imported: { | ||
manager: 'Manager4099@Test.Gov', | ||
createdBy: 'user4096@Test.gov', | ||
otherSpecialists: 'user4097@TEST.gov, user4098@test.gov', | ||
}, | ||
}; | ||
|
||
const report2 = { | ||
activityRecipientType: 'grantee', | ||
status: REPORT_STATUSES.DRAFT, | ||
regionId: 1, | ||
ECLKCResourcesUsed: ['test'], | ||
legacyId: 'legacy-2', | ||
imported: { | ||
manager: 'Manager4099@test.gov', | ||
createdBy: 'user4097@Test.gov', | ||
otherSpecialists: 'user4096@test.gov', | ||
}, | ||
}; | ||
|
||
const user1 = { | ||
id: 4096, | ||
homeRegionId: 1, | ||
name: 'user', | ||
hsesUsername: 'user', | ||
hsesUserId: '4096', | ||
email: 'user4096@test.gov', | ||
}; | ||
|
||
const user2 = { | ||
id: 4097, | ||
homeRegionId: 1, | ||
name: 'user2', | ||
hsesUsername: 'user2', | ||
hsesUserId: '4097', | ||
email: 'user4097@test.gov', | ||
}; | ||
|
||
const user3 = { | ||
id: 4098, | ||
homeRegionId: 1, | ||
name: 'user3', | ||
hsesUsername: 'user3', | ||
hsesUserId: '4098', | ||
email: 'user4098@test.gov', | ||
}; | ||
|
||
const manager = { | ||
id: 4099, | ||
homeRegionId: 1, | ||
name: 'manager', | ||
hsesUsername: 'manager', | ||
hsesUserId: '4099', | ||
email: 'manager4099@test.gov', | ||
}; | ||
|
||
describe('reconcile legacy reports', () => { | ||
let mockReport1; | ||
let mockReport2; | ||
let mockUser1; | ||
let mockUser2; | ||
let mockUser3; | ||
let mockManager; | ||
|
||
beforeAll(async () => { | ||
mockReport1 = await ActivityReport.create(report1); | ||
mockReport2 = await ActivityReport.create(report2); | ||
mockUser1 = await User.create(user1); | ||
mockUser2 = await User.create(user2); | ||
mockUser3 = await User.create(user3); | ||
mockManager = await User.create(manager); | ||
}); | ||
|
||
afterAll(async () => { | ||
await ActivityReportCollaborator.destroy({ | ||
where: { activityReportId: [mockReport1.id, mockReport2.id] }, | ||
}); | ||
await ActivityReport.destroy({ where: { id: [mockReport1.id, mockReport2.id] } }); | ||
await User.destroy({ | ||
where: { id: [mockUser1.id, mockUser2.id, mockUser3.id, mockManager.id] }, | ||
}); | ||
await db.sequelize.close(); | ||
}); | ||
it('adds an author if there is one', async () => { | ||
await reconcileAuthors(mockReport1); | ||
mockReport1 = await ActivityReport.findOne({ where: { id: mockReport1.id } }); | ||
expect(mockReport1.userId).toBe(mockUser1.id); | ||
}); | ||
it('adds an approvingManager if there is one', async () => { | ||
await reconcileApprovingManagers(mockReport1); | ||
mockReport1 = await ActivityReport.findOne({ where: { id: mockReport1.id } }); | ||
expect(mockReport1.approvingManagerId).toBe(manager.id); | ||
}); | ||
it('adds collaborators', async () => { | ||
await reconcileCollaborators(mockReport1); | ||
const collaborators = await ActivityReportCollaborator.findAll({ | ||
where: { activityReportId: mockReport1.id }, | ||
}); | ||
expect(collaborators.length).toBe(2); | ||
}); | ||
it('tests the reconciliation process', async () => { | ||
await reconcileLegacyReports(); | ||
mockReport2 = await ActivityReport.findOne({ where: { id: mockReport2.id } }); | ||
expect(mockReport2.userId).toBe(user2.id); | ||
expect(mockReport2.approvingManagerId).toBe(manager.id); | ||
const collaborators = await ActivityReportCollaborator.findAll({ | ||
where: { activityReportId: mockReport2.id }, | ||
}); | ||
expect(collaborators.length).toBe(1); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
import { Op } from 'sequelize'; | ||
import { userByEmail } from './users'; | ||
import { ActivityReport, ActivityReportCollaborator } from '../models'; | ||
import { logger } from '../logger'; | ||
|
||
/* | ||
* Returns all legacy reports that either: | ||
* 1. are missing an author | ||
* 2. are missing an approving manager | ||
* 3. have colloborators in the imported field | ||
* These are the only reports that might need reconciliation | ||
*/ | ||
const getLegacyReports = async () => { | ||
const reports = await ActivityReport.findAll({ | ||
where: { | ||
legacyId: { | ||
[Op.ne]: null, | ||
}, | ||
imported: { | ||
[Op.ne]: null, | ||
}, | ||
[Op.or]: [ | ||
{ | ||
userId: { | ||
[Op.eq]: null, | ||
}, | ||
}, | ||
{ | ||
approvingManagerId: { | ||
[Op.eq]: null, | ||
}, | ||
}, | ||
{ | ||
imported: { | ||
otherSpecialists: { | ||
[Op.ne]: '', | ||
}, | ||
}, | ||
}, | ||
], | ||
|
||
}, | ||
}); | ||
return reports; | ||
}; | ||
|
||
/* | ||
* Checks a report to see if the email address listed in the imported.manager field | ||
* belongs to any user. If it does, it updates the report with that user.id in the | ||
* approvingManager column | ||
*/ | ||
export const reconcileApprovingManagers = async (report) => { | ||
try { | ||
const user = await userByEmail(report.imported.manager); | ||
if (user) { | ||
await ActivityReport.update({ approvingManagerId: user.id }, { where: { id: report.id } }); | ||
logger.info(`Updated approvingManager for report ${report.displayId} to user Id ${user.id}`); | ||
} | ||
} catch (err) { | ||
logger.error(err); | ||
} | ||
}; | ||
/* | ||
* Checks a report to see if the email address listed in the imported.createdBy field | ||
* belongs to any user. If it does, it updates the report with that user.id in the | ||
* userId column | ||
*/ | ||
export const reconcileAuthors = async (report) => { | ||
try { | ||
const user = await userByEmail(report.imported.createdBy); | ||
if (user) { | ||
await ActivityReport.update({ userId: user.id }, { where: { id: report.id } }); | ||
logger.info(`Updated author for report ${report.displayId} to user Id ${user.id}`); | ||
} | ||
} catch (err) { | ||
logger.error(err); | ||
} | ||
}; | ||
|
||
/* | ||
* First checks if the number of collaborators is different than the number of | ||
* entries in the imported.otherSpecialists field. If not, then no reconciliation is needed. | ||
* If there is a difference, it tries to find users matching the email addresses in the | ||
* otherSpecialists field. It then uses findorCreate to add collaborators that haven't yet | ||
* been added. | ||
*/ | ||
export const reconcileCollaborators = async (report) => { | ||
try { | ||
const collaborators = await ActivityReportCollaborator | ||
.findAll({ where: { activityReportId: report.id } }); | ||
// In legacy reports, specialists are in a single column seperated by commas. | ||
// First, get a list of other specialists and split on commas eliminating any blanks. | ||
const splitOtherSpecialists = report.imported.otherSpecialists.split(',').filter((j) => j !== ''); | ||
// Next we map the other specialists to lower case and trim whitespace to standardize them. | ||
const otherSpecialists = splitOtherSpecialists.map((i) => i.toLowerCase().trim()); | ||
if (otherSpecialists.length !== collaborators.length) { | ||
const users = []; | ||
otherSpecialists.forEach((specialist) => { | ||
users.push(userByEmail(specialist)); | ||
}); | ||
const userArray = await Promise.all(users); | ||
const pendingCollaborators = []; | ||
userArray.forEach((user) => { | ||
if (user) { | ||
pendingCollaborators.push(ActivityReportCollaborator | ||
.findOrCreate({ where: { activityReportId: report.id, userId: user.id } })); | ||
} | ||
}); | ||
const newCollaborators = await Promise.all(pendingCollaborators); | ||
// findOrCreate returns an array with the second value being a boolean | ||
// which is true if a new object is created. This counts the number of objects where | ||
// c[1] is true | ||
const numberOfNewCollaborators = newCollaborators.filter((c) => c[1]).length; | ||
if (numberOfNewCollaborators > 0) { | ||
logger.info(`Added ${numberOfNewCollaborators} collaborator for report ${report.displayId}`); | ||
} | ||
} | ||
} catch (err) { | ||
logger.error(err); | ||
} | ||
}; | ||
|
||
export default async function reconcileLegacyReports() { | ||
// Get all reports that might need reconciliation | ||
const reports = await getLegacyReports(); | ||
// Array to help promises from reports that are getting reconciled | ||
const updates = []; | ||
try { | ||
reports.forEach((report) => { | ||
// if there is no author, try to reconcile the author | ||
if (!report.userId) { | ||
updates.push(reconcileAuthors(report)); | ||
} | ||
// if there is no approving manager, try to reconcile the approving manager | ||
if (!report.approvingManagerId) { | ||
updates.push(reconcileApprovingManagers(report)); | ||
} | ||
// if the report has collaborators, check if collaborators need reconcilliation. | ||
if (report.imported.otherSpecialists !== '') { | ||
updates.push(reconcileCollaborators(report)); | ||
} | ||
}); | ||
// let all promises resolve | ||
await Promise.all(updates); | ||
} catch (err) { | ||
logger.error(err); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import reconcileLegacyReports from '../services/legacyreports'; | ||
import { auditLogger } from '../logger'; | ||
|
||
reconcileLegacyReports().then(process.exit(0)).catch((e) => { | ||
auditLogger.error(e); | ||
process.exit(1); | ||
}); |