Skip to content

Prototype v2: Custom activity authoring #93

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

Draft
wants to merge 16 commits 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
45 changes: 45 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,28 @@ export const updateCourseUnitDtoValidator = async (
return next();
};

export const coursePageDtoValidator = async (
req: Request,
res: Response,
next: NextFunction,
) => {
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.elements) {
return res.status(400).send("Elements 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 +79,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();
};
14 changes: 7 additions & 7 deletions backend/models/courseelement.mgmodel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,19 @@ const CourseElementSchema: Schema = new Schema({
"MultipleChoice",
"Matching",
"Table",
"RichText",
"Text",
"Image",
],
},
});

export interface RichTextElement extends CourseElement {
type: DisplayElementType.RichText;
export interface TextElement extends CourseElement {
type: DisplayElementType.Text;
content: string;
backgroundColor?: string;
}

const RichTextElementSchema: Schema = new Schema({
const TextElementSchema: Schema = new Schema({
content: {
type: String,
required: true,
Expand Down Expand Up @@ -173,9 +173,9 @@ const CourseElementModel = mongoose.model<CourseElement>(
CourseElementSchema,
);

export const RichTextElementModel = CourseElementModel.discriminator(
"RichTextElement",
RichTextElementSchema,
export const TextElementModel = CourseElementModel.discriminator(
"TextElement",
TextElementSchema,
);

export const ImageElementModel = CourseElementModel.discriminator(
Expand Down
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
79 changes: 35 additions & 44 deletions backend/models/coursepage.mgmodel.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,42 @@
import mongoose, { Schema, Document } from "mongoose";
import { Element, PageType } from "../types/courseTypes";
import { validateElementData } from "../utilities/courseUtils";

export type ElementSkeleton = {
id: string;
x: number;
y: number;
w: number;
h: number;
content: string;
};

const ElementSkeletonSchema: Schema = new Schema({
x: {
type: Number,
required: true,
},
y: {
type: Number,
required: true,
},
w: {
type: Number,
required: true,
},
h: {
type: Number,
required: true,
},
content: {
type: String,
required: true,
const ElementSchema: Schema = new Schema(
{
x: {
type: Number,
required: true,
},
y: {
type: Number,
required: true,
},
w: {
type: Number,
required: true,
},
h: {
type: Number,
required: true,
},
data: {
type: Schema.Types.Mixed,
required: true,
},
},
});
{ _id: false },
);

export type PageType = "Lesson" | "Activity";
ElementSchema.path("data").validate((value) => {
if (!value || !value.type) {
return false;
}
return validateElementData(value);
});

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

Expand All @@ -46,7 +45,7 @@ export interface LessonPage extends CoursePage {
}

export interface ActivityPage extends CoursePage {
layout: [ElementSkeleton];
elements: Element[];
}

const baseOptions = {
Expand All @@ -55,14 +54,6 @@ const baseOptions = {

export const CoursePageSchema: Schema = new Schema(
{
title: {
type: String,
required: true,
},
displayIndex: {
type: Number,
required: true,
},
type: {
type: String,
required: true,
Expand All @@ -80,8 +71,8 @@ const LessonPageSchema: Schema = new Schema({
});

const ActivityPageSchema: Schema = new Schema({
layout: {
type: [ElementSkeletonSchema],
elements: {
type: [ElementSchema],
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