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

Course page API (+ other course API tweaks) #89

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
48 changes: 48 additions & 0 deletions backend/middlewares/validators/courseValidators.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Request, Response, NextFunction } from "express";
import { getApiValidationError, validatePrimitive } from "./util";
import CourseUnitService from "../../services/implementations/courseUnitService";
import CourseModuleService from "../../services/implementations/courseModuleService";

/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
export const createCourseUnitDtoValidator = async (
Expand Down Expand Up @@ -36,6 +37,31 @@ export const updateCourseUnitDtoValidator = async (
return next();
};

export const coursePageDtoValidator = async (
req: Request,
res: Response,
next: NextFunction,
) => {
if (!validatePrimitive(req.body.title, "string")) {
return res.status(400).send(getApiValidationError("title", "string"));
}
if (!validatePrimitive(req.body.type, "string")) {
return res.status(400).send(getApiValidationError("type", "string"));
}
if (req.body.type === "Lesson") {
if (!validatePrimitive(req.body.source, "string")) {
return res.status(400).send(getApiValidationError("source", "string"));
}
} else if (req.body.type === "Activity") {
if (!req.body.layout) {
return res.status(400).send("Layout field missing for Activity Page");
}
} else {
return res.status(400).send(`Invalid page type "${req.body.type}"`);
}
return next();
};

export const moduleBelongsToUnitValidator = async (
req: Request,
res: Response,
Expand All @@ -56,3 +82,25 @@ export const moduleBelongsToUnitValidator = async (
}
return next();
};

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

const courseModuleService: CourseModuleService = new CourseModuleService();

const courseModule = await courseModuleService.getCourseModule(moduleId);

if (!courseModule.pages.includes(pageId)) {
return res
.status(404)
.send(
`Page with ID ${pageId} is not found in module with ID ${moduleId}`,
);
}

return next();
};
1 change: 1 addition & 0 deletions backend/models/coursemodule.mgmodel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const CourseModuleSchema: Schema = new Schema({
CourseModuleSchema.set("toObject", {
virtuals: true,
versionKey: false,
flattenObjectIds: true,
transform: (_doc: Document, ret: Record<string, unknown>) => {
// eslint-disable-next-line no-underscore-dangle
delete ret._id;
Expand Down
21 changes: 4 additions & 17 deletions backend/models/coursepage.mgmodel.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
import mongoose, { Schema, Document } from "mongoose";

export type ElementSkeleton = {
id: string;
x: number;
y: number;
w: number;
h: number;
content: string;
};
import { ObjectId } from "mongodb";
import { ElementSkeleton, PageType } from "../types/courseTypes";

const ElementSkeletonSchema: Schema = new Schema({
x: {
Expand All @@ -27,17 +20,15 @@ const ElementSkeletonSchema: Schema = new Schema({
required: true,
},
content: {
type: String,
type: ObjectId,
required: true,
ref: "CourseElement",
},
});

export type PageType = "Lesson" | "Activity";

export interface CoursePage extends Document {
id: string;
title: string;
displayIndex: number;
type: PageType;
}

Expand All @@ -59,10 +50,6 @@ export const CoursePageSchema: Schema = new Schema(
type: String,
required: true,
},
displayIndex: {
type: Number,
required: true,
},
type: {
type: String,
required: true,
Expand Down
1 change: 1 addition & 0 deletions backend/models/courseunit.mgmodel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const CourseUnitSchema: Schema = new Schema({
CourseUnitSchema.set("toObject", {
virtuals: true,
versionKey: false,
flattenObjectIds: true,
transform: (_doc: Document, ret: Record<string, unknown>) => {
// eslint-disable-next-line no-underscore-dangle
delete ret._id;
Expand Down
72 changes: 72 additions & 0 deletions backend/rest/courseRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,20 @@ import { Router } from "express";
import CourseUnitService from "../services/implementations/courseUnitService";
import { getErrorMessage } from "../utilities/errorUtils";
import {
coursePageDtoValidator,
createCourseUnitDtoValidator,
moduleBelongsToUnitValidator,
pageBelongsToModuleValidator,
updateCourseUnitDtoValidator,
} from "../middlewares/validators/courseValidators";
import { isAuthorizedByRole } from "../middlewares/auth";
import CourseModuleService from "../services/implementations/courseModuleService";
import CoursePageService from "../services/implementations/coursePageService";

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

courseRouter.get(
"/",
Expand Down Expand Up @@ -148,4 +152,72 @@ courseRouter.delete(
},
);

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

courseRouter.post(
"/:unitId/:moduleId",
isAuthorizedByRole(new Set(["Administrator"])),
moduleBelongsToUnitValidator,
coursePageDtoValidator,
async (req, res) => {
try {
const newCoursePage = await coursePageService.createCoursePage(
req.params.moduleId,
req.body,
);
res.status(200).json(newCoursePage);
} catch (e: unknown) {
res.status(500).send(getErrorMessage(e));
}
},
);

courseRouter.put(
"/:unitId/:moduleId/:pageId",
isAuthorizedByRole(new Set(["Administrator"])),
moduleBelongsToUnitValidator,
pageBelongsToModuleValidator,
coursePageDtoValidator,
async (req, res) => {
try {
const updatedCoursePage = await coursePageService.updateCoursePage(
req.params.pageId,
req.body,
);
res.status(200).json(updatedCoursePage);
} catch (e: unknown) {
res.status(500).send(getErrorMessage(e));
}
},
);

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

export default courseRouter;
23 changes: 22 additions & 1 deletion backend/services/implementations/courseModuleService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class CourseModuleService implements ICourseModuleService {
_id: { $in: courseUnit.modules },
});

return courseModules;
return courseModules.map((courseModule) => courseModule.toObject());
} catch (error) {
Logger.error(
`Failed to get course modules for course unit with id: ${courseUnitId}. Reason = ${getErrorMessage(
Expand All @@ -43,6 +43,27 @@ class CourseModuleService implements ICourseModuleService {
}
}

async getCourseModule(courseModuleId: string): Promise<CourseModuleDTO> {
try {
const courseModule: CourseModule | null = await MgCourseModule.findById(
courseModuleId,
);

if (!courseModule) {
throw new Error(`id ${courseModuleId} not found.`);
}

return courseModule.toObject();
} catch (error) {
Logger.error(
`Failed to get course module with id: ${courseModuleId}. Reason = ${getErrorMessage(
error,
)}`,
);
throw error;
}
}

async createCourseModule(
courseUnitId: string,
courseModuleDTO: CreateCourseModuleDTO,
Expand Down
Loading
Loading