diff --git a/backend/typescript/delegation-algorithm/README.md b/backend/typescript/delegation-algorithm/README.md new file mode 100644 index 0000000..b2974a1 --- /dev/null +++ b/backend/typescript/delegation-algorithm/README.md @@ -0,0 +1,17 @@ +## Delegation Algorithm + +Run the delegation algorithm from the command line of the running application: + +Install ts-node: + +``` +npm install -g ts-node +``` + +Run the algorithm: + +``` +ts-node /delegation-algorithm/src/index.ts +``` + +The `applicationdashboardtable` should be populated with the review data. \ No newline at end of file diff --git a/backend/typescript/delegation-algorithm/src/index.ts b/backend/typescript/delegation-algorithm/src/index.ts new file mode 100644 index 0000000..29e3f24 --- /dev/null +++ b/backend/typescript/delegation-algorithm/src/index.ts @@ -0,0 +1,64 @@ +import Sequelize from "sequelize" + +import Application from "../../models/application.model"; +import ApplicationDashboardModel from "../../models/applicationDashboard.model"; +import User from "../../models/user.model"; +import { sequelize } from "../../models"; + +import { roundRobinPairs, assignApplicationsToPairs } from "./roundRobinPairs"; + + +const roles = ["Developer", "Designer"]; + +async function runDelegationAlgorithms() { + await Promise.all(roles.map(async function (role) { + await delegationAlgorithm(role); + })) +} + +async function delegationAlgorithm(role: string) { + sequelize.authenticate(); + + const applications = await loadApplications(role); + const reviewers = await loadReviewers(role); + + const uniquePairs = roundRobinPairs(reviewers); + const totalPairs = assignApplicationsToPairs(uniquePairs, applications); + + await Promise.all( + applications.map(async function (application, i) { + return Promise.all( + totalPairs[i].map(async function (reviewer) { + await ApplicationDashboardModel.create({ + applicationId: application.id, + reviewerId: reviewer.id, + reviewerEmail: reviewer.email, + passionFSG: 0, + teamPlayer: 0, + desireToLearn: 0, + skill: 0, + skillCategory: "junior", + reviewerComments: "", + recommendedSecondChoice: "N/A" + }); + }) + ); + }) + ); +} + +async function loadReviewers(role: string): Promise { + return await User.findAll({ + attributes: ["id", "email"], + where: { role: role } + }); +} + +async function loadApplications(role: string): Promise { + return await Application.findAll({ + attributes: ["id"], + where: { firstChoiceRole: { [Sequelize.Op.like]: `%${role}%` } } + }); +} + +runDelegationAlgorithms() diff --git a/backend/typescript/delegation-algorithm/src/roundRobinPairs.ts b/backend/typescript/delegation-algorithm/src/roundRobinPairs.ts new file mode 100644 index 0000000..60f8fbc --- /dev/null +++ b/backend/typescript/delegation-algorithm/src/roundRobinPairs.ts @@ -0,0 +1,51 @@ +import { isEqual } from "lodash"; + +import Application from "../../models/application.model"; +import User from "../../models/user.model"; + + +// Generate a list of all unique pairs of users — from a given list of users +// Uses Round Robin algorithm for optimized time complexity +export function roundRobinPairs(reviewers: (User | {})[]): [User, User][] { + if (reviewers.length % 2 !== 0) { + reviewers.push({}); + } + + const fixedReviewer = reviewers[0]; + let rotatingReviewers = reviewers.slice(1); + const pairs: [(User | {}), (User | {})][] = []; + + for (let i = 0; i < reviewers.length - 1; i++) { + for (let j = 0; j < reviewers.length / 2 - 1; j++) { + if (!isEqual(rotatingReviewers[j], {}) && !isEqual(rotatingReviewers[reviewers.length - j - 2], {})) { + pairs.push([rotatingReviewers[j], rotatingReviewers[reviewers.length - j - 2]]); + } + } + if (!isEqual(fixedReviewer, {}) && !isEqual(rotatingReviewers[Math.floor(reviewers.length / 2) - 1], {})) { + pairs.push([fixedReviewer, rotatingReviewers[Math.floor(reviewers.length / 2) - 1]]); + } + rotatingReviewers = rotatingReviewers.slice(1).concat(rotatingReviewers.slice(0, 1)); //rotate list + } + + shuffleArray(pairs); + return pairs as [User, User][]; +} + +// Multiply pairs to equal the number of applications +export function assignApplicationsToPairs(pairs: [User, User][], applications: Application[]): [User, User][] { + const totalPairsNeeded = applications.length; + while (pairs.length < totalPairsNeeded) { + pairs.push(...pairs.slice(0, totalPairsNeeded - pairs.length)); + } + + shuffleArray(pairs); + return pairs; +} + +// Shuffle a list of items +function shuffleArray(array: T[]): void { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } +} diff --git a/backend/typescript/migrations/2023.02.18T17.43.41.create-user-table.ts b/backend/typescript/migrations/2023.02.18T17.43.41.create-user-table.ts index 83ef00b..1a54982 100644 --- a/backend/typescript/migrations/2023.02.18T17.43.41.create-user-table.ts +++ b/backend/typescript/migrations/2023.02.18T17.43.41.create-user-table.ts @@ -10,21 +10,24 @@ const SEEDED_DATA = [ last_name: "Doe", email: "johndoe@gmail.com", auth_id: "bide", - role: "User", + permission: "Reviewers", + role: "Developer", }, { first_name: "Jane", last_name: "Doe", email: "janedoe@gmail.ca", auth_id: "none", - role: "Admin", + permission: "Reviewers", + role: "Developer", }, { first_name: "UW", last_name: "Blueprint", email: "recruitmenttools@uwblueprint.org", auth_id: "1Z4wyuonu9MhAi4VoAEiTMVj1iT2", - role: "Admin", + permission: "Reviewers", + role: "Developer", }, ]; // recruitmenttools@uwblueprint.org @@ -55,8 +58,13 @@ export const up: Migration = async ({ context: sequelize }) => { primaryKey: true, allowNull: false, }, + permission: { + type: DataType.ENUM("VP Talent", "Eteam", "Engineering", "Product", "Design", "Reviewers"), + allowNull: false, + defaultValue: "Reviewers" + }, role: { - type: DataType.ENUM("User", "Admin"), + type: DataType.ENUM("Developer", "Designer"), allowNull: false, }, createdAt: DataType.DATE, diff --git a/backend/typescript/models/user.model.ts b/backend/typescript/models/user.model.ts index 92fd943..ba71b32 100644 --- a/backend/typescript/models/user.model.ts +++ b/backend/typescript/models/user.model.ts @@ -1,7 +1,7 @@ /* eslint import/no-cycle: 0 */ import { Column, DataType, HasMany, Model, Table } from "sequelize-typescript"; -import { Role } from "../types"; +import { Permission, Role } from "../types"; import ApplicationDashboardTable from "./applicationDashboard.model"; @Table({ tableName: "users" }) @@ -21,7 +21,10 @@ export default class User extends Model { @Column({ type: DataType.INTEGER, primaryKey: true, autoIncrement: true }) id!: number; - @Column({ type: DataType.ENUM("User", "Admin") }) + @Column({ type: DataType.ENUM("VP Talent", "Eteam", "Engineering", "Product", "Design", "Reviewers"), allowNull: false, defaultValue: 'Reviewers' }) + permission!: Permission; + + @Column({ type: DataType.ENUM("Developer, Designer"), allowNull: false }) role!: Role; @HasMany(() => ApplicationDashboardTable) diff --git a/backend/typescript/types.ts b/backend/typescript/types.ts index d1299dd..3894da7 100644 --- a/backend/typescript/types.ts +++ b/backend/typescript/types.ts @@ -1,4 +1,6 @@ -export type Role = "User" | "Admin"; +export type Permission = "VP Talent" | "Eteam" | "Engineering" | "Product" | "Design" | "Reviewers"; + +export type Role = "Developer" | "Designer"; export enum StatusType { ACCEPTED = "accepted",