Skip to content

Commit

Permalink
Merge pull request #70 from uwblueprint/maggie/course-units-api
Browse files Browse the repository at this point in the history
Course Units API - Part 2: Modules
  • Loading branch information
mxc-maggiechen authored Oct 29, 2024
2 parents 3662943 + 945be1f commit 2bc3668
Show file tree
Hide file tree
Showing 7 changed files with 376 additions and 0 deletions.
33 changes: 33 additions & 0 deletions backend/middlewares/validators/courseValidators.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Request, Response, NextFunction } from "express";
import { getApiValidationError, validatePrimitive } from "./util";
import CourseUnitService from "../../services/implementations/courseUnitService";

/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
export const createCourseUnitDtoValidator = async (
Expand All @@ -13,6 +14,17 @@ export const createCourseUnitDtoValidator = async (
return next();
};

export const createCourseModuleDtoValidator = async (
req: Request,
res: Response,
next: NextFunction,
) => {
if (!validatePrimitive(req.body.title, "string")) {
return res.status(400).send(getApiValidationError("title", "string"));
}
return next();
};

export const updateCourseUnitDtoValidator = async (
req: Request,
res: Response,
Expand All @@ -23,3 +35,24 @@ export const updateCourseUnitDtoValidator = async (
}
return next();
};

export const moduleBelongsToUnitValidator = async (
req: Request,
res: Response,
next: NextFunction,
) => {
const { unitId, moduleId } = req.params;

const courseUnitService: CourseUnitService = new CourseUnitService();

const courseUnit = await courseUnitService.getCourseUnit(unitId);

if (!courseUnit.modules.includes(moduleId)) {
return res
.status(404)
.send(
`Module with ID ${moduleId} is not found in unit with ID ${unitId}`,
);
}
return next();
};
76 changes: 76 additions & 0 deletions backend/rest/courseRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ import CourseUnitService from "../services/implementations/courseUnitService";
import { getErrorMessage } from "../utilities/errorUtils";
import {
createCourseUnitDtoValidator,
moduleBelongsToUnitValidator,
updateCourseUnitDtoValidator,
} from "../middlewares/validators/courseValidators";
import { isAuthorizedByRole } from "../middlewares/auth";
import CourseModuleService from "../services/implementations/courseModuleService";

const courseRouter: Router = Router();
const courseUnitService: CourseUnitService = new CourseUnitService();
const courseModuleService: CourseModuleService = new CourseModuleService();

courseRouter.get(
"/",
Expand All @@ -23,6 +26,21 @@ courseRouter.get(
},
);

courseRouter.get(
"/:unitId",
isAuthorizedByRole(new Set(["Administrator", "Facilitator", "Learner"])),
async (req, res) => {
try {
const courseModules = await courseModuleService.getCourseModules(
req.params.unitId,
);
res.status(200).json(courseModules);
} catch (e: unknown) {
res.status(500).send(getErrorMessage(e));
}
},
);

courseRouter.post(
"/",
isAuthorizedByRole(new Set(["Administrator"])),
Expand All @@ -39,6 +57,25 @@ courseRouter.post(
},
);

courseRouter.post(
"/:unitId",
isAuthorizedByRole(new Set(["Administrator"])),
createCourseUnitDtoValidator,
async (req, res) => {
try {
const newCourseModule = await courseModuleService.createCourseModule(
req.params.unitId,
{
title: req.body.title,
},
);
res.status(201).json(newCourseModule);
} catch (e: unknown) {
res.status(500).send(getErrorMessage(e));
}
},
);

courseRouter.put(
"/:unitId",
isAuthorizedByRole(new Set(["Administrator"])),
Expand All @@ -56,6 +93,27 @@ courseRouter.put(
},
);

courseRouter.put(
"/:unitId/:moduleId",
isAuthorizedByRole(new Set(["Administrator"])),
updateCourseUnitDtoValidator,
moduleBelongsToUnitValidator,
async (req, res) => {
const { moduleId } = req.params;
try {
const updatedCourseModule = await courseModuleService.updateCourseModule(
moduleId,
{
title: req.body.title,
},
);
res.status(200).json(updatedCourseModule);
} catch (e: unknown) {
res.status(500).send(getErrorMessage(e));
}
},
);

courseRouter.delete(
"/:unitId",
isAuthorizedByRole(new Set(["Administrator"])),
Expand All @@ -72,4 +130,22 @@ courseRouter.delete(
},
);

courseRouter.delete(
"/:unitId/:moduleId",
isAuthorizedByRole(new Set(["Administrator"])),
moduleBelongsToUnitValidator,
async (req, res) => {
const { unitId, moduleId } = req.params;
try {
const deletedCourseUnitId = await courseModuleService.deleteCourseModule(
unitId,
moduleId,
);
res.status(200).json({ id: deletedCourseUnitId });
} catch (e: unknown) {
res.status(500).send(getErrorMessage(e));
}
},
);

export default courseRouter;
177 changes: 177 additions & 0 deletions backend/services/implementations/courseModuleService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
/* eslint-disable class-methods-use-this */
import { startSession } from "mongoose";
import {
CourseModuleDTO,
CreateCourseModuleDTO,
UpdateCourseModuleDTO,
} from "../../types/courseTypes";
import logger from "../../utilities/logger";
import MgCourseUnit, { CourseUnit } from "../../models/courseunit.mgmodel";
import { getErrorMessage } from "../../utilities/errorUtils";
import MgCourseModule, {
CourseModule,
} from "../../models/coursemodule.mgmodel";
import ICourseModuleService from "../interfaces/courseModuleService";

const Logger = logger(__filename);

class CourseModuleService implements ICourseModuleService {
async getCourseModules(
courseUnitId: string,
): Promise<Array<CourseModuleDTO>> {
try {
const courseUnit: CourseUnit | null = await MgCourseUnit.findById(
courseUnitId,
);

if (!courseUnit) {
throw new Error(`Course unit with id ${courseUnitId} not found.`);
}

const courseModules: Array<CourseModule> = await MgCourseModule.find({
_id: { $in: courseUnit.modules },
});

return courseModules;
} catch (error) {
Logger.error(
`Failed to get course modules for course unit with id: ${courseUnitId}. Reason = ${getErrorMessage(
error,
)}`,
);
throw error;
}
}

async createCourseModule(
courseUnitId: string,
courseModuleDTO: CreateCourseModuleDTO,
): Promise<CourseModuleDTO> {
let newCourseModule: CourseModule | undefined;
const session = await startSession(); // start a transaction
session.startTransaction();
try {
const courseUnit: CourseUnit | null = await MgCourseUnit.findById(
courseUnitId,
).session(session);

if (!courseUnit) {
throw new Error(`Course unit with id ${courseUnitId} not found.`);
}
const numCourseModules = courseUnit.modules.length;

newCourseModule = await MgCourseModule.create({
...courseModuleDTO,
displayIndex: numCourseModules + 1,
session,
});

if (!newCourseModule) {
throw new Error(
`Error with creating course module with DTO: ${courseModuleDTO}`,
);
}

await MgCourseUnit.findByIdAndUpdate(courseUnitId, {
$push: { modules: newCourseModule.id }, // newModule is the object/value you want to push into the array
}).session(session);

await session.commitTransaction();
} catch (error) {
Logger.error(
`Failed to create course module under course unit with id: ${courseUnitId}. Reason = ${getErrorMessage(
error,
)}`,
);
throw error;
} finally {
session.endSession();
}

return {
id: newCourseModule.id,
displayIndex: newCourseModule.displayIndex,
title: newCourseModule.title,
} as CourseModuleDTO;
}

async updateCourseModule(
courseModuleId: string,
courseModuleDTO: UpdateCourseModuleDTO,
): Promise<CourseModuleDTO> {
let updatedModule: CourseModule | null;
try {
updatedModule = await MgCourseModule.findByIdAndUpdate(
courseModuleId,
{
title: courseModuleDTO.title,
},
{ new: true, runValidators: true },
);

if (!updatedModule) {
throw new Error(`Course module with id ${courseModuleId} not found.`);
}
} catch (error) {
Logger.error(
`Failed to update course module. Reason = ${getErrorMessage(error)}`,
);
throw error;
}
return {
id: updatedModule.id,
title: updatedModule.title,
displayIndex: updatedModule.displayIndex,
} as CourseModuleDTO;
}

async deleteCourseModule(
courseUnitId: string,
courseModuleId: string,
): Promise<string> {
let deletedCourseModuleId: string;
const session = await startSession();
session.startTransaction();
try {
// first update the course units module reference
const courseUnit = await MgCourseUnit.findByIdAndUpdate(
courseUnitId,
{
$pull: { modules: courseModuleId },
},
{ new: true, runValidators: true },
).session(session);

if (!courseUnit) {
throw new Error(`Course unit with id ${courseUnitId} not found`);
}

// then find ID of course module and delete
const deletedCourseModule: CourseModule | null =
await MgCourseModule.findByIdAndDelete(courseModuleId).session(session);
if (!deletedCourseModule) {
throw new Error(`Course module with id ${courseModuleId} not found`);
}

deletedCourseModuleId = deletedCourseModule.id;
// get the index and update the ones behind it
await MgCourseModule.updateMany(
{ displayIndex: { $gt: deletedCourseModule.displayIndex } },
{ $inc: { displayIndex: -1 } },
).session(session);

await session.commitTransaction();
} catch (error: unknown) {
Logger.error(
`Failed to delete course module. Reason = ${getErrorMessage(error)}`,
);
throw error;
} finally {
await session.endSession();
}

return deletedCourseModuleId;
}
}

export default CourseModuleService;
25 changes: 25 additions & 0 deletions backend/services/implementations/courseUnitService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,31 @@ class CourseUnitService implements ICourseUnitService {
}
}

async getCourseUnit(
unitId: string,
): Promise<CourseUnitDTO & { modules: string[] }> {
try {
const courseUnit: CourseUnit | null = await MgCourseUnit.findById(unitId);

if (!courseUnit) {
throw new Error(`Course unit with id ${unitId} not found.`);
}

const courseModuleIds = courseUnit.modules.map((id) => {
return id.toString();
});

return { ...(courseUnit as CourseUnitDTO), modules: courseModuleIds };
} catch (error) {
Logger.error(
`Failed to get course with id ${unitId}. Reason = ${getErrorMessage(
error,
)}`,
);
throw error;
}
}

async createCourseUnit(
courseUnit: CreateCourseUnitDTO,
): Promise<CourseUnitDTO> {
Expand Down
Loading

0 comments on commit 2bc3668

Please sign in to comment.