Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[Prod] Add GoalStatusChange table to DB schema, update labels on TR dashboard, Group permissions update for session report POC users #2107

Merged
merged 22 commits into from
Apr 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/logical_data_model.encoded

Large diffs are not rendered by default.

30 changes: 30 additions & 0 deletions docs/logical_data_model.puml
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,20 @@ class GoalSimilarityGroups{
version : integer
}

class GoalStatusChanges{
* id : integer : <generated>
* goalId : integer : REFERENCES "Goals".id
* userId : integer : REFERENCES "Users".id
* createdAt : timestamp with time zone : now()
* newStatus : varchar(255)
* reason : text
* updatedAt : timestamp with time zone : now()
* userName : varchar(255)
* userRoles : varchar(255)[]
context : text
oldStatus : varchar(255)
}

class GoalTemplateFieldPrompts{
* id : integer : <generated>
* goalTemplateId : integer : REFERENCES "GoalTemplates".id
Expand Down Expand Up @@ -1480,6 +1494,20 @@ class ZALGoalSimilarityGroups{
session_sig : text
}

class ZALGoalStatusChanges{
* id : bigint : <generated>
* 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 ZALGoalTemplateFieldPrompts{
* id : bigint : <generated>
* data_id : bigint
Expand Down Expand Up @@ -2374,6 +2402,7 @@ Goals "1" --[#black,dashed,thickness=2]--{ "n" GoalFieldResponses : goal, respo
Goals "1" --[#black,dashed,thickness=2]--{ "n" GoalResources : goal, goalResources
Goals "1" --[#black,dashed,thickness=2]--{ "n" GoalSimilarityGroupGoals : goal, goalSimilarityGroupGoals
Goals "1" --[#black,dashed,thickness=2]--{ "n" GoalSimilarityGroups : finalGoal, goalSimilarityGroups
Goals "1" --[#black,dashed,thickness=2]--{ "n" GoalStatusChanges : goal, statusChanges
Goals "1" --[#black,dashed,thickness=2]--{ "n" Goals : parentGoal, childGoals
Goals "1" --[#black,dashed,thickness=2]--{ "n" Objectives : objectives, goal
Goals "1" --[#black,dashed,thickness=2]--{ "n" SimScoreGoalCaches : scoreOne, scoreTwo, goalOne, goalTwo
Expand Down Expand Up @@ -2453,6 +2482,7 @@ Users "1" --[#black,dashed,thickness=2]--{ "n" ActivityReports : author, lastUp
Users "1" --[#black,dashed,thickness=2]--{ "n" CommunicationLogs : author, communicationLogs
Users "1" --[#black,dashed,thickness=2]--{ "n" EventReportPilotNationalCenterUsers : user, eventReportPilotNationalCenterUsers
Users "1" --[#black,dashed,thickness=2]--{ "n" GoalCollaborators : user, goalCollaborators
Users "1" --[#black,dashed,thickness=2]--{ "n" GoalStatusChanges : user, goalStatusChanges
Users "1" --[#black,dashed,thickness=2]--{ "n" GroupCollaborators : user, groupCollaborators
Users "1" --[#black,dashed,thickness=2]--{ "n" NationalCenterUsers : user, nationalCenterUsers
Users "1" --[#black,dashed,thickness=2]--{ "n" ObjectiveCollaborators : user, objectiveCollaborators
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ export default function TrainingReportDashboard() {
filters={[]}
loading={false}
title="Hours of training by National Center"
subtitle="Hours reported on training report sessions"
subtitle="Hours reported on Training Report sessions"
xAxisLabel="National Center"
yAxisLabel="Hours"
yAxisLabel="Number of hours"
/>
</Grid>
</Grid>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/widgets/TotalHrsAndRecipientGraph.js
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ export function TotalHrsAndRecipientGraph({ data, loading, hideYAxis }) {
<Grid row className="position-relative margin-bottom-2">
<Grid desktop={{ col: 'auto' }} mobileLg={{ col: 8 }}><h2 className="ttahub--dashboard-widget-heading margin-0">Total TTA hours</h2></Grid>
<Grid desktop={{ col: 'auto' }} className="ttahub--show-accessible-data-button desktop:margin-y-0 mobile-lg:margin-y-1">
{ !showAccessibleData && <MediaCaptureButton id="rd-save-screenshot" className="margin-x-2" reference={widget} buttonText="Save screenshot" /> }
{ !showAccessibleData && <MediaCaptureButton id="rd-save-screenshot" title="Total TTA hours" className="margin-x-2" reference={widget} buttonText="Save screenshot" /> }
<button
type="button"
className="usa-button--unstyled"
Expand Down
109 changes: 109 additions & 0 deletions src/goalServices/changeGoalStatus.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import faker from '@faker-js/faker';
import changeGoalStatus from './changeGoalStatus';
import db from '../models';

const fakeName = faker.name.firstName() + faker.name.lastName();
const mockUser = {
id: faker.datatype.number(),
homeRegionId: 1,
name: fakeName,
hsesUsername: fakeName,
hsesUserId: fakeName,
lastLogin: new Date(),
};

describe('changeGoalStatus service', () => {
let user;
let userRole;
let role;
let goal;
let grant;
let recipient;
const newStatus = 'In Progress';
const reason = 'All objectives achieved';
const context = 'Tree planted successfully';

beforeAll(async () => {
user = await db.User.create(mockUser);
recipient = await db.Recipient.create({
id: faker.datatype.number(),
name: faker.name.firstName(),
});
grant = await db.Grant.create({
id: faker.datatype.number(),
number: faker.datatype.string(),
recipientId: recipient.id,
regionId: 1,
startDate: new Date(),
endDate: new Date(),
});
goal = await db.Goal.create({
name: 'Plant a tree',
status: 'Draft',
grantId: grant.id,
});
role = await db.Role.create({
id: faker.datatype.number(),
name: 'Astronaut',
isSpecialist: true,
});
userRole = await db.UserRole.create({
userId: user.id,
roleId: role.id,
});
});

afterAll(async () => {
await db.User.destroy({ where: { id: mockUser.id } });
await db.Goal.destroy({ where: { id: goal.id } });
await db.Grant.destroy({ where: { id: grant.id } });
await db.Recipient.destroy({ where: { id: recipient.id } });
await db.UserRole.destroy({ where: { userId: user.id } });
await db.Role.destroy({ where: { id: role.id } });
});

it('should change the status of a goal and create a status change log', async () => {
await changeGoalStatus({
goalId: goal.id,
userId: mockUser.id,
newStatus,
reason,
context,
});

const updatedGoal = await db.Goal.findByPk(goal.id);
const statusChangeLog = await db.GoalStatusChange.findOne({
where: { goalId: goal.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).toBe(mockUser.id);
expect(statusChangeLog.userName).toBe(user.name);
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,
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');
});
});
56 changes: 56 additions & 0 deletions src/goalServices/changeGoalStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import db from '../models';

interface GoalStatusChangeParams {
goalId: number;
userId: number;
newStatus: string;
reason: string;
context: string;
}

export default async function changeGoalStatus({
goalId,
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: [],
},
},
],
}),
db.Goal.findByPk(goalId),
]);

if (!goal || !user) {
throw new Error('Goal or user not found');
}

const oldStatus = goal.status;

await db.GoalStatusChange.create({
goalId,
userId,
userName: user.name,
userRoles: user.roles.map((role) => role.name),
oldStatus,
newStatus,
reason,
context,
});

await goal.reload();

return goal;
}
4 changes: 1 addition & 3 deletions src/goalServices/closeMultiRecipientGoalsFromAdmin.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ describe('closeMultiRecipientGoalsFromAdmin', () => {
closeSuspendReason: CLOSE_SUSPEND_REASONS[0],
};

const response = await closeMultiRecipientGoalsFromAdmin(data);
const response = await closeMultiRecipientGoalsFromAdmin(data, 1);
expect(response.isError).toBe(false);
expect(response.goals.length).toBe(2);

Expand Down Expand Up @@ -157,8 +157,6 @@ describe('closeMultiRecipientGoalsFromAdmin', () => {

updatedGoals.forEach((updatedGoal) => {
expect(updatedGoal.status).toBe(GOAL_STATUS.CLOSED);
expect(updatedGoal.closeSuspendContext).toBe(data.closeSuspendContext);
expect(updatedGoal.closeSuspendReason).toBe(data.closeSuspendReason);
updatedGoal.objectives.forEach((objective) => {
const expectedStatus = objective.onApprovedAR
? OBJECTIVE_STATUS.COMPLETE : objectiveNotOnApprovedAr.status;
Expand Down
6 changes: 2 additions & 4 deletions src/goalServices/goalUpdate.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,8 @@ describe('Change Goal Status', () => {
const reason = 'TTA complete';
const context = 'This goal has been completed.';
const updatedGoals = await updateGoalStatusById(
goal.id.toString(),
[goal.id],
5,
oldStatus,
newStatus,
reason,
Expand All @@ -216,8 +217,5 @@ describe('Change Goal Status', () => {
expect(updatedGoals.length).toEqual(1);
const updatedGoal = updatedGoals[0];
expect(updatedGoal.status).toEqual(newStatus);
expect(updatedGoal.closeSuspendReason).toEqual(reason);
expect(updatedGoal.closeSuspendContext).toEqual(context);
expect(updatedGoal.previousStatus).toEqual(oldStatus);
});
});
29 changes: 12 additions & 17 deletions src/goalServices/goals.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import {
setSimilarityGroupAsUserMerged,
} from '../services/goalSimilarityGroup';
import Users from '../policies/user';
import changeGoalStatus from './changeGoalStatus';

const namespace = 'SERVICE:GOALS';

Expand Down Expand Up @@ -2319,6 +2320,7 @@ export function verifyAllowedGoalStatusTransition(oldStatus, newStatus, previous
/**
* Updates a goal status by id
* @param {number[]} goalIds
* @param {number} userId
* @param {string} oldStatus
* @param {string} newStatus
* @param {string} closeSuspendReason
Expand All @@ -2328,6 +2330,7 @@ export function verifyAllowedGoalStatusTransition(oldStatus, newStatus, previous
*/
export async function updateGoalStatusById(
goalIds,
userId,
oldStatus,
newStatus,
closeSuspendReason,
Expand All @@ -2346,22 +2349,13 @@ export async function updateGoalStatusById(
return false;
}

// finally, if everything is golden, we update the goal
const g = await Goal.update({
status: newStatus,
closeSuspendReason,
closeSuspendContext,
previousStatus: oldStatus,
}, {
where: {
id: goalIds,
},
returning: true,
individualHooks: true,
});

const [, updated] = g;
return updated;
return Promise.all(goalIds.map((goalId) => changeGoalStatus({
goalId,
userId,
newStatus,
reason: closeSuspendReason,
context: closeSuspendContext,
})));
}

export async function getGoalsForReport(reportId) {
Expand Down Expand Up @@ -3489,7 +3483,7 @@ Exampled request body, the data param:
}
}
*/
export async function closeMultiRecipientGoalsFromAdmin(data) {
export async function closeMultiRecipientGoalsFromAdmin(data, userId) {
const {
selectedGoal,
closeSuspendContext,
Expand Down Expand Up @@ -3523,6 +3517,7 @@ export async function closeMultiRecipientGoalsFromAdmin(data) {
isError: false,
goals: await updateGoalStatusById(
goalIds,
userId,
status,
GOAL_STATUS.CLOSED,
closeSuspendReason,
Expand Down
Loading