Skip to content

Commit 657ccfa

Browse files
joshuazhou744Joshua ZhoujanekhuongMikaVohltektaxi
committed
Feat/Team Checkin Form routes, validators, controllers, and middleware (#974)
* Create checkin form submit endpoint * Finish hacker checkin form submit endpoint * Change checkin form spreadsheet * api route for checking in teams * Add devpost and discord tag field validation * Update db with devpost url * Add url validation: * added team checkin form status to settings * remove workshops * more validation * test * Updated email template dates and header logo * Added hackboard role support * Revert "Added hackboard role support" This reverts commit 481d303. * Feat/auto emails (#949) * Update email templates with tentative dates * Create service for sending automated status emails * Created function to find hacker by their status * API routing for automated emails * Cleaner code * API routing for getStatusCount function * Added emails.js logic to middleware and controller files * fixed authorization bug * Added hackboard role support * Revert "Added hackboard role support" This reverts commit 481d303. * update email dates --------- Co-authored-by: Joshua Zhou <joshuazhou744@gmail.com> Co-authored-by: Mika Vohl <mikavohl@gmail.com> Co-authored-by: Tavi Pollard <tavienpollard@gmail.com> * Feat/create application review (#944) * not finished create app rev * Reviewer status: 1/2 works (needs to enter twice??) * Hacker Reviewer Feature * Cleaner code * remove uneccessary comments * not finished create app rev * Reviewer status: 1/2 works (needs to enter twice??) * Hacker Reviewer Feature * Cleaner code * remove uneccessary comments * not finished create app rev * Reviewer status: 1/2 works (needs to enter twice??) * Hacker Reviewer Feature * Cleaner code * remove uneccessary comments * not finished create app rev * Reviewer status: 1/2 works (needs to enter twice??) * Hacker Reviewer Feature * Cleaner code * Fixed Application Error: implemented reviewers values from feat/review_filter and fixed the default value errors for reviewer status, name, and comments * Fixed Application Error: implemented reviewers values from feat/review_filter and fixed the default value errors for reviewer status, name, and comments * fixed authorization issues by adding all routes to routes.constant.js with unique IDs * disable emails on status change by admin --------- Co-authored-by: Tavi Pollard <tavienpollard@gmail.com> Co-authored-by: Tavi Pollard <45189395+tektaxi@users.noreply.github.com> * fixed merge issue with objectIDs * Added hackboard role support (#955) * Added hackboard role support * minor edits to hackboard permissions --------- Co-authored-by: Tavi Pollard <tavienpollard@gmail.com> * Updated account invitation email content * Feat/assign reviewers (#959) * Assign 2 reviewers per unreviewed Hacker submitted before cutoff time. Will need a cleaning for documentation * removed to requirement that the reviewer name has to be empty (now it's just filtering if hacker was created before cutoff) * clean * Use string[] parameter instead of hardcoded names --------- Co-authored-by: JAMIE XIAO <jamie.xiao.ca@gmail.com> * Added script for sending interest form emails (#954) * Added script for sending interest form emails * Usage comments * quick fixes to assignReviewers (#961) * quick fixes to assignreviewers * fixed non existant fields conditional statements for assignReviewers * need to fix: reviewer1 & 2 have same name * fixed duplicate names * cut off to dec 1 11:59 pm * update cutoff * date fix?? * add hacker id to team api response schema * new acceptance email * updated Marriott booking deadline * round 2 acceptance email template * updated rejection email * updated wording * added volunteer link * added applied status to validateStatus function * overrode email template to send declined emails to hackers with applied status (#972) * Finish hacker checkin form submit endpoint * format sheet name * remove checkin router dup * fix --------- Co-authored-by: Joshua Zhou <j.zhou11@share.epsb.ca> Co-authored-by: janekhuong <janekhuong05@gmail.com> Co-authored-by: Mika Vohl <mikavohl@gmail.com> Co-authored-by: Tavi Pollard <tavienpollard@gmail.com> Co-authored-by: Jamie Xiao <141690843+JamieXiao@users.noreply.github.com> Co-authored-by: Tavi Pollard <45189395+tektaxi@users.noreply.github.com> Co-authored-by: Mika Vohl <103958325+MikaVohl@users.noreply.github.com> Co-authored-by: JAMIE XIAO <jamie.xiao.ca@gmail.com> Co-authored-by: Mubeen Mohammed <mubeen.mohammed@mail.mcgill.ca>
1 parent a2fabfe commit 657ccfa

File tree

13 files changed

+424
-13502
lines changed

13 files changed

+424
-13502
lines changed

app.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const searchRouter = require("./routes/api/search");
3131
const settingsRouter = require("./routes/api/settings");
3232
const volunteerRouter = require("./routes/api/volunteer");
3333
const roleRouter = require("./routes/api/role");
34+
const checkinRouter = require("./routes/api/checkin");
3435
const emailsRouter = require("./routes/api/emails");
3536

3637
const app = express();
@@ -115,8 +116,12 @@ settingsRouter.activate(apiRouter);
115116
Services.log.info("Settings router activated");
116117
roleRouter.activate(apiRouter);
117118
Services.log.info("Role router activated");
119+
checkinRouter.activate(apiRouter);
120+
Services.log.info("Checkin router activated");
118121
emailsRouter.activate(apiRouter);
119122
Services.log.info("Emails router activated");
123+
checkinRouter.activate(apiRouter);
124+
Services.log.info("Checkin router activated");
120125

121126
app.use("/", indexRouter);
122127
app.use("/api", apiRouter);

constants/checkin-options.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Team checkin form constants for Mchacks 13
2+
"use strict";
3+
4+
const PRIZE_CATEGORIES = [
5+
"Best Beginner Hack",
6+
"Best Design",
7+
"Chaotic Evil",
8+
"Best Use of AI or AI Agents",
9+
];
10+
11+
const SPONSOR_CHALLENGES = [
12+
"HoloRay",
13+
"Athena AI",
14+
"Gumloop",
15+
"National Bank",
16+
"Tail'ed",
17+
"BassiliChat AI",
18+
"Dobson Center",
19+
"Desjardins",
20+
"NOVA",
21+
"CSUS"
22+
];
23+
24+
const MLH_CHALLENGES = [
25+
"Best Use of ElevenLabs",
26+
"Best Use of Gemini API",
27+
"Best Use of MongoDB Atlas",
28+
"Best Use of DigitalOcean",
29+
"Best Use of Solana",
30+
"Best Use of Auth0"
31+
];
32+
33+
module.exports = {
34+
PRIZE_CATEGORIES,
35+
SPONSOR_CHALLENGES,
36+
MLH_CHALLENGES
37+
};

controllers/checkin.controller.js

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"use strict";
2+
3+
const Services = {
4+
Sheets: require('../services/sheets.service'),
5+
Hacker: require('../services/hacker.service'),
6+
Team: require('../services/team.service'),
7+
Account: require('../services/account.service')
8+
};
9+
10+
/**
11+
* @function submitCheckin
12+
* @param {{body: {formData: Object}, user: {id: string}}} req
13+
* @param {*} res
14+
* @return {JSON} Success status
15+
* @description Handles the check-in form submission and adds data to Google Sheets
16+
* Automatically fetches team member emails from the logged-in user's team
17+
*/
18+
async function submitCheckin(req, res) {
19+
try {
20+
// Get logged-in hacker
21+
const hacker = await Services.Hacker.findByAccountId(req.user.id);
22+
23+
if (!hacker) {
24+
return res.status(404).json({
25+
message: "Hacker not found",
26+
data: {}
27+
});
28+
}
29+
30+
// Check hacker has a team
31+
if (!hacker.teamId) {
32+
return res.status(400).json({
33+
message: "You must be part of a team to submit check-in",
34+
data: {}
35+
});
36+
}
37+
38+
// Fetch team data
39+
const team = await Services.Team.findById(hacker.teamId);
40+
41+
if (!team) {
42+
return res.status(404).json({
43+
message: "Team not found",
44+
data: {}
45+
});
46+
}
47+
48+
// Fetch all team member emails
49+
const teamMemberEmails = [];
50+
for (const memberId of team.members) {
51+
const memberHacker = await Services.Hacker.findById(memberId);
52+
if (memberHacker) {
53+
const memberAccount = await Services.Account.findById(memberHacker.accountId);
54+
if (memberAccount) {
55+
teamMemberEmails.push(memberAccount.email);
56+
}
57+
}
58+
}
59+
60+
// Update team's devpostURL in the database if provided
61+
if (req.body.formData.devpostLink) {
62+
await Services.Team.updateOne(hacker.teamId, {
63+
devpostURL: req.body.formData.devpostLink
64+
});
65+
}
66+
67+
// Prepare data for Google Sheets with team member emails
68+
const teamIdString = team._id ? team._id.toString() : hacker.teamId.toString();
69+
70+
const checkinData = {
71+
teamMember1: teamMemberEmails[0] || '',
72+
teamMember2: teamMemberEmails[1] || '',
73+
teamMember3: teamMemberEmails[2] || '',
74+
teamMember4: teamMemberEmails[3] || '',
75+
prizeCategories: req.body.formData.prizeCategories,
76+
sponsorChallenges: req.body.formData.sponsorChallenges,
77+
mlhChallenges: req.body.formData.mlhChallenges,
78+
// workshopsAttended: req.body.formData.workshopsAttended,
79+
discordTag: req.body.formData.discordTag,
80+
devpostLink: req.body.formData.devpostLink,
81+
teamId: teamIdString
82+
};
83+
84+
await Services.Sheets.appendCheckinData(checkinData);
85+
86+
return res.status(200).json({
87+
message: "Check-in data successfully submitted",
88+
data: {}
89+
});
90+
} catch (error) {
91+
console.error('Checkin submission error:', error);
92+
return res.status(500).json({
93+
message: "Error submitting check-in data",
94+
data: {}
95+
});
96+
}
97+
}
98+
99+
module.exports = {
100+
submitCheckin
101+
};

cookies.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Netscape HTTP Cookie File
2+
# https://curl.se/docs/http-cookies.html
3+
# This file was generated by libcurl! Edit at your own risk.

middlewares/settings.middleware.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ async function updateSettings(req, res, next) {
6464
* @description Confirms that openTime < closeTime < confirmTime
6565
*/
6666
function confirmValidPatch(req, res, next) {
67+
if (!req.body.settingsDetails.openTime &&
68+
!req.body.settingsDetails.closeTime &&
69+
!req.body.settingsDetails.confirmTime) {
70+
return next();
71+
}
6772
const openTime = new Date(req.body.settingsDetails.openTime);
6873
const closeTime = new Date(req.body.settingsDetails.closeTime);
6974
const confirmTime = new Date(req.body.settingsDetails.confirmTime);
@@ -126,9 +131,35 @@ async function confirmAppsOpen(req, res, next) {
126131
}
127132
}
128133

134+
/**
135+
* @function confirmCheckinOpen
136+
* @param {*} req
137+
* @param {*} res
138+
* @param {*} next
139+
* @description Only succeeds if check-in is currently open
140+
*/
141+
async function confirmCheckinOpen(req, res, next) {
142+
const settings = await Services.Settings.getSettings();
143+
if (!settings) {
144+
return next({
145+
status: 500,
146+
message: Constants.Error.GENERIC_500_MESSAGE
147+
});
148+
}
149+
if (settings.checkinOpen) {
150+
return next();
151+
}
152+
153+
return next({
154+
status: 403,
155+
message: Constants.Error.SETTINGS_403_MESSAGE
156+
});
157+
}
158+
129159
module.exports = {
130160
parsePatch: parsePatch,
131161
confirmValidPatch: confirmValidPatch,
162+
confirmCheckinOpen: Middleware.Util.asyncMiddleware(confirmCheckinOpen),
132163
confirmAppsOpen: Middleware.Util.asyncMiddleware(confirmAppsOpen),
133164
updateSettings: Middleware.Util.asyncMiddleware(updateSettings),
134165
getSettings: Middleware.Util.asyncMiddleware(getSettings)
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"use strict";
2+
3+
const { body } = require('express-validator');
4+
const {
5+
PRIZE_CATEGORIES,
6+
SPONSOR_CHALLENGES,
7+
MLH_CHALLENGES
8+
} = require('../../constants/checkin-options');
9+
10+
/**
11+
* Validator for check-in form submission
12+
*/
13+
const checkinValidator = [
14+
body('formData.prizeCategories')
15+
.isArray()
16+
.withMessage('Prize categories must be an array')
17+
.custom((values) =>
18+
Array.isArray(values) &&
19+
values.every((value) => typeof value === 'string' && PRIZE_CATEGORIES.includes(value))
20+
)
21+
.withMessage('Prize categories contain invalid selections'),
22+
body('formData.sponsorChallenges')
23+
.isArray()
24+
.withMessage('Sponsor challenges must be an array')
25+
.custom((values) =>
26+
Array.isArray(values) &&
27+
values.every((value) => typeof value === 'string' && SPONSOR_CHALLENGES.includes(value))
28+
)
29+
.withMessage('Sponsor challenges contain invalid selections'),
30+
body('formData.mlhChallenges')
31+
.isArray()
32+
.withMessage('MLH challenges must be an array')
33+
.custom((values) =>
34+
Array.isArray(values) &&
35+
values.every((value) => typeof value === 'string' && MLH_CHALLENGES.includes(value))
36+
)
37+
.withMessage('MLH challenges contain invalid selections'),
38+
// body('formData.workshopsAttended').isArray().withMessage('Workshops attended must be an array'),
39+
body('formData.discordTag').notEmpty().withMessage('Discord tag is required'),
40+
body('formData.devpostLink')
41+
.notEmpty()
42+
.withMessage('Devpost link is required')
43+
.bail()
44+
.isURL({ require_protocol: true, protocols: ['http', 'https'] })
45+
.withMessage('Devpost link must be a valid URL')
46+
.bail()
47+
.custom((value) => {
48+
try {
49+
const url = new URL(value);
50+
return url.hostname === 'devpost.com' || url.hostname.endsWith('.devpost.com');
51+
} catch (error) {
52+
return false;
53+
}
54+
})
55+
.withMessage('Devpost link must be a devpost.com URL')
56+
];
57+
58+
module.exports = checkinValidator;

middlewares/validators/settings.validator.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ module.exports = {
66
VALIDATOR.dateValidator("body", "openTime", true),
77
VALIDATOR.dateValidator("body", "closeTime", true),
88
VALIDATOR.dateValidator("body", "confirmTime", true),
9-
VALIDATOR.booleanValidator("body", "isRemote", true)
9+
VALIDATOR.booleanValidator("body", "isRemote", true),
10+
VALIDATOR.booleanValidator("body", "checkinOpen", true),
1011
]
1112
};

models/settings.model.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ const settings = new mongoose.Schema({
1717
isRemote: {
1818
type: Boolean,
1919
default: false
20+
},
21+
checkinOpen: {
22+
type: Boolean,
23+
default: false
2024
}
2125
});
2226

0 commit comments

Comments
 (0)