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

First iteration of teammate adding route #286

Open
wants to merge 19 commits into
base: 2024-root
Choose a base branch
from
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
7 changes: 6 additions & 1 deletion backend/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ import {
} from "./routes/application_info";
import { getMeetInfo, setMeetInfo } from "./routes/meet_info";
import { getUsedMeals, setUsedMeals } from "./routes/used_meals";
import { getWorkshopList, setWorkshopList } from "./routes/workshop_info";
import { getCheckIn, setCheckIn } from "./routes/check_in";
import { getTeamInfo, setTeamInfo } from "./routes/team_info";
import { addTeammate, getTeamInfo, removeTeammate, setTeamInfo } from "./routes/team_info";
import { getSubmitInfo, setSubmitInfo } from "./routes/submit_info";
import { getUserDetail } from "./routes/user_detail";
import { getUserList, getUserStats, getMeetList } from "./routes/user_list";
Expand Down Expand Up @@ -185,6 +186,10 @@ authenticatedRoute.get("/users/:userId/forms/submit_info", getSubmitInfo);
authenticatedRoute.put("/users/:userId/forms/submit_info", setSubmitInfo);
authenticatedRoute.get("/users/:userId/forms/team_info", getTeamInfo);
authenticatedRoute.put("/users/:userId/forms/team_info", setTeamInfo);
authenticatedRoute.get("/users/:userId/forms/workshop_info", getWorkshopList);
authenticatedRoute.put("/users/:userId/forms/workshop_info", setWorkshopList);
authenticatedRoute.put("/users/:userId/forms/add_teammate", addTeammate);
authenticatedRoute.put("/users/:userId/forms/remove_teammate", removeTeammate);

// What permission should this one be?
authenticatedRoute.get("/users/:userId/status", getApplicationStatus);
Expand Down
5 changes: 5 additions & 0 deletions backend/models/Application.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ export interface ITeamInfo {
teamList?: String;
}

export interface IWorkshopInfo {
workshopList?: String;
}

export interface ICheckIn {
checkInStatus?: Boolean;
}
Expand Down Expand Up @@ -101,6 +105,7 @@ export interface IApplication extends Document {
used_meals: IUsedMeals;
check_in: ICheckIn;
team_info: ITeamInfo;
workshop_info: IWorkshopInfo;
// we can conceivably add additional forms here.
};
admin_info: {
Expand Down
2 changes: 2 additions & 0 deletions backend/models/ApplicationAnyYear.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import meetInfoSchema from "./meetInfoSchema";
import usedMealsSchema from "./usedMealsSchema";
import checkInSchema from "./checkInSchema";
import teamInfoSchema from "./teamInfoSchema";
import workshopInfoSchema from "./workshopInfoSchema";
import submitInfoSchema from "./submitInfoSchema";
import reviewSchema from "./reviewSchema";
import { STATUS, TRANSPORTATION_STATUS } from "../constants";
Expand All @@ -22,6 +23,7 @@ export const applicationSchema: Schema = new mongoose.Schema({
"used_meals": usedMealsSchema,
"check_in": checkInSchema,
"team_info": teamInfoSchema,
"workshop_info": workshopInfoSchema,
"submit_info": submitInfoSchema
},
"admin_info": adminInfoSchema, // Only editable by admin.
Expand Down
8 changes: 8 additions & 0 deletions backend/models/workshopInfoSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import mongoose from "mongoose";
import { Schema } from "mongoose";

const workshopInfoSchema: Schema = new mongoose.Schema({
workshopList: String,
}, { _id: false });

export default workshopInfoSchema;
6 changes: 3 additions & 3 deletions backend/routes/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ import { prepopulateMeetInfo } from "./meet_info";
export function getDeadline(type) {
switch (type) {
case "is":
return new Date("2023-12-13:45:00.000Z");
return new Date("2024-02-18:45:00.000Z");
case "stanford":
return new Date("2024-02-15T07:59:00.000Z");
return new Date("2024-02-18T07:59:00.000Z");
case "oos":
default:
return new Date("2023-12-13:45:00.000Z");
return new Date("2024-02-18:45:00.000Z");
}
}

Expand Down
209 changes: 208 additions & 1 deletion backend/routes/team_info.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { Request, Response } from 'express';
import { getApplicationAttribute, setApplicationAttribute } from "./common";
import { IApplication } from '../models/Application.d';
import Application from '../models/Application';
import { HACKATHON_YEAR } from '../constants';
import { mergeWith, pickBy, without } from "lodash";

export function getTeamInfo(req: Request, res: Response) {
return getApplicationAttribute(req, res, (e: IApplication) => {
Expand All @@ -15,4 +18,208 @@ export function setTeamInfo(req: Request, res: Response) {
},
e => e.forms.team_info
);
}
}

// parse existing teammate list, or create one with only user if doesn't exist
function parseList(list: string | null, email: string): Record<string, number> {
try {
return JSON.parse(list);
} catch (_) {
return { [email]: 1 };
}
}

// filter out pending teammates from team list
function filterPending(list: Record<string, number>) {
return pickBy(list, value => value !== 0);
}

// filter out approved teammates from team list
function filterApproved(list: Record<string, number>) {
return pickBy(list, value => value !== 1);
}

// combine team lists, preferring a confirmed teammate (a number 1) over a pending teammate (a number 0)
function combineLists(one: Record<string, number>, two: Record<string, number>): Record<string, number> {
return mergeWith(one, two, (oneValue, twoValue) => oneValue || twoValue);
}

function applicationByEmail(email: string): PromiseLike<IApplication | null> {
return Application.findOne(
{ "user.email": email, year: HACKATHON_YEAR },
{ __v: 0, reviews: 0 }
);
}

// add each teammate from `two` to the teamList of each user in `one`
async function combineTeams(one: Record<string, number>, two: Record<string, number>) {
for (const email of Object.keys(one)) {
const teammate = await applicationByEmail(email);

if (!teammate) {
throw new Error("Teammate not found.");
}

const teammateList = parseList(teammate.forms.team_info.teamList.toString(), email);
const newTeam = combineLists(teammateList, two);

teammate.forms.team_info.teamList = JSON.stringify(newTeam);
await teammate.save();
}
}

export async function addTeammate(req: Request, res: Response) {
// find request user's application
const user: IApplication | null = await Application.findOne(
{ "user.id": req.params.userId },
{ __v: 0, reviews: 0 }
);

if (!user) {
res.status(404).json({message: "User application not found."});
return;
}

// find requested teammate user's application
const teammate = await applicationByEmail(req.body.email);

if (!teammate) {
res.status(404).json({message: "Teammate application not found."});
return;
}

// filter out user's pending teammates
const userList = parseList(user.forms.team_info.teamList.toString(), user.user.email);
const userConfirmed = filterPending(userList);

// filter out teammate's pending teammates
const teammateList = parseList(teammate.forms.team_info.teamList.toString(), teammate.user.email);
const teammateConfirmed = filterPending(teammateList);

// requested teammate hasn't added user
if (!teammateList.hasOwnProperty(user.user.email)) {
// don't allow user to have more than 8 pending teammates
if (Object.keys(filterApproved(userList)).length > 8) {
res.status(400).json({message: "Too many pending teammates."});
return;
}

userList[teammate.user.email] = 0;
user.forms.team_info.teamList = JSON.stringify(userList);

await user.save();
res.status(200).json(user.forms.team_info);
return;
}

// check combined teams at most four people
const combinedConfirmed = combineLists(userConfirmed, teammateConfirmed);
if (Object.keys(combinedConfirmed).length > 4) {
res.status(400).json({message: "Too many combined teammates."});
return;
}

// for each of user's current teammate, combine with requested teammate
try {
await combineTeams(userConfirmed, teammateConfirmed);
} catch (e) {
if (e instanceof Error) {
res.status(404).json({message: e.message});
return;
}
}

// for each of requeste teammates's current teammate, combine with user
try {
await combineTeams(teammateConfirmed, userConfirmed);
} catch (e) {
if (e instanceof Error) {
res.status(404).json({message: e.message});
return;
}
}

res.status(200).json(user.forms.team_info);
}

// remove an email `removed` from each confirmed teammate in `team`
async function removeTeammateFromAll(team: Record<string, number>, removed: string) {
for (const email of without(Object.keys(team), removed)) {
const teammate = await applicationByEmail(email);

if (!teammate) {
throw new Error("Teammate not found.");
}

const teammateList = parseList(teammate.forms.team_info.teamList.toString(), email);
delete teammateList[removed];

teammate.forms.team_info.teamList = JSON.stringify(teammateList);
await teammate.save();
}
}

export async function removeTeammate(req: Request, res: Response) {
// find request user's application
const user: IApplication | null = await Application.findOne(
{ "user.id": req.params.userId },
{ __v: 0, reviews: 0 }
);

if (!user) {
res.status(404).json({message: "User application not found."});
return;
}

// find removed teammate user's application
const teammate = await applicationByEmail(req.body.email);

if (!teammate) {
res.status(404).json({message: "Teammate application not found."});
return;
}

// filter out user's pending teammates
const userList = parseList(user.forms.team_info.teamList.toString(), user.user.email);
const userConfirmed = filterPending(userList);

// ensure email exists in team
if (!userList.hasOwnProperty(teammate.user.email)) {
res.status(400).json({message: "Removed teammate is not in user's team list."});
return;
}

// if pending, only remove from user's team list
if (!userConfirmed.hasOwnProperty(teammate.user.email)) {
delete userList[teammate.user.email];
user.forms.team_info.teamList = JSON.stringify(userList);
await user.save()

res.status(200).json(user.forms.team_info);
return;
}

// delete all emails except self and pending teammates in deleted user's team list
const teammateList = parseList(teammate.forms.team_info.teamList.toString(), teammate.user.email);
for (const email of without(Object.keys(teammateList), teammate.user.email)) {
if (teammateList[email] === 1) {
delete teammateList[email];
}
}

teammate.forms.team_info.teamList = JSON.stringify(teammateList);
await teammate.save();

// for each user in the team except the deleted user,
// remove the deleted user's email
try {
await removeTeammateFromAll(userConfirmed, teammate.user.email);
} catch (e) {
if (e instanceof Error) {
res.status(404).json({message: e.message});
return;
}
}

res.status(200).json(user.forms.team_info);
}
1 change: 1 addition & 0 deletions backend/routes/user_list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export async function getMeetList(req: Request, res: Response) {
"forms.meet_info": 1,
"forms.application_info.first_name": 1,
"forms.application_info.last_name": 1,
"forms.team_info.teamList": 1,
}
);

Expand Down
18 changes: 18 additions & 0 deletions backend/routes/workshop_info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Request, Response } from 'express';
import { getApplicationAttribute, setApplicationAttribute } from "./common";
import { IApplication } from '../models/Application.d';

export function getWorkshopList(req: Request, res: Response) {
return getApplicationAttribute(req, res, (e: IApplication) => {
return e.forms.workshop_info || {};
}, true);
}

export function setWorkshopList(req: Request, res: Response) {
return setApplicationAttribute(req, res,
(e: IApplication) => {
e.forms.workshop_info = req.body;
},
e => e.forms.workshop_info
);
}
8 changes: 4 additions & 4 deletions src/themes/timber_pine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,20 +63,20 @@ export default {
{
key: "oos",
label: "out-of-state",
date: "2023-12-12T07:59:00.000Z",
date: "2024-02-20T07:59:00.000Z",
display_date: "December 11th, 2023",
},
{
key: "is",
label: "in-state",
date: "2023-12-12T07:59:00.000Z",
date: "2024-02-20T07:59:00.000Z",
display_date: "December 11th, 2023",
},
{
key: "stanford",
label: "Stanford student",
date: "2024-02-15T07:59:00.000Z",
display_date: "February 15th, 2024",
date: "2024-02-18T07:59:00.000Z",
display_date: "February 18th, 2024",
},
],
logo: require("./assets/logo.svg"),
Expand Down