Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 13 additions & 0 deletions backend/typescript/graphql/resolvers/reviewDashboardResolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import ReviewDashboardService from "../../services/implementations/reviewDashboa
import {
ReviewDashboardRowDTO,
ReviewDashboardSidePanelDTO,
ReviewedApplicantRecordDTO,
} from "../../types";
import { getErrorMessage } from "../../utilities/errorUtils";

Expand Down Expand Up @@ -35,6 +36,18 @@ const reviewDashboardResolvers = {
}
},
},
Mutation: {
delegateReviewers: async (
_parent: undefined,
args: { positions: string[] },
): Promise<ReviewedApplicantRecordDTO[]> => {
try {
return await reviewDashboardService.delegateReviewers(args.positions);
} catch (error) {
throw new Error(getErrorMessage(error));
}
},
},
};

export default reviewDashboardResolvers;
26 changes: 19 additions & 7 deletions backend/typescript/graphql/types/reviewDashboardType.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { gql } from "apollo-server-express";
import {
ApplicationStatus,

Check warning on line 3 in backend/typescript/graphql/types/reviewDashboardType.ts

View workflow job for this annotation

GitHub Actions / run-lint

'ApplicationStatus' is defined but never used
PositionTitle,

Check warning on line 4 in backend/typescript/graphql/types/reviewDashboardType.ts

View workflow job for this annotation

GitHub Actions / run-lint

'PositionTitle' is defined but never used
Review,

Check warning on line 5 in backend/typescript/graphql/types/reviewDashboardType.ts

View workflow job for this annotation

GitHub Actions / run-lint

'Review' is defined but never used
ReviewerDTO,

Check warning on line 6 in backend/typescript/graphql/types/reviewDashboardType.ts

View workflow job for this annotation

GitHub Actions / run-lint

'ReviewerDTO' is defined but never used
ReviewStatus,

Check warning on line 7 in backend/typescript/graphql/types/reviewDashboardType.ts

View workflow job for this annotation

GitHub Actions / run-lint

'ReviewStatus' is defined but never used
SkillCategory,

Check warning on line 8 in backend/typescript/graphql/types/reviewDashboardType.ts

View workflow job for this annotation

GitHub Actions / run-lint

'SkillCategory' is defined but never used
} from "../../types";

const reviewDashboardType = gql`
type ReviewerDTO {
Expand All @@ -17,13 +25,13 @@
totalScore: Int
}

type Review {
passionFSG: Int
teamPlayer: Int
desireToLearn: Int
skill: Int
skillCategory: String
comments: String
type ReviewedApplicantRecordDTO {
applicantRecordId: String!
reviewerId: Int!
review: Review
status: String!
score: Int
reviewerHasConflict: Boolean!
}

type ReviewDetails {
Expand Down Expand Up @@ -51,6 +59,10 @@

reviewDashboardSidePanel(applicantId: String!): ReviewDashboardSidePanelDTO!
}

extend type Mutation {
delegateReviewers(positions: [String!]!): [ReviewedApplicantRecordDTO!]!
}
`;

export default reviewDashboardType;
114 changes: 113 additions & 1 deletion backend/typescript/services/implementations/reviewDashboardService.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
import { Op } from "sequelize";
import {
PositionTitle,
ReviewDashboardRowDTO,
ReviewedApplicantRecordDTO,
CreateReviewedApplicantRecordDTO,
ReviewDashboardSidePanelDTO,
PositionTitle,
} from "../../types";
import IReviewDashboardService from "../interfaces/IReviewDashboardService";
import { getErrorMessage } from "../../utilities/errorUtils";
import logger from "../../utilities/logger";
import ApplicantRecord from "../../models/applicantRecord.model";
import User from "../../models/user.model";
import ReviewedApplicantRecordService from "./reviewedApplicantRecordService";

const Logger = logger(__filename);

const reviewedApplicantRecordService = new ReviewedApplicantRecordService();

function toDTO(model: ApplicantRecord): ReviewDashboardRowDTO {
return {
firstName: model.applicant!.firstName,
Expand Down Expand Up @@ -137,6 +144,111 @@ class ReviewDashboardService implements IReviewDashboardService {
throw error;
}
}

async delegateReviewers(
positions: string[],
): Promise<ReviewedApplicantRecordDTO[]> {
// NOTE: We do not have to concern ourselves with locality. That is, each user can be
// assigned to the same partner every time.

const delegations = Array<CreateReviewedApplicantRecordDTO>();
// maps (applicant_record_id) => pair of user_ids assigned to it

// STEP 1:
// Populate the FSM
// NOTE: need to add a sentinel value at the end of the list if the number of user is odd.
// The last 'real' user will bear the burden of solo reviewing.

// Get users and group by position
const groups = (
await User.findAll({
attributes: { exclude: ["createdAt", "updatedAt"] },
where: { position: { [Op.in]: positions } },
})
).reduce((map, user) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const pos = user.position!;
const arr = map.get(pos) ?? [];
arr.push(user.id);
map.set(pos, arr);
return map;
}, new Map<string, number[]>());

// Build FSM
// maps (position title) => (current index of list, list of users with position_title)
const FSM = new Map<string, [number, (number | undefined)[]]>(
positions.map((title) => [title, [0, groups.get(title) ?? []]]),
);

// Validate FSM for correctness
Array.from(FSM.entries()).forEach(([title, [, userIds]]) => {
if (userIds.length === 0) {
// no users with this position
throw new Error(`Invalid amount of users with position ${title}.`);
}
if (userIds.length % 2 !== 0) {
// sentinel value of undefined at the end
userIds.push(undefined);
}
});

// STEP 2:
// Round robin with the FSM
/*
for (auto& a : applicant_records) {
pair<int,vector<string>>& position_entry = FSM[a.position];

// get first user
string id1 = position_entry.second[position_entry.first];
position_entry.first++;
position_entry.first %= position_entry.second.size();

// get second user
string id2 = position_entry.second[position_entry.first];
position_entry.first++;
position_entry.first %= position_entry.second.size();

delegations[a.id] = make_pair(id1, id2);
}
*/
const applicantRecords = await ApplicantRecord.findAll({
attributes: { exclude: ["createdAt", "updatedAt"] },
where: { position: { [Op.in]: positions } },
});
applicantRecords.forEach((record) => {
/* eslint-disable @typescript-eslint/no-non-null-assertion */
const [count, userIds] = FSM.get(record.position)!;
let newCount = count;
const assignedReviewer1 = FSM.get(record.position)![1][newCount];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: Could FSM.get(record.position)![1] be replaced by userIds from line 220?

newCount++;
newCount %= FSM.get(record.position)![1].length;
const assignedReviewer2 = FSM.get(record.position)![1][newCount];
newCount++;
newCount %= FSM.get(record.position)![1].length;
FSM.set(record.position, [newCount, userIds]);

if (assignedReviewer1 !== undefined) {
delegations.push({
applicantRecordId: record.id,
reviewerId: assignedReviewer1,
});
}

if (assignedReviewer2 !== undefined) {
delegations.push({
applicantRecordId: record.id,
reviewerId: assignedReviewer2,
});
}
});

// STEP 3:
// Batch the delegations into ReviewedApplicantRecords
// NOTE: do not add the sentinel value we inserted earlier.
return reviewedApplicantRecordService.bulkCreateReviewedApplicantRecord(
delegations,
);
}
}

export default ReviewDashboardService;
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
ReviewDashboardRowDTO,
ReviewDashboardSidePanelDTO,
ReviewedApplicantRecordDTO,
} from "../../types";

interface IReviewDashboardService {
Expand All @@ -14,6 +15,13 @@ interface IReviewDashboardService {
resultsPerPage: number,
): Promise<ReviewDashboardRowDTO[]>;

/**
* Assigns each user to an applicant record to review, and
* returns the newly created ReviewedApplicantRecords
* @Param positions the list of positions the algorithm should run on
*/
delegateReviewers(positions: string[]): Promise<ReviewedApplicantRecordDTO[]>;

/**
* Fetch data that can fill out the review dashboard side panel for an applicant
* @Param applicantId the ID of the applicant
Expand Down
Loading