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

Make stage stages endpoint more flexible #127

Merged
merged 2 commits into from
Sep 6, 2024
Merged
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
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
Loading