Skip to content
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
22 changes: 21 additions & 1 deletion src/associations.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
APIKeyRole,
APIKeyRole,
ActiveClass,
Class,
ClassStories,
IgnoreClass,
Expand All @@ -16,6 +17,25 @@ import { APIKey } from "./models/api_key";

export function setUpAssociations() {

Student.belongsToMany(ActiveClass, {
through: StudentsClasses,
sourceKey: "id",
targetKey: "id",
foreignKey: "student_id",
otherKey: "class_id",
onUpdate: "CASCADE",
onDelete: "CASCADE"
});
ActiveClass.belongsToMany(Student, {
through: StudentsClasses,
sourceKey: "id",
targetKey: "id",
foreignKey: "class_id",
otherKey: "student_id",
onUpdate: "CASCADE",
onDelete: "CASCADE"
});

Student.belongsToMany(Class, {
through: StudentsClasses,
sourceKey: "id",
Expand Down
38 changes: 26 additions & 12 deletions src/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { createNamespace } from "cls-hooked";
import * as S from "@effect/schema/Schema";

import {
ActiveClass,
Class,
Educator,
ClassStories,
Expand Down Expand Up @@ -650,16 +651,18 @@ export async function deleteStageState(studentID: number, storyName: string, sta
});
}

export async function getClassesForEducator(educatorID: number): Promise<Class[]> {
return Class.findAll({
export async function getClassesForEducator(educatorID: number, activeOnly=true): Promise<Class[]> {
const model = activeOnly ? ActiveClass : Class;
return model.findAll({
where: {
educator_id: educatorID
}
});
}

export async function getClassesForStudent(studentID: number): Promise<Class[]> {
return Class.findAll({
export async function getClassesForStudent(studentID: number, activeOnly=true): Promise<Class[]> {
const model = activeOnly ? ActiveClass : Class;
return model.findAll({
include: [{
model: Student,
where: {
Expand All @@ -680,30 +683,41 @@ export async function getStudentsForClass(classID: number): Promise<Student[]> {
});
}

export async function deactivateClass(cls: Class): Promise<Class> {
return cls.update({ active: false });
}

export async function deactiveClassByID(id: number): Promise<boolean> {
return Class.update({ active: false }, { where: { id } })
.then(result => result[0] > 0);
}

export async function deleteClass(id: number): Promise<number> {
return Class.destroy({
where: { id }
});
}

export async function findClassByCode(code: string): Promise<Class | null> {
return Class.findOne({
export async function findClassByCode(code: string, activeOnly=false): Promise<Class | null> {
const model = activeOnly ? ActiveClass : Class;
return model.findOne({
where: { code }
});
}

export async function findClassById(id: number): Promise<Class | null> {
return Class.findOne({
export async function findClassById(id: number, activeOnly=false): Promise<Class | null> {
const model = activeOnly ? ActiveClass : Class;
return model.findOne({
where: { id }
});
}

export async function findClassByIdOrCode(identifier: string): Promise<Class | null> {
export async function findClassByIdOrCode(identifier: string, activeOnly=false): Promise<Class | null> {
const id = Number(identifier);
if (isNaN(id)) {
return findClassByCode(identifier);
return findClassByCode(identifier, activeOnly);
} else {
return findClassById(id);
return findClassById(id, activeOnly);
}
}

Expand Down Expand Up @@ -915,7 +929,7 @@ export async function isClassStoryActive(classID: number, storyName: string): Pr

export async function setClassStoryActive(classID: number, storyName: string, active: boolean): Promise<boolean> {
const result = await ClassStories.update(
{ active },
{ active },
{
where: {
class_id: classID,
Expand Down
10 changes: 10 additions & 0 deletions src/models/active_class.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Class, CLASS_ATTRIBUTES } from "./class";
import { Sequelize } from "sequelize";

// NB: This model is actually a view defined on the Classes table
// where `active = 1`
export class ActiveClass extends Class {}

export function initializeActiveClassModel(sequelize: Sequelize) {
ActiveClass.init(CLASS_ATTRIBUTES, { sequelize });
}
146 changes: 74 additions & 72 deletions src/models/class.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Educator } from "./educator";
import { Sequelize, DataTypes, Model, InferAttributes, InferCreationAttributes, CreationOptional } from "sequelize";

const CLASS_STATUS_VALUES = ["seed", "real_good_data", "real_bad_data", "test_good_data", "test_bad_data"] as const;
export const CLASS_STATUS_VALUES = ["seed", "real_good_data", "real_bad_data", "test_good_data", "test_bad_data"] as const;
type ClassStatus = typeof CLASS_STATUS_VALUES[number];

export class Class extends Model<InferAttributes<Class>, InferCreationAttributes<Class>> {
Expand All @@ -16,81 +16,83 @@ export class Class extends Model<InferAttributes<Class>, InferCreationAttributes
declare test: CreationOptional<boolean>;
declare seed: CreationOptional<boolean>;
declare expected_size: CreationOptional<number>;
declare small_class: CreationOptional<boolean>;
declare readonly small_class: CreationOptional<boolean>;
declare status: CreationOptional<ClassStatus>;
}

export const CLASS_ATTRIBUTES = {
id: {
type: DataTypes.INTEGER.UNSIGNED,
allowNull: false,
autoIncrement: true,
primaryKey: true
},
name: {
type: DataTypes.STRING,
allowNull: false
},
educator_id: {
type: DataTypes.INTEGER.UNSIGNED,
allowNull: false,
references: {
model: Educator,
key: "id"
}
},
created: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: Sequelize.literal("CURRENT_TIMESTAMP")
},
updated: {
type: DataTypes.DATE,
allowNull: true,
defaultValue: null
},
active: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: true,
},
code: {
type: DataTypes.STRING,
allowNull: false
},
asynchronous: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
},
test: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: 0
},
seed: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: 0,
},
expected_size: {
type: DataTypes.INTEGER.UNSIGNED,
allowNull: false,
},
small_class: {
type: DataTypes.VIRTUAL,
// type: "tinyint(1) GENERATED ALWAYS AS (expected_size < 15) VIRTUAL",
get(this: Class): boolean {
return this.expected_size < 15;
}
},
status: {
type: DataTypes.ENUM(...CLASS_STATUS_VALUES),
allowNull: true,
defaultValue: null,
},
};

export function initializeClassModel(sequelize: Sequelize) {
Class.init({
id: {
type: DataTypes.INTEGER.UNSIGNED,
allowNull: false,
autoIncrement: true,
primaryKey: true
},
name: {
type: DataTypes.STRING,
allowNull: false
},
educator_id: {
type: DataTypes.INTEGER.UNSIGNED,
allowNull: false,
references: {
model: Educator,
key: "id"
}
},
created: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: Sequelize.literal("CURRENT_TIMESTAMP")
},
updated: {
type: DataTypes.DATE,
allowNull: true,
defaultValue: null
},
active: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
},
code: {
type: DataTypes.STRING,
allowNull: false
},
asynchronous: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
},
test: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: 0
},
seed: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: 0,
},
expected_size: {
type: DataTypes.INTEGER.UNSIGNED,
allowNull: false,
},
small_class: {
type: DataTypes.VIRTUAL,
// type: "tinyint(1) GENERATED ALWAYS AS (expected_size < 15) VIRTUAL",
get() {
return this.expected_size < 15;
}
},
status: {
type: DataTypes.ENUM(...CLASS_STATUS_VALUES),
allowNull: true,
defaultValue: null,
},
}, {
Class.init(CLASS_ATTRIBUTES, {
sequelize,
indexes: [
{
Expand Down
3 changes: 3 additions & 0 deletions src/models/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { APIKeyRole, initializeAPIKeyRoleModel } from "./api_key_role";
import { Class, initializeClassModel } from "./class";
import { ActiveClass, initializeActiveClassModel } from "./active_class";
import { DashboardClassGroup, initializeDashboardClassGroupModel } from "./dashboard_class_group";
import { DummyClass, initializeDummyClassModel } from "./dummy_class";
import { Educator, initializeEducatorModel } from "./educator";
Expand All @@ -26,6 +27,7 @@ import { initializeUserExperienceRatingModel } from "./user_experience";
export {
APIKeyRole,
Class,
ActiveClass,
ClassStories,
CosmicDSSession,
DashboardClassGroup,
Expand Down Expand Up @@ -54,6 +56,7 @@ export function initializeModels(db: Sequelize) {
initializeSessionModel(db);
initializeEducatorModel(db);
initializeClassModel(db);
initializeActiveClassModel(db);
initializeStudentModel(db);
initializeStoryModel(db);
initializeClassStoryModel(db);
Expand Down
19 changes: 16 additions & 3 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
updateStageState,
deleteStageState,
findClassById,
deactivateClass,
getStages,
getStory,
getStageStates,
Expand Down Expand Up @@ -1225,7 +1226,7 @@ export function createApp(db: Sequelize, options?: AppOptions): Express {
return;
}

cls.destroy()
deactivateClass(cls)
.then(() => res.status(204).end())
.catch(error => {
console.log(error);
Expand Down Expand Up @@ -2177,6 +2178,10 @@ export function createApp(db: Sequelize, options?: AppOptions): Express {
* required: true
* schema:
* type: integer
* - name: active_only
* in: query
* schema:
* type: boolean
* responses:
* 200:
* description: The given educator exists. Returns an object containing the educator ID and a list of Class objects
Expand All @@ -2200,6 +2205,8 @@ export function createApp(db: Sequelize, options?: AppOptions): Express {
*/
app.get("/educator-classes/:educatorID", async (req, res) => {
const params = req.params;
const activeOnlyString = req.query.active_only as string | undefined;
const activeOnly = activeOnlyString?.toLowerCase() !== "false";
const educatorID = Number(params.educatorID);
const educator = await findEducatorById(educatorID);
if (educator === null) {
Expand All @@ -2208,7 +2215,7 @@ export function createApp(db: Sequelize, options?: AppOptions): Express {
});
return;
}
const classes = await getClassesForEducator(educatorID);
const classes = await getClassesForEducator(educatorID, activeOnly);
res.json({
educator_id: educatorID,
classes,
Expand All @@ -2229,6 +2236,10 @@ export function createApp(db: Sequelize, options?: AppOptions): Express {
* required: true
* schema:
* type: integer
* - name: active_only
* in: query
* schema:
* type: boolean
* responses:
* 200:
* description: Returns an object containing the student ID, as well as a list of Class items, one for each of the student's classes
Expand All @@ -2252,8 +2263,10 @@ export function createApp(db: Sequelize, options?: AppOptions): Express {
*/
app.get("/student-classes/:studentID", async (req, res) => {
const params = req.params;
const activeOnlyString = req.query.active_only as string | undefined;
const activeOnly = activeOnlyString?.toLowerCase() !== "false";
const studentID = Number(params.studentID);
const classes = await getClassesForStudent(studentID);
const classes = await getClassesForStudent(studentID, activeOnly);
res.json({
student_id: studentID,
classes: classes
Expand Down
4 changes: 4 additions & 0 deletions src/sql/create_active_class_view.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
CREATE VIEW ActiveClasses AS
SELECT *
FROM Classes
WHERE active = 1;
Loading
Loading