Skip to content

Commit

Permalink
Add endpoints for getting merged classes and adding a class to a merg…
Browse files Browse the repository at this point in the history
…e group.
  • Loading branch information
Carifio24 committed Nov 4, 2024
1 parent eec2bb4 commit c2c7e2a
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 96 deletions.
70 changes: 7 additions & 63 deletions src/stories/hubbles_law/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ async function getClassIDsForSyncClass(classID: number): Promise<number[]> {
return classIDs;
}

async function getMergedIDsForClass(classID: number): Promise<number[]> {
export async function getMergedIDsForClass(classID: number): Promise<number[]> {
// TODO: Currently this uses two queries:
// The first to get the merge group (if there is one)
// Then a second to get all of the classes in the merge group
Expand Down Expand Up @@ -758,35 +758,12 @@ export async function getMergeDataForClass(classID: number): Promise<SyncMergedH
return SyncMergedHubbleClasses.findOne({ where: { class_id: classID } });
}

export async function eligibleClassesForMerge(database: Sequelize, classID: number, sizeThreshold=20): Promise<Class[]> {
const size = await classSize(classID);

// Running into the limits of the ORM a bit here
// Maybe there's a clever way to write this?
// But straight SQL gets the job done
return database.query(
`SELECT * FROM (SELECT
id,
test,
(SELECT
COUNT(*)
FROM
StudentsClasses
WHERE
StudentsClasses.class_id = id) AS size
FROM
Classes) q
WHERE
(size >= ${sizeThreshold - size} AND test = 0)
`, { type: QueryTypes.SELECT }) as Promise<Class[]>;
}

type GroupData = {
unique_gid: string;
is_group: boolean;
merged_count: number;
};
export async function findClassForMerge(database: Sequelize): Promise<Class & GroupData> {
export async function findClassForMerge(database: Sequelize, classID: number): Promise<Class & GroupData> {
// The SQL is complicated enough here; doing this with the ORM
// will probably be unreadable
const result = await database.query(
Expand All @@ -807,6 +784,7 @@ export async function findClassForMerge(database: Sequelize): Promise<Class & Gr
HAVING COUNT(student_id) >= 12
) C
ON Classes.id = C.class_id
WHERE id != ${classID}
GROUP BY unique_gid
ORDER BY is_group ASC, group_count ASC, merged_count DESC
LIMIT 1;
Expand All @@ -822,7 +800,7 @@ async function nextMergeGroupID(): Promise<number> {
[Sequelize.fn("MAX", Sequelize.col("group_id")), "group_id"]
]
})) as (HubbleClassMergeGroup & { group_id: number })[];
return max[0].group_id as number;
return (max[0].group_id + 1) as number;
}

export async function addClassToMergeGroup(classID: number): Promise<number | null> {
Expand All @@ -838,50 +816,16 @@ export async function addClassToMergeGroup(classID: number): Promise<number | nu
return null;
}

const clsToMerge = await findClassForMerge(database);
const clsToMerge = await findClassForMerge(database, classID);
let mergeGroup;
if (clsToMerge.is_group) {
mergeGroup = await HubbleClassMergeGroup.create({ class_id: classID, group_id: Number(clsToMerge.unique_gid), merge_order: clsToMerge.merged_count + 1 });
} else {
const newGroupID = await nextMergeGroupID();
mergeGroup = await HubbleClassMergeGroup.create({ class_id: classID, group_id: newGroupID, merge_order: 1 });
await HubbleClassMergeGroup.create({ class_id: clsToMerge.id, group_id: newGroupID, merge_order: 1 });
mergeGroup = await HubbleClassMergeGroup.create({ class_id: classID, group_id: newGroupID, merge_order: 2 });
}

return mergeGroup.group_id;

}

// Try and merge the class with the given ID with another class such that the total size is above the threshold
// We say "try" because if a client doesn't know that the merge has already occurred, we may get
// multiple such requests from different student clients.
// If a merge has already been created, we don't make another one - we just return the existing one, with a
// message that indicates that this was the case.
export interface MergeAttemptData {
mergeData: SyncMergedHubbleClasses | null;
message: string;
}
export async function tryToMergeClass(db: Sequelize, classID: number): Promise<MergeAttemptData> {
const cls = await findClassById(classID);
if (cls === null) {
return { mergeData: null, message: "Invalid class ID!" };
}

let mergeData = await getMergeDataForClass(classID);
if (mergeData !== null) {
return { mergeData, message: "Class already merged" };
}

const eligibleClasses = await eligibleClassesForMerge(db, classID);
if (eligibleClasses.length > 0) {
const index = Math.floor(Math.random() * eligibleClasses.length);
const classToMerge = eligibleClasses[index];
mergeData = await SyncMergedHubbleClasses.create({ class_id: classID, merged_class_id: classToMerge.id });
if (mergeData === null) {
return { mergeData, message: "Error creating merge!" };
}
return { mergeData, message: "New merge created" };
}

return { mergeData: null, message: "No eligible classes to merge" };

}
91 changes: 58 additions & 33 deletions src/stories/hubbles_law/router.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import * as S from "@effect/schema/Schema";
import * as Either from "effect/Either";

import { Galaxy } from "./models/galaxy";

import {
Expand Down Expand Up @@ -32,9 +35,10 @@ import {
getGalaxyById,
removeSampleHubbleMeasurement,
getAllNthSampleHubbleMeasurements,
tryToMergeClass,
getClassMeasurementCount,
getStudentsWithCompleteMeasurementsCount
getStudentsWithCompleteMeasurementsCount,
getMergedIDsForClass,
addClassToMergeGroup
} from "./database";

import {
Expand All @@ -45,7 +49,7 @@ import {
import { Express, Router } from "express";
import { Sequelize } from "sequelize";
import { classForStudentStory, findClassById, findStudentById } from "../../database";
import { SyncMergedHubbleClasses, initializeModels } from "./models";
import { initializeModels } from "./models";
import { setUpHubbleAssociations } from "./associations";

export const router = Router();
Expand Down Expand Up @@ -395,51 +399,72 @@ router.get(["/class-measurements/:studentID", "stage-3-measurements/:studentID"]
});
});

router.get("/all-data", async (req, res) => {
const minimal = (req.query?.minimal as string)?.toLowerCase() === "true";
const beforeMs: number = parseInt(req.query.before as string);
const before = isNaN(beforeMs) ? null : new Date(beforeMs);
const [measurements, studentData, classData] =
await Promise.all([
getAllHubbleMeasurements(before, minimal),
getAllHubbleStudentData(before, minimal),
getAllHubbleClassData(before, minimal)
]);
router.get("/merged-classes/:classID", async (req, res) => {
const classID = Number(req.params.classID);
const cls = await findClassById(classID);
if (cls === null) {
res.status(404).json({
message: `No class found with ID ${classID}`,
});
return;
}
const classIDs = await getMergedIDsForClass(classID);
res.json({
measurements,
studentData,
classData
merged_class_ids: classIDs,
});
});

router.put("/sync-merged-class/:classID", async(req, res) => {
const classID = parseInt(req.params.classID);
if (isNaN(classID)) {
res.statusCode = 400;
res.json({
error: "Class ID must be a number"
const MergeClassInfo = S.struct({
class_id: S.number.pipe(S.int()),
});
router.put("/merge-class", async (req, res) => {
const body = req.body;
const maybe = S.decodeUnknownEither(MergeClassInfo)(body);

if (Either.isLeft(maybe)) {
res.status(400).json({
message: `Expected class ID to be an integer, got ${body.class_id}`,
});
return;
}
const database = SyncMergedHubbleClasses.sequelize;
if (database === undefined) {
res.status(500).json({
error: "Error connecting to database",

const data = maybe.right;
const cls = await findClassById(data.class_id);
if (cls === null) {
res.status(404).json({
message: `No class found with ID ${data.class_id}`,
});
return;
}
const data = await tryToMergeClass(database, classID);
if (data.mergeData === null) {
res.statusCode = 404;
res.json({
error: data.message

const groupID = await addClassToMergeGroup(data.class_id);
if (groupID === null) {
res.status(500).json({
message: `There was an error while adding class ${data.class_id} to a merge group`,
});
return;
}

res.json({
merge_info: data.mergeData,
message: data.message
class_id: data.class_id,
group_id: groupID,
});
});

router.get("/all-data", async (req, res) => {
const minimal = (req.query?.minimal as string)?.toLowerCase() === "true";
const beforeMs: number = parseInt(req.query.before as string);
const before = isNaN(beforeMs) ? null : new Date(beforeMs);
const [measurements, studentData, classData] =
await Promise.all([
getAllHubbleMeasurements(before, minimal),
getAllHubbleStudentData(before, minimal),
getAllHubbleClassData(before, minimal)
]);
res.json({
measurements,
studentData,
classData
});
});

Expand Down

0 comments on commit c2c7e2a

Please sign in to comment.