Skip to content

Commit

Permalink
Merge branch 'main' into legacy-comment-file-upload
Browse files Browse the repository at this point in the history
  • Loading branch information
rahearn authored Mar 24, 2021
2 parents aeec8a0 + cd700ad commit a69be99
Show file tree
Hide file tree
Showing 9 changed files with 349 additions and 24 deletions.
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ parameters:
default: "main"
type: string
sandbox_git_branch: # change to feature branch to test deployment
default: "177-display-external-link-warning"
default: "kw-ie-keyboard-nav"
type: string
prod_new_relic_app_id:
default: "877570491"
Expand Down
42 changes: 21 additions & 21 deletions R14ActivityReportsTest.csv

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions frontend/src/components/DatePicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ const DateInput = ({
onDateChange={(d) => {
const newDate = d ? d.format(dateFmt) : d;
onChange(newDate);
const input = document.getElementById(name);
if (input) input.focus();
}}
onFocusChange={({ focused }) => {
if (!focused) {
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@
"import:goals:local": "./node_modules/.bin/babel-node ./src/tools/importTTAPlanGoals.js",
"import:goals": "node ./build/server/tools/importTTAPlanGoals.js",
"import:hses:local": "./node_modules/.bin/babel-node ./src/tools/importGrantGranteesCLI.js --skipdownload",
"import:hses": "node ./build/server/tools/importGrantGranteesCLI.js"
"import:hses": "node ./build/server/tools/importGrantGranteesCLI.js",
"reconcile:legacy": "node ./build/server/tools/reconcileLegacyReports.js",
"reconcile:legacy:local": "./node_modules/.bin/babel-node ./src/tools/reconcileLegacyReports.js"
},
"repository": {
"type": "git",
Expand Down
126 changes: 126 additions & 0 deletions src/services/legacyreport.test.js
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);
});
});
148 changes: 148 additions & 0 deletions src/services/legacyreports.js
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);
}
}
9 changes: 9 additions & 0 deletions src/services/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ export async function userById(userId) {
});
}

export async function userByEmail(email) {
return User.findOne({
attributes: ['id'],
where: {
email: { [Op.iLike]: email },
},
});
}

export async function usersWithPermissions(regions, scopes) {
return User.findAll({
attributes: ['id', 'name'],
Expand Down
33 changes: 32 additions & 1 deletion src/services/users.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import db, {
} from '../models';

import {
usersWithPermissions, userById,
usersWithPermissions, userById, userByEmail,
} from './users';

import SCOPES from '../middleware/scopeConstants';
Expand Down Expand Up @@ -43,6 +43,37 @@ describe('Users DB service', () => {
expect(user.name).toBe('user 1');
});
});
describe('userByEmail', () => {
beforeEach(async () => {
await User.create({
id: 50,
name: 'user 50',
email: 'user50@test.gov',
hsesUsername: 'user.50',
hsesUserId: '50',
});
await User.create({
id: 51,
name: 'user 51',
email: 'user51@test.gov',
hsesUsername: 'user.51',
hsesUserId: '51',
});
});

afterEach(async () => {
await User.destroy({ where: { id: [50, 51] } });
});

it('retrieves the correct user', async () => {
const user = await userByEmail('user50@test.gov');
expect(user.id).toBe(50);
});
it('retrieves the correct user if case differs', async () => {
const user = await userByEmail('User51@Test.Gov');
expect(user.id).toBe(51);
});
});

describe('usersWithPermissions', () => {
const users = [
Expand Down
7 changes: 7 additions & 0 deletions src/tools/reconcileLegacyReports.js
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);
});

0 comments on commit a69be99

Please sign in to comment.