Skip to content

Commit

Permalink
Squashed commit of the following:
Browse files Browse the repository at this point in the history
commit 0b133b8
Author: Michael Sullivan <[email protected]>
Date:   Tue May 14 09:09:16 2024 -0700

    Feature/mraysu/program form v2 (#100)

    * Update Backend Program Schema

    * V2 UI

    * Disabled Editing Program Type

    * Frontend-backend integration

    * Lint fixes

    ---------

    Co-authored-by: mraysu <[email protected]>
    Co-authored-by: Adhithya Ananthan <[email protected]>

commit e17b509
Author: parth4apple <[email protected]>
Date:   Tue May 14 09:01:15 2024 -0700

    Student and Enrollment Schema modifications (#101)

    * feat: initial schema

    * feat: edit routes

    * feat: test and fix routes
  • Loading branch information
aaronchan32 committed May 20, 2024
1 parent 1498994 commit e078081
Show file tree
Hide file tree
Showing 19 changed files with 424 additions and 403 deletions.
5 changes: 0 additions & 5 deletions backend/src/controllers/program.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/* eslint-disable @typescript-eslint/no-misused-promises */
import { RequestHandler } from "express";
import { validationResult } from "express-validator";
import { Schema } from "mongoose";
//import { error } from "firebase-functions/logger";

import ProgramModel from "../models/program";
Expand All @@ -13,11 +12,7 @@ export type Program = {
abbreviation: string;
type: string;
daysOfWeek: string[];
startDate: Date;
endDate: Date;
color: string; //colorValueHex;
studentUIDs: Schema.Types.ObjectId[];
renewalDate: Date;
hourlyPay: string;
sessions: [string[]];
};
Expand Down
115 changes: 33 additions & 82 deletions backend/src/controllers/student.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,64 +5,31 @@

import { RequestHandler } from "express";
import { validationResult } from "express-validator";
import mongoose, { HydratedDocument } from "mongoose";

import EnrollmentModel from "../models/enrollment";
import StudentModel from "../models/student";
import { programLink } from "../types/programLink";
import { addStudentToPrograms, removeStudentFromPrograms } from "../util/student";
import { Enrollment } from "../types/enrollment";
import { createEnrollment, editEnrollment } from "../util/enrollment";
import validationErrorParser from "../util/validationErrorParser";

export type contact = {
lastName: string;
firstName: string;
email: string;
phoneNumber: string;
};

export type typedModel = {
student: contact;
emergency: contact;
serviceCoordinator: contact;
location: string;
medication: string;
birthday: string;
intakeDate: string;
tourDate: string;
programs: programLink[];
dietary: string[];
otherString: string;
};

type Contact = {
lastName: string;
firstName: string;
email: string;
phoneNumber: string;
};

type Student = {
_id: string;
student: Contact;
emergency: Contact;
serviceCoordinator: Contact;
location: string;
medication?: string;
birthday: Date;
intakeDate: Date;
tourDate: Date;
programs: programLink[];
dietary: string[];
otherString?: string;
};
type Student = HydratedDocument<typeof StudentModel>;
type StudentRequest = Student & { enrollments: Enrollment[] };

export const createStudent: RequestHandler = async (req, res, next) => {
try {
const errors = validationResult(req);

validationErrorParser(errors);

const newStudent = await StudentModel.create(req.body as typedModel);
const programIds = newStudent.programs.map((programObj: programLink) => programObj.programId);
await addStudentToPrograms(newStudent._id, programIds);
const { enrollments, ...studentData } = req.body as StudentRequest;
const newStudent = await StudentModel.create(studentData);
// create enrollments for the student
await Promise.all(
enrollments.map(async (program: Enrollment) => {
await createEnrollment({ ...program, studentId: newStudent._id });
}),
);

res.status(201).json(newStudent);
} catch (error) {
Expand All @@ -77,34 +44,29 @@ export const editStudent: RequestHandler = async (req, res, next) => {
validationErrorParser(errors);

const studentId = req.params.id;
const studentData = req.body as Student;
const { enrollments, ...studentData } = req.body as StudentRequest;

if (studentId !== studentData._id) {
if (studentId !== studentData._id.toString()) {
return res.status(400).json({ message: "Invalid student ID" });
}

const prevStudent = await StudentModel.findById(studentId);
const editedStudent = await StudentModel.findOneAndUpdate({ _id: studentId }, studentData, {
const updatedStudent = await StudentModel.findByIdAndUpdate(studentId, studentData, {
new: true,
});

if (!prevStudent || !editedStudent) {
return res.status(404).json({ message: "No object in database with provided ID" });
if (!updatedStudent) {
return res.status(404).json({ message: "Student not found" });
}

// remove student from possibly stale programs
const prevProgramIds = prevStudent.programs.map(
(programObj: programLink) => programObj.programId,
);
await removeStudentFromPrograms(prevStudent._id, prevProgramIds);

// add student to new programs
const newProgramIds = editedStudent.programs.map(
(programObj: programLink) => programObj.programId,
// update enrollments for the student
await Promise.all(
enrollments.map(async (enrollment: Enrollment) => {
const enrollmentExists = await EnrollmentModel.findById(enrollment._id);
const enrollmentBody = { ...enrollment, studentId: new mongoose.Types.ObjectId(studentId) };
if (!enrollmentExists) await createEnrollment(enrollmentBody);
else await editEnrollment(enrollmentBody);
}),
);
await addStudentToPrograms(editedStudent._id, newProgramIds);

res.status(200).json(editedStudent);
res.status(200).json({ ...updatedStudent, enrollments });
} catch (error) {
next(error);
}
Expand All @@ -114,26 +76,15 @@ export const getAllStudents: RequestHandler = async (_, res, next) => {
try {
const students = await StudentModel.find();

res.status(200).json(students);
} catch (error) {
next(error);
}
};

export const deleteAllStudents: RequestHandler = async (_, res, next) => {
try {
// remove students from all programs
const students = await StudentModel.find();
await Promise.all(
// gather all enrollments for each student and put them in student.programs
const hydratedStudents = await Promise.all(
students.map(async (student) => {
const programIds = student.programs.map((programObj: programLink) => programObj.programId);
await removeStudentFromPrograms(student._id, programIds);
const enrollments = await EnrollmentModel.find({ studentId: student._id });
return { ...student.toObject(), programs: enrollments };
}),
);

await StudentModel.deleteMany();

res.status(204).end();
res.status(200).json(hydratedStudents);
} catch (error) {
next(error);
}
Expand Down
28 changes: 28 additions & 0 deletions backend/src/models/enrollment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import mongoose, { InferSchemaType } from "mongoose";

const enrollmentSchema = new mongoose.Schema({
studentId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Student",
required: true,
unique: false,
},
programId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Program",
required: true,
unique: false,
},
status: { type: String, required: true },
dateUpdated: { type: Date, required: true, default: Date.now() },
hoursLeft: { type: Number, required: true },
schedule: { type: [String], required: true },
sessionTime: { type: [String], required: true },
startDate: { type: Date, required: true },
renewalDate: { type: Date, required: true },
authNumber: { type: String, required: true },
});

type Enrollment = InferSchemaType<typeof enrollmentSchema>;

export default mongoose.model<Enrollment>("Enrollment", enrollmentSchema);
8 changes: 2 additions & 6 deletions backend/src/models/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,11 @@ import { InferSchemaType, Schema, model } from "mongoose";

const programSchema = new Schema({
name: { type: String, required: true },
abbreviation: { type: String, required: true }, // e.g. ENTR
abbreviation: { type: String, required: true }, // e.g. ENTR, should be unique
type: { type: String, required: true }, // regular vs. varying
daysOfWeek: { type: [String], required: true }, // M, T, W, TH, F
startDate: { type: Date, required: true },
endDate: { type: Date, required: true },
color: { type: String, required: true },
students: { type: [Schema.Types.ObjectId], ref: "Students", required: false },
renewalDate: { type: Date, required: true },
hourly: { type: Number, required: true },
hourlyPay: { type: Number, required: true },
sessions: { type: [[String]], required: true },
});

Expand Down
18 changes: 5 additions & 13 deletions backend/src/models/student.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,11 @@ const studentSchema = new Schema({
intakeDate: { type: Date, required: true },
tourDate: { type: Date, required: true },

programs: {
type: [
{
programId: { type: Schema.Types.ObjectId, ref: "Program", required: true },
status: { type: String, required: true },
dateUpdated: { type: Date, required: true, default: Date.now() },
hoursLeft: { type: Number, required: true },
},
],
required: true,
},
conservation: { type: Boolean, required: true },
UCINumber: { type: String, required: true },
incidentForm: { type: String, required: true },
documents: { type: [String], required: true },
profilePicture: { type: Schema.Types.ObjectId, ref: "Image", required: false },

progressNotes: {
type: [Schema.Types.ObjectId],
Expand All @@ -56,8 +50,6 @@ const studentSchema = new Schema({

//Will contain list of all dietary restrictions
dietary: { type: [String] },

otherString: { type: String, default: "" },
});

type Student = InferSchemaType<typeof studentSchema>;
Expand Down
13 changes: 13 additions & 0 deletions backend/src/types/enrollment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

import mongoose, { Schema } from "mongoose";

import EnrollmentModel from "../models/enrollment";

// get the enrollment type from the enrollment model
export type Enrollment = Extract<
typeof EnrollmentModel,
mongoose.Model<any, any, any>
> extends mongoose.Model<infer U>
? U & { _id: Schema.Types.ObjectId }
: never;
8 changes: 0 additions & 8 deletions backend/src/types/programLink.ts

This file was deleted.

21 changes: 21 additions & 0 deletions backend/src/util/enrollment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import EnrollmentModel from "../models/enrollment";
import { Enrollment } from "../types/enrollment";

export const createEnrollment = async (req: Enrollment) => {
try {
await EnrollmentModel.create(req);
} catch (e) {
console.log(e);
throw e;
}
};

export const editEnrollment = async (req: Enrollment) => {
try {
console.log(req);
await EnrollmentModel.findByIdAndUpdate(req._id, req);
} catch (e) {
console.log(e);
throw e;
}
};
67 changes: 31 additions & 36 deletions backend/src/util/student.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,53 @@
import mongoose from "mongoose";

import ProgramModel from "../models/program";
import { programLink } from "../types/programLink";

type ObjectId = mongoose.Types.ObjectId;

export const programValidatorUtil = async (programs: programLink[]) => {
import { Enrollment } from "../types/enrollment";

export const programValidatorUtil = async (enrollments: Enrollment[]) => {
// verify all fields are present
const requiredFields = [
"programId",
"status",
"hoursLeft",
"schedule",
"sessionTime",
"startDate",
"renewalDate",
"authNumber",
];
enrollments.forEach((enrollment: Enrollment) => {
requiredFields.forEach((field) => {
if (!enrollment[field as keyof Enrollment])
throw new Error(`Field ${field} is required on enrollment`);
});
});

// verify statuses are correct and student is not in more than 2 programs
const allowedStatuses = ["Joined", "Waitlisted", "Archived", "Not a fit"];
const programIds = new Set();
let active = 0;
let varying = 0;
await Promise.all(
programs.map(async (program) => {
programIds.add(program.programId);
if (!mongoose.Types.ObjectId.isValid(program.programId))
enrollments.map(async (enrollment) => {
programIds.add(enrollment.programId);
if (!mongoose.Types.ObjectId.isValid(enrollment.programId))
throw new Error("Program ID format is invalid");

if (!allowedStatuses.includes(program.status))
if (!allowedStatuses.includes(enrollment.status))
throw new Error("Status must be one of: " + allowedStatuses.join(", "));

const programType = (await ProgramModel.findById(program.programId))?.type;
if (program.status === "Joined") {
const programType = (await ProgramModel.findById(enrollment.programId))?.type;
if (enrollment.status === "Joined") {
active++;
if (programType === "varying") varying++;
}
}),
);
if (programIds.size !== programs.length) throw new Error("Programs must be unique");

// handle error reporting
if (programIds.size !== enrollments.length) throw new Error("Programs must be unique");
if (active > 2) throw new Error("Student can only be active in 2 programs");
if (varying > 1) throw new Error("Student can only be in 1 varying program");

return true;
};

export const addStudentToPrograms = async (studentId: ObjectId, programIds: ObjectId[]) => {
await Promise.all(
programIds.map(async (programId) => {
await ProgramModel.findByIdAndUpdate(
programId,
{ $push: { students: studentId } },
{ new: true },
);
}),
);
};

export const removeStudentFromPrograms = async (studentId: ObjectId, programIds: ObjectId[]) => {
await Promise.all(
programIds.map(async (programId) => {
await ProgramModel.findByIdAndUpdate(
programId,
{ $pull: { students: studentId } },
{ new: true },
);
}),
);
};
Loading

0 comments on commit e078081

Please sign in to comment.