diff --git a/backend/app.ts b/backend/app.ts index 72551d5..29bdc05 100644 --- a/backend/app.ts +++ b/backend/app.ts @@ -8,12 +8,14 @@ import teamsRouter from "./judging-algorithm/routes/teams-routes.js" import cabinsRouter from "./cabin-sorting/routes/sortedHackers-routes.js" const app = express(); +const PORT = process.env.PORT || 4000; app.use(express.json()); app.use( cors({ - origin: 'http://localhost:4000', + // problem lies here + origin: [`http://localhost:${PORT}`, 'http://localhost:3000'], credentials: true }) ); @@ -24,7 +26,6 @@ app.use('/', roomsRouter); app.use('/', rotationTimesRouter); app.use('/', teamsRouter); -const PORT = process.env.PORT || 4000; app.listen(PORT, () => { console.log(`Server is running in http://localhost:${PORT}`); diff --git a/backend/cabin-sorting/controllers/sortedHackers-controller.ts b/backend/cabin-sorting/controllers/sortedHackers-controller.ts index cc5b8c2..87ab2ae 100644 --- a/backend/cabin-sorting/controllers/sortedHackers-controller.ts +++ b/backend/cabin-sorting/controllers/sortedHackers-controller.ts @@ -1,17 +1,47 @@ import sortedHackersService from "../service/sortedHackers-service.js"; +import express, { Response, Request } from 'express'; - -const getSortedHackers = async (_req: any, res: any) => { - const sortedHackers = await sortedHackersService.getSortedHackers(); - res.json(sortedHackers) - return sortedHackers; +const getSortedHackers = async (_req: Request, res: Response) => { + try { + const sortedHackers = await sortedHackersService.getSortedHackers(); + if (!sortedHackers || Object.values(sortedHackers).length === 0) { + res.status(404).json({message: "Sorted hackers were null/empty..."}) + } + res.json(sortedHackers) + return sortedHackers; + } catch (err) { + res.status(400).json({message: "Failed to get sorted hackers.", err}) + } + return; }; -const createSortedHacker = async (req: any, res: any) => { - const hackerInformation = req.body; - const createResponse = await sortedHackersService.createSortedHacker(hackerInformation) - res.json(createResponse) - return createResponse; +const createSortedHacker = async (req: Request, res: Response) => { + try { + const hackerInformation = req.body; + const createResponse = await sortedHackersService.createSortedHacker(hackerInformation) + if (!createResponse || Object.values(createResponse).length === 0) { + res.status(404).json({message: "Sorted hackers were null/empty..."}) + } + res.json(createResponse) + return createResponse; + } catch (err) { + res.status(400).json({message: "Failed to create sorted hackers.", err}) + } + return; }; -export default {getSortedHackers, createSortedHacker}; \ No newline at end of file +const groupHackersByCabin = async (_req: Request, res: Response) => { + try { + const groupedHackers = await sortedHackersService.groupHackersByCabin() + if (!groupedHackers || Object.values(groupedHackers).length === 0) { + res.status(404).json({message: "Grouped hackers were null/empty..."}) + } + res.json(groupedHackers) + return groupedHackers + } catch (err) { + res.status(400).json({message: "Failed to group hackers by cabin.", err}) + } + return; +} + +export default {getSortedHackers, createSortedHacker, groupHackersByCabin }; \ No newline at end of file diff --git a/backend/cabin-sorting/data/json_outputs/sortedHackers.json b/backend/cabin-sorting/data/json_outputs/sortedHackers.json index 81291e6..f3220fb 100644 --- a/backend/cabin-sorting/data/json_outputs/sortedHackers.json +++ b/backend/cabin-sorting/data/json_outputs/sortedHackers.json @@ -1 +1,128 @@ -[{"question0":"answer1","question1":"answer1","question2":"answer1","question3":"answer1","question4":"answer1","question5":"answer1","question6":"answer1","question7":"answer1","question8":"answer1","question9":"answer1","question10":"answer1","question11":"answer1","id":"64accf3dd36486421bc50e53","email":"test","assignedCabin":"cabin1","secondAssignedCabin":" cabin2"},{"question0":"answer2","question1":"answer2","question2":"answer2","question3":"answer2","question4":"answer2","question5":"answer2","question6":"answer2","question7":"answer2","question8":"answer2","question9":"answer2","question10":"answer2","question11":"answer2","id":"64accf66d36486421bc50e55","email":"test2","assignedCabin":" cabin2","secondAssignedCabin":"cabin1"},{"question0":"answer3","question1":"answer3","question2":"answer3","question3":"answer3","question4":"answer3","question5":"answer3","question6":"answer3","question7":"answer3","question8":"answer3","question9":"answer3","question10":"answer3","question11":"answer3","id":"64accf7fd36486421bc50e57","email":"test3","assignedCabin":" cabin3","secondAssignedCabin":"cabin1"},{"question0":"answer4","question1":"answer4","question2":"answer4","question3":"answer4","question4":"answer4","question5":"answer4","question6":"answer4","question7":"answer4","question8":"answer4","question9":"answer4","question10":"answer4","question11":"answer4","id":"64accf8ed36486421bc50e59","email":"test4","assignedCabin":" cabin4","secondAssignedCabin":"cabin1"}] \ No newline at end of file +[ + { + "id": "64accf3dd36486421bc50e53", + "email": "test", + "question0": "answer1", + "question1": "answer1", + "question2": "answer1", + "question3": "answer1", + "question4": "answer1", + "question5": "answer1", + "question6": "answer1", + "question7": "answer1", + "question8": "answer1", + "question9": "answer1", + "question10": "answer1", + "question11": "answer1", + "assignedCabin": "cabin1", + "secondAssignedCabin": "cabin2" + }, + { + "id": "64accf66d36486421bc50e55", + "email": "test2", + "question0": "answer2", + "question1": "answer2", + "question2": "answer2", + "question3": "answer2", + "question4": "answer2", + "question5": "answer2", + "question6": "answer2", + "question7": "answer2", + "question8": "answer2", + "question9": "answer2", + "question10": "answer2", + "question11": "answer2", + "assignedCabin": "cabin2", + "secondAssignedCabin": "cabin1" + }, + { + "id": "64accf7fd36486421bc50e57", + "email": "test3", + "question0": "answer3", + "question1": "answer3", + "question2": "answer3", + "question3": "answer3", + "question4": "answer3", + "question5": "answer3", + "question6": "answer3", + "question7": "answer3", + "question8": "answer3", + "question9": "answer3", + "question10": "answer3", + "question11": "answer3", + "assignedCabin": "cabin3", + "secondAssignedCabin": "cabin1" + }, + { + "id": "64accf8ed36486421bc50e59", + "email": "test4", + "question0": "answer4", + "question1": "answer4", + "question2": "answer4", + "question3": "answer4", + "question4": "answer4", + "question5": "answer4", + "question6": "answer4", + "question7": "answer4", + "question8": "answer4", + "question9": "answer4", + "question10": "answer4", + "question11": "answer4", + "assignedCabin": "cabin4", + "secondAssignedCabin": "cabin1" + }, + { + "id": "64ae0242cdcfed39b8b59958", + "email": "test4", + "question0": "answer4", + "question1": "answer4", + "question2": "answer4", + "question3": "answer4", + "question4": "answer4", + "question5": "answer4", + "question6": "answer4", + "question7": "answer4", + "question8": "answer4", + "question9": "answer4", + "question10": "answer4", + "question11": "answer4", + "assignedCabin": "cabin4", + "secondAssignedCabin": "cabin1" + }, + { + "id": "64ae024dcdcfed39b8b5995a", + "email": "test4", + "question0": "answer4", + "question1": "answer4", + "question2": "answer4", + "question3": "answer4", + "question4": "answer4", + "question5": "answer4", + "question6": "answer4", + "question7": "answer4", + "question8": "answer4", + "question9": "answer4", + "question10": "answer4", + "question11": "answer4", + "assignedCabin": "cabin4", + "secondAssignedCabin": "cabin1" + }, + { + "id": "64b0a5b67c6e7f3511e8509e", + "email": "test4", + "question0": "answer4", + "question1": "answer4", + "question2": "answer4", + "question3": "answer4", + "question4": "answer4", + "question5": "answer4", + "question6": "answer4", + "question7": "answer4", + "question8": "answer4", + "question9": "answer4", + "question10": "answer4", + "question11": "answer4", + "assignedCabin": "cabin4", + "secondAssignedCabin": "cabin1" + } +] \ No newline at end of file diff --git a/backend/cabin-sorting/routes/sortedHackers-routes.ts b/backend/cabin-sorting/routes/sortedHackers-routes.ts index c37c86a..bcafb30 100644 --- a/backend/cabin-sorting/routes/sortedHackers-routes.ts +++ b/backend/cabin-sorting/routes/sortedHackers-routes.ts @@ -6,4 +6,6 @@ const router = express.Router(); router.get('/sortedHackers', controller.getSortedHackers); router.post('/sortedHackers', controller.createSortedHacker); +router.get('/groupedHackers', controller.groupHackersByCabin); + export default router; diff --git a/backend/cabin-sorting/schemas/sortedHackers-schema.ts b/backend/cabin-sorting/schemas/sortedHackers-schema.ts index 5813a91..65276c2 100644 --- a/backend/cabin-sorting/schemas/sortedHackers-schema.ts +++ b/backend/cabin-sorting/schemas/sortedHackers-schema.ts @@ -15,6 +15,7 @@ export const sortedHackersSchema = new mongoose.Schema( versionKey: false }, ); + export default sortedHackersSchema; diff --git a/backend/cabin-sorting/service/sortedHackers-service.ts b/backend/cabin-sorting/service/sortedHackers-service.ts index 1733daf..2f07179 100644 --- a/backend/cabin-sorting/service/sortedHackers-service.ts +++ b/backend/cabin-sorting/service/sortedHackers-service.ts @@ -1,35 +1,166 @@ -import { Types } from "mongoose"; -import * as sortedHackersDao from "../dao/sortedHackers-dao.js"; +import * as fs from 'fs'; +import * as path from 'path'; +import { parse } from 'csv-parse/sync'; +import { Types } from 'mongoose'; +import * as sortedHackersDao from '../dao/sortedHackers-dao.js'; +import { FormattedHacker, Hacker } from '../types.js'; +import sortedHackersSchema from '../schemas/sortedHackers-schema.js'; + +let CABIN_SIZE: number; +let QUESTIONS_SIZE: number; +let hackerList: HackerDataType[]; +let answerList: string[]; +let cabinList: string[]; interface HackerDataType { - id: string, - email: string, - [key: string] : string, + id: string; + email: string; + [key: string]: string; } -const getSortedHackers = async () => { - const rawHackerData = await sortedHackersDao.getSortedHackers(); - const formattedHackerData = rawHackerData.map(hackerData => { - const {_id, email, applicationResponses} = hackerData - const initialHackerData : HackerDataType = { - id: _id.toString(), - email: email, - } +const getSortedHackers = async (): Promise => { + const rawHackerData = await sortedHackersDao.getSortedHackers(); + const formattedHackerData = rawHackerData.map((hackerData) => { + const { _id, email, applicationResponses } = hackerData; + const initialHackerData: HackerDataType = { + id: _id.toString(), + email: email + }; - function formatApplicationResponses(accumulatedResponse : HackerDataType, currentResponseKey : any, currentResponseIndex : number) { - const currentResponseQuestionKey = "question" + currentResponseIndex; - accumulatedResponse[currentResponseQuestionKey] = applicationResponses[currentResponseKey] - return accumulatedResponse - } + function formatApplicationResponses( + accumulatedResponse: HackerDataType, + currentResponseKey: any, + currentResponseIndex: number + ) { + const currentResponseQuestionKey = 'question' + currentResponseIndex; + accumulatedResponse[currentResponseQuestionKey] = + applicationResponses[currentResponseKey]; + return accumulatedResponse; + } - return Object.keys(applicationResponses).reduce(formatApplicationResponses, initialHackerData) - }) - return formattedHackerData; + return Object.keys(applicationResponses).reduce( + formatApplicationResponses, + initialHackerData + ); + }); + return formattedHackerData; }; -const createSortedHacker = async (hacker : any) => { - const createResponse = await sortedHackersDao.createdSortedHacker(hacker) +const createSortedHacker = async (hacker: Hacker): Promise => { + const createResponse = await sortedHackersDao.createdSortedHacker(hacker); return createResponse; }; - -export default {getSortedHackers, createSortedHacker}; \ No newline at end of file + +const groupHackersByCabin = async (): Promise => { + const hackers = await assignHackerCabins(); + let cabinEmails: string[][] = []; + + + const groupedHackers = hackers.reduce((accum, hacker) => { + const {assignedCabin, email, id} = hacker + const cabinNum = cabinList.indexOf(assignedCabin); + if (cabinNum === -1) { + console.error( + `Cabin assigned to Hacker ${id} could not be found` + ); + return accum; + } + if (!accum[cabinNum]) { + accum[cabinNum] = [] + } + accum[cabinNum].push(email); + + return accum; + }, cabinEmails); + + return groupedHackers +}; + +function loadCSV(filepath: string, headers: boolean): any[] { + const csvFileAbsolutePath = path.resolve( + 'cabin-sorting', + 'data', + 'csv_inputs', + filepath); + + // error handling in case file is missing + let fileContent; + try { + fileContent = fs.readFileSync(csvFileAbsolutePath, { + encoding: 'utf-8' + }); + } catch (err) { + console.log(`File cannot be found: "${filepath}"`); + return []; + } + + const options = { + delimiter: ',', + columns: headers + }; + return parse(fileContent, options); +} + +async function assignHackerCabins() { + hackerList = await getSortedHackers(); + answerList = loadCSV('answer.csv', true); + cabinList = loadCSV('cabinTypes.csv', false)[0]; + + CABIN_SIZE = Object.keys(cabinList).length; + QUESTIONS_SIZE = Object.keys(answerList[0]).length; + + // ensuring the CSV files exists before continuing + if ( + hackerList.length === 0 || + answerList.length === 0 || + cabinList.length === 0 + ) { + console.error( + 'Please add the respective csv file(s) to the folder to run the sorting algorithm' + ); + return []; + } else { + matchAnswers(); + return hackerList; + } +} + +function matchAnswers() { + hackerList.forEach((hacker: any) => { + // each element = a different cabin, all initialized to 0 + const cabinScore = Array(CABIN_SIZE).fill(0); + + incrementCabinScores(hacker, cabinScore); + + // create extra column for hacker that determines the cabin they should + // join (the one with the most points) + const cabinOptions: string[] = Object.values(cabinList); + const maxIndex: number = cabinScore.indexOf(Math.max(...cabinScore)); + hacker.assignedCabin = cabinOptions[maxIndex]; + + // find backup cabin for hacker (in case the first choice fills up) + const counterCopy = cabinScore.slice(); + counterCopy[maxIndex] = -1; + hacker.secondAssignedCabin = + cabinOptions[counterCopy.indexOf(Math.max(...counterCopy))]; + }); +} + +function incrementCabinScores(hacker: any, cabinScore: number[]) { + answerList.forEach((cabin: any, cabinIndex: number) => { + for ( + let questionIndex = 0; + questionIndex < QUESTIONS_SIZE; + questionIndex++ + ) { + if ( + cabin['question' + questionIndex.toString()] === + hacker['question' + questionIndex.toString()] + ) { + cabinScore[cabinIndex]++; + } + } + }); +} + +export default { getSortedHackers, createSortedHacker, groupHackersByCabin }; diff --git a/backend/judging-algorithm/yarn.lock b/backend/judging-algorithm/yarn.lock index 33402d1..e44865b 100644 --- a/backend/judging-algorithm/yarn.lock +++ b/backend/judging-algorithm/yarn.lock @@ -2353,6 +2353,11 @@ babel-preset-jest@^29.5.0: babel-plugin-jest-hoist "^29.5.0" babel-preset-current-node-syntax "^1.0.0" +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + base64-js@^1.3.1: version "1.5.1" resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" @@ -2372,6 +2377,14 @@ bowser@^2.11.0: resolved "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz" integrity sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA== +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + braces@^3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz" @@ -2535,6 +2548,11 @@ commondir@^1.0.1: resolved "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz" integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + convert-source-map@^1.6.0, convert-source-map@^1.7.0: version "1.9.0" resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz" diff --git a/ui/components/templateDropdown/selectedCabin.tsx b/ui/components/templateDropdown/selectedCabin.tsx index a66200c..7a34a5f 100644 --- a/ui/components/templateDropdown/selectedCabin.tsx +++ b/ui/components/templateDropdown/selectedCabin.tsx @@ -18,7 +18,10 @@ type SelectedCabinProps = { cabinValues: string[][]; }; -export default function SelectedCabin ({ cabinNames, cabinValues }: SelectedCabinProps) { +export default function SelectedCabin ({ + cabinNames, + cabinValues +}: SelectedCabinProps) { const [selectedItem, setSelectedItem] = React.useState(-1) const [copied, setCopied] = React.useState(false) @@ -26,7 +29,7 @@ export default function SelectedCabin ({ cabinNames, cabinValues }: SelectedCabi const handleChange = (event: SelectChangeEvent) => { const str = event.target.value - setSelectedItem(Number(str.charAt(str.length - 1))) + setSelectedItem(Number(str.charAt(str.length - 1)) - 1) setCopied(false) setOpenSnackBar(false) } @@ -57,43 +60,42 @@ export default function SelectedCabin ({ cabinNames, cabinValues }: SelectedCabi ))} - {copied - ? ( + + { + if (!cabinValues[selectedItem]) { + return + } + navigator.clipboard.writeText(cabinValues[selectedItem].toString()) + setCopied(true) + setOpenSnackBar(true) + }} + onMouseEnter={() => setHoverCopy(true)} + onMouseLeave={() => setHoverCopy(false)} + > + + + + + + {copied && ( - ) - : ( - selectedItem && ( - { - navigator.clipboard.writeText(cabinValues[selectedItem].toString()) - setCopied(true) - setOpenSnackBar(true) - }} - onMouseEnter={() => setHoverCopy(true)} - onMouseLeave={() => setHoverCopy(false)} - > - - - - - ) - )} - + )} {openSnackBar && ( = async () => { + const url = process.env.GROUPED_HACKERS_URL || '' + const res = await axios.get(url) + const data = res.data + return { props: { data } } +} + +export default function CabinSorting (props : InferGetStaticPropsType) { + const cabinValues = props.data const cabinHeaders: string[] = [ 'Cabin 1', 'Cabin 2', @@ -18,20 +30,14 @@ export default function CabinSorting () { 'Cabin 6' ] - const cabinValues: string[][] = [ - ['email1-1', 'email1-2'], - ['email2-1', 'email2-2', 'email2-3'], - ['email3-1', 'email3-2'], - ['email4-1', 'email4-2'], - [], - ['email6-1', 'email6-2'] - ] + const rows: string[][] = [] + cabinHeaders.forEach(() => { + rows.push([]) + }) - const rows: string[][] = [[]] - Object.values(cabinValues).forEach((value: any, index: number) => { + cabinValues.forEach((value: any, index: number) => { value.forEach((entry: string, entryIndex: number) => { - if (rows.length < entryIndex + 1) rows.push([]) - rows[entryIndex][index] = entry + rows[index][entryIndex] = entry }) }) @@ -73,11 +79,11 @@ export default function CabinSorting () {
- +
Copy email list
- + diff --git a/yarn.lock b/yarn.lock index 57ab204..53d2efa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -207,7 +207,7 @@ human-signals@^4.3.0: resolved "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz" integrity sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ== -husky@^8.0.1: +husky@^8.0.3: version "8.0.3" resolved "https://registry.yarnpkg.com/husky/-/husky-8.0.3.tgz#4936d7212e46d1dea28fef29bb3a108872cd9184" integrity sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg== @@ -300,6 +300,13 @@ log-update@^4.0.0: slice-ansi "^4.0.0" wrap-ansi "^6.2.0" +loose-envify@^1.1.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + memory-pager@^1.0.2: version "1.5.0" resolved "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz" @@ -415,6 +422,13 @@ punycode@^2.1.1: resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz" integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== +react@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" + integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== + dependencies: + loose-envify "^1.1.0" + restore-cursor@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz"