Skip to content

Commit

Permalink
Merge pull request #127 from Carifio24/stage-states-class-story
Browse files Browse the repository at this point in the history
Make stage stages endpoint more flexible
  • Loading branch information
Carifio24 authored Sep 6, 2024
2 parents 5faf3f1 + 8d0c711 commit 9989062
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 28 deletions.
59 changes: 42 additions & 17 deletions src/database.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Model, Op, QueryTypes, Sequelize } from "sequelize";
import { Model, Op, QueryTypes, Sequelize, WhereOptions } from "sequelize";
import dotenv from "dotenv";

import {
Expand All @@ -19,6 +19,7 @@ import {
createVerificationCode,
encryptPassword,
isNumberArray,
Either,
} from "./utils";


Expand Down Expand Up @@ -328,8 +329,6 @@ export async function checkEducatorLogin(email: string, password: string): Promi
return checkLogin(email, password, findEducatorByEmail);
}



export async function getAllStudents(): Promise<Student[]> {
return Student.findAll();
}
Expand Down Expand Up @@ -390,7 +389,7 @@ export async function updateStoryState(studentID: number, storyName: string, new
return result?.story_state ?? null;
}

export async function getStageState(studentID: number, storyName: string, stageName: string): Promise<JSON | null> {
export async function getStudentStageState(studentID: number, storyName: string, stageName: string): Promise<JSON | null> {
const result = await StageState.findOne({
where: {
student_id: studentID,
Expand All @@ -405,6 +404,45 @@ export async function getStageState(studentID: number, storyName: string, stageN
return result?.state ?? null;
}

export type StageStateQuery = { storyName: string, stageName?: string } & Either<{studentID: number}, {classID: number}>;

export async function getStageStates(query: StageStateQuery): Promise<Record<string, JSON[]>> {
const where: WhereOptions = { story_name: query.storyName };
if (query.stageName != undefined) {
where.stage_name = query.stageName;
}

if (query.classID != undefined) {
const students = await StudentsClasses.findAll({
where: { class_id: query.classID }
});
const studentIDs = students.map(sc => sc.student_id);
where.student_id = {
[Op.in]: studentIDs
};
} else {
where.student_id = query.studentID;
}

const results = await StageState.findAll({ where })
.catch(error => {
console.log(error);
return null;
});

const stageStates: Record<string, JSON[]> = {};
if (results !== null) {
results.forEach(result => {
const states = stageStates[result.stage_name] ?? [];
states.push(result.state);
stageStates[result.stage_name] = states;
});
}

return stageStates;

}

export async function updateStageState(studentID: number, storyName: string, stageName: string, newState: JSON): Promise<JSON | null> {
const query = {
student_id: studentID,
Expand Down Expand Up @@ -441,19 +479,6 @@ export async function deleteStageState(studentID: number, storyName: string, sta
});
}

export async function getStageStates(studentID: number, storyName: string): Promise<Record<string, JSON>> {
const stages = await getStages(storyName);
const stageNames = stages.map(stage => stage.stage_name);
const stageStates: Record<string, JSON> = {};
for (const name of stageNames) {
const state = await getStageState(studentID, storyName, name);
if (state !== null) {
stageStates[name] = state;
}
}
return stageStates;
}

export async function getClassesForEducator(educatorID: number): Promise<Class[]> {
return Class.findAll({
where: {
Expand Down
53 changes: 42 additions & 11 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,14 @@ import {
currentVersionForQuestion,
getQuestionsForStory,
getDashboardGroupClasses,
getStageState,
getStudentStageState,
updateStageState,
deleteStageState,
findClassById,
getStages,
getStory,
getStageStates,
StageStateQuery,
} from "./database";

import { getAPIKey, hasPermission } from "./authorization";
Expand Down Expand Up @@ -482,7 +483,13 @@ app.get("/stages/:storyName", async (req, res) => {
});
});

app.get("/stage-states/:studentID/:storyName", async (req, res) => {
// Use query parameters `student_id`, `class_id`, and `stage_name` to filter output
// `stage_name` is optional. If not specified, return value will be an object of the form
// { stage1: [<states>], stage2: [<states>], ... }
// If specified, this returns an object of the form [<states>]
// At least one of `student_id` and `class_id` must be specified.
// If both are specified, only `student_id` is used
app.get("/stage-states/:storyName", async (req, res) => {
const storyName = req.params.storyName;
const story = await getStory(storyName);

Expand All @@ -493,25 +500,49 @@ app.get("/stage-states/:studentID/:storyName", async (req, res) => {
return;
}

const studentID = Number(req.params.studentID);
const student = await findStudentById(studentID);
if (student === null) {
res.status(404).json({
error: `No student found with ID ${studentID}`
let query: StageStateQuery;
const studentID = Number(req.query.student_id);
const classID = Number(req.query.class_id);
if (!isNaN(studentID)) {
const student = await findStudentById(studentID);
if (student === null) {
res.status(404).json({
error: `No student found with ID ${studentID}`
});
return;
}
query = { storyName, studentID };
} else if (!isNaN(classID)) {
const cls = await findClassById(classID);
if (cls === null) {
res.status(404).json({
error: `No class found with ID ${classID}`
});
return;
}
query = { storyName, classID };
} else {
res.status(400).json({
error: "Must specify either a student or a class ID"
});
return;
}

const stageStates = await getStageStates(studentID, storyName);
res.json(stageStates);
const stageName = req.query.stage_name as string;
if (stageName != undefined) {
query.stageName = stageName;
}
const stageStates = await getStageStates(query);
const results = (stageName != undefined) ? stageStates[stageName] : stageStates;
res.json(results);
});

app.get("/stage-state/:studentID/:storyName/:stageName", async (req, res) => {
const params = req.params;
const studentID = Number(params.studentID);
const storyName = params.storyName;
const stageName = params.stageName;
const state = await getStageState(studentID, storyName, stageName);
const state = await getStudentStageState(studentID, storyName, stageName);
const status = state !== null ? 200 : 404;
res.status(status).json({
student_id: studentID,
Expand Down Expand Up @@ -542,7 +573,7 @@ app.delete("/stage-state/:studentID/:storyName/:stageName", async (req, res) =>
const studentID = Number(params.studentID);
const storyName = params.storyName;
const stageName = params.stageName;
const state = await getStageState(studentID, storyName, stageName);
const state = await getStudentStageState(studentID, storyName, stageName);
if (state != null) {
res.status(200);
const count = await deleteStageState(studentID, storyName, stageName);
Expand Down
8 changes: 8 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ import { Model } from "sequelize";
// This type describes objects that we're allowed to pass to a model's `update` method
export type UpdateAttributes<M extends Model> = Parameters<M["update"]>[0];

export type Only<T, U> = {
[P in keyof T]: T[P];
} & {
[P in keyof U]?: never;
};

export type Either<T, U> = Only<T,U> | Only<U,T>;

export function createVerificationCode(): string {
return nanoid(21);
}
Expand Down

0 comments on commit 9989062

Please sign in to comment.