374 lines
12 KiB
D
374 lines
12 KiB
D
module api_modules.classroom_compliance;
|
|
|
|
import handy_httpd;
|
|
import handy_httpd.handlers.path_handler;
|
|
import std.typecons : Nullable;
|
|
import d2sqlite3;
|
|
|
|
import db;
|
|
import data_utils;
|
|
import api_modules.auth : User, getUserOrThrow;
|
|
|
|
struct ClassroomComplianceClass {
|
|
const ulong id;
|
|
const ushort number;
|
|
const string schoolYear;
|
|
const ulong userId;
|
|
}
|
|
|
|
struct ClassroomComplianceStudent {
|
|
const ulong id;
|
|
const string name;
|
|
const ulong classId;
|
|
}
|
|
|
|
struct ClassroomComplianceDeskAssignment {
|
|
const ulong id;
|
|
const ulong classId;
|
|
const ushort deskNumber;
|
|
const ulong studentId;
|
|
}
|
|
|
|
struct ClassroomComplianceEntry {
|
|
const ulong id;
|
|
const ulong classId;
|
|
const string classDescription;
|
|
const ulong studentId;
|
|
const string date;
|
|
const ulong createdAt;
|
|
const bool absent;
|
|
}
|
|
|
|
struct ClassroomComplianceEntryPhone {
|
|
const ulong entryId;
|
|
const bool compliant;
|
|
}
|
|
|
|
struct ClassroomComplianceEntryBehavior {
|
|
const ulong entryId;
|
|
const ubyte rating;
|
|
const string comment;
|
|
}
|
|
|
|
void registerApiEndpoints(PathHandler handler) {
|
|
const ROOT_PATH = "/api/classroom-compliance";
|
|
|
|
handler.addMapping(Method.POST, ROOT_PATH ~ "/classes", &createClass);
|
|
handler.addMapping(Method.GET, ROOT_PATH ~ "/classes", &getClasses);
|
|
const CLASS_PATH = ROOT_PATH ~ "/classes/:classId:ulong";
|
|
handler.addMapping(Method.DELETE, CLASS_PATH, &deleteClass);
|
|
|
|
handler.addMapping(Method.POST, CLASS_PATH ~ "/students", &createStudent);
|
|
handler.addMapping(Method.GET, CLASS_PATH ~ "/students", &getStudents);
|
|
const STUDENT_PATH = CLASS_PATH ~ "/students/:studentId:ulong";
|
|
handler.addMapping(Method.PUT, STUDENT_PATH, &updateStudent);
|
|
handler.addMapping(Method.DELETE, STUDENT_PATH, &deleteStudent);
|
|
|
|
handler.addMapping(Method.POST, CLASS_PATH ~ "/desk-assignments", &setDeskAssignments);
|
|
handler.addMapping(Method.GET, CLASS_PATH ~ "/desk-assignments", &getDeskAssignments);
|
|
handler.addMapping(Method.DELETE, CLASS_PATH ~ "/desk-assignments", &removeAllDeskAssignments);
|
|
|
|
handler.addMapping(Method.POST, CLASS_PATH ~ "/entries", &createEntry);
|
|
}
|
|
|
|
void createClass(ref HttpRequestContext ctx) {
|
|
User user = getUserOrThrow(ctx);
|
|
struct ClassPayload {
|
|
ushort number;
|
|
string schoolYear;
|
|
}
|
|
auto payload = readJsonPayload!(ClassPayload)(ctx);
|
|
auto db = getDb();
|
|
const bool classNumberExists = canFind(
|
|
db,
|
|
"SELECT id FROM classroom_compliance_class WHERE number = ? AND school_year = ? AND user_id = ?",
|
|
payload.number,
|
|
payload.schoolYear,
|
|
user.id
|
|
);
|
|
if (classNumberExists) {
|
|
ctx.response.status = HttpStatus.BAD_REQUEST;
|
|
ctx.response.writeBodyString("There is already a class with this number, for this school year.");
|
|
return;
|
|
}
|
|
auto stmt = db.prepare("INSERT INTO classroom_compliance_class (number, school_year, user_id) VALUES (?, ?, ?)");
|
|
stmt.bindAll(payload.number, payload.schoolYear, user.id);
|
|
stmt.execute();
|
|
ulong classId = db.lastInsertRowid();
|
|
auto newClass = findOne!(ClassroomComplianceClass)(
|
|
db,
|
|
"SELECT * FROM classroom_compliance_class WHERE id = ? AND user_id = ?",
|
|
classId,
|
|
user.id
|
|
).orElseThrow();
|
|
writeJsonBody(ctx, newClass);
|
|
}
|
|
|
|
void getClasses(ref HttpRequestContext ctx) {
|
|
User user = getUserOrThrow(ctx);
|
|
auto db = getDb();
|
|
auto classes = findAll!(ClassroomComplianceClass)(
|
|
db,
|
|
"SELECT * FROM classroom_compliance_class WHERE user_id = ? ORDER BY school_year DESC, number ASC",
|
|
user.id
|
|
);
|
|
writeJsonBody(ctx, classes);
|
|
}
|
|
|
|
void deleteClass(ref HttpRequestContext ctx) {
|
|
User user = getUserOrThrow(ctx);
|
|
auto cls = getClassOrThrow(ctx, user);
|
|
auto db = getDb();
|
|
db.execute("DELETE FROM classroom_compliance_class WHERE id = ? AND user_id = ?", cls.id, user.id);
|
|
}
|
|
|
|
ClassroomComplianceClass getClassOrThrow(ref HttpRequestContext ctx, in User user) {
|
|
ulong classId = ctx.request.getPathParamAs!ulong("classId");
|
|
auto db = getDb();
|
|
return findOne!(ClassroomComplianceClass)(
|
|
db,
|
|
"SELECT * FROM classroom_compliance_class WHERE user_id = ? AND id = ?",
|
|
user.id,
|
|
classId
|
|
).orElseThrow(() => new HttpStatusException(HttpStatus.NOT_FOUND));
|
|
}
|
|
|
|
void createStudent(ref HttpRequestContext ctx) {
|
|
User user = getUserOrThrow(ctx);
|
|
auto cls = getClassOrThrow(ctx, user);
|
|
struct StudentPayload {
|
|
string name;
|
|
}
|
|
auto payload = readJsonPayload!(StudentPayload)(ctx);
|
|
auto db = getDb();
|
|
bool studentExists = canFind(
|
|
db,
|
|
"SELECT id FROM classroom_compliance_student WHERE name = ? AND class_id = ?",
|
|
payload.name,
|
|
cls.id
|
|
);
|
|
if (studentExists) {
|
|
ctx.response.status = HttpStatus.BAD_REQUEST;
|
|
ctx.response.writeBodyString("Student with that name already exists.");
|
|
return;
|
|
}
|
|
db.execute("INSERT INTO classroom_compliance_student (name, class_id) VALUES (?, ?)", payload.name, cls.id);
|
|
ulong studentId = db.lastInsertRowid();
|
|
auto student = findOne!(ClassroomComplianceStudent)(
|
|
db,
|
|
"SELECT * FROM classroom_compliance_student WHERE id = ?",
|
|
studentId
|
|
).orElseThrow();
|
|
writeJsonBody(ctx, student);
|
|
}
|
|
|
|
void getStudents(ref HttpRequestContext ctx) {
|
|
User user = getUserOrThrow(ctx);
|
|
auto cls = getClassOrThrow(ctx, user);
|
|
auto db = getDb();
|
|
auto students = findAll!(ClassroomComplianceStudent)(
|
|
db,
|
|
"SELECT * FROM classroom_compliance_student WHERE class_id = ? ORDER BY name ASC",
|
|
cls.id
|
|
);
|
|
writeJsonBody(ctx, students);
|
|
}
|
|
|
|
void updateStudent(ref HttpRequestContext ctx) {
|
|
User user = getUserOrThrow(ctx);
|
|
auto student = getStudentOrThrow(ctx, user);
|
|
struct StudentUpdatePayload {
|
|
string name;
|
|
}
|
|
auto payload = readJsonPayload!(StudentUpdatePayload)(ctx);
|
|
// If there is nothing to update, quit.
|
|
if (payload.name == student.name) return;
|
|
// Check that the new name doesn't already exist.
|
|
auto db = getDb();
|
|
bool newNameExists = canFind(
|
|
db,
|
|
"SELECT id FROM classroom_compliance_student WHERE name = ? AND class_id = ?",
|
|
payload.name,
|
|
student.classId
|
|
);
|
|
if (newNameExists) {
|
|
ctx.response.status = HttpStatus.BAD_REQUEST;
|
|
ctx.response.writeBodyString("Student with that name already exists.");
|
|
return;
|
|
}
|
|
db.execute(
|
|
"UPDATE classroom_compliance_student SET name = ? WHERE id = ?",
|
|
payload.name,
|
|
student.id
|
|
);
|
|
auto updatedStudent = findOne!(ClassroomComplianceStudent)(
|
|
db,
|
|
"SELECT * FROM classroom_compliance_student WHERE id = ?",
|
|
student.id
|
|
).orElseThrow();
|
|
writeJsonBody(ctx, updatedStudent);
|
|
}
|
|
|
|
void deleteStudent(ref HttpRequestContext ctx) {
|
|
User user = getUserOrThrow(ctx);
|
|
auto student = getStudentOrThrow(ctx, user);
|
|
auto db = getDb();
|
|
db.execute(
|
|
"DELETE FROM classroom_compliance_student WHERE id = ? AND class_id = ?",
|
|
student.id,
|
|
student.classId
|
|
);
|
|
}
|
|
|
|
ClassroomComplianceStudent getStudentOrThrow(ref HttpRequestContext ctx, in User user) {
|
|
ulong classId = ctx.request.getPathParamAs!ulong("classId");
|
|
ulong studentId = ctx.request.getPathParamAs!ulong("studentId");
|
|
auto db = getDb();
|
|
string query = "
|
|
SELECT s.*
|
|
FROM classroom_compliance_student s
|
|
LEFT JOIN classroom_compliance_class c ON s.class_id = c.id
|
|
WHERE s.id = ? AND s.class_id = ? AND c.user_id = ?
|
|
";
|
|
return findOne!(ClassroomComplianceStudent)(
|
|
db,
|
|
query,
|
|
studentId,
|
|
classId,
|
|
user.id
|
|
).orElseThrow(() => new HttpStatusException(HttpStatus.NOT_FOUND));
|
|
}
|
|
|
|
private struct DeskAssignmentPayloadEntry {
|
|
ushort deskNumber;
|
|
Nullable!ulong studentId;
|
|
}
|
|
private struct DeskAssignmentPayload {
|
|
DeskAssignmentPayloadEntry[] entries;
|
|
}
|
|
|
|
void setDeskAssignments(ref HttpRequestContext ctx) {
|
|
User user = getUserOrThrow(ctx);
|
|
auto cls = getClassOrThrow(ctx, user);
|
|
auto payload = readJsonPayload!(DeskAssignmentPayload)(ctx);
|
|
auto db = getDb();
|
|
auto validationError = validateDeskAssignments(db, payload, cls.id);
|
|
if (validationError) {
|
|
import slf4d;
|
|
warnF!"Desk assignment validation failed: %s"(validationError.value);
|
|
ctx.response.status = HttpStatus.BAD_REQUEST;
|
|
ctx.response.writeBodyString(validationError.value);
|
|
return;
|
|
}
|
|
|
|
db.begin();
|
|
try {
|
|
db.execute(
|
|
"DELETE FROM classroom_compliance_desk_assignment WHERE class_id = ?",
|
|
cls.id
|
|
);
|
|
auto stmt = db.prepare(
|
|
"INSERT INTO classroom_compliance_desk_assignment (class_id, desk_number, student_id) VALUES (?, ?, ?)"
|
|
);
|
|
foreach (entry; payload.entries) {
|
|
stmt.bindAll(cls.id, entry.deskNumber, entry.studentId);
|
|
stmt.execute();
|
|
stmt.clearBindings();
|
|
stmt.reset();
|
|
}
|
|
db.commit();
|
|
// Return the new list of desk assignments to the user.
|
|
auto newAssignments = findAll!(ClassroomComplianceDeskAssignment)(
|
|
db,
|
|
"SELECT * FROM classroom_compliance_desk_assignment WHERE class_id = ?",
|
|
cls.id
|
|
);
|
|
writeJsonBody(ctx, newAssignments);
|
|
} catch (Exception e) {
|
|
db.rollback();
|
|
}
|
|
}
|
|
|
|
Optional!string validateDeskAssignments(Database db, in DeskAssignmentPayload payload, ulong classId) {
|
|
import std.algorithm : canFind, map;
|
|
import std.array;
|
|
// Check that desks are numbered 1 .. N.
|
|
for (int n = 1; n <= payload.entries.length; n++) {
|
|
bool deskPresent = false;
|
|
foreach (entry; payload.entries) {
|
|
if (entry.deskNumber == n) {
|
|
deskPresent = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!deskPresent) return Optional!string.of("Desks should be numbered from 1 to N.");
|
|
}
|
|
auto allStudents = findAll!(ClassroomComplianceStudent)(
|
|
db,
|
|
"SELECT * FROM classroom_compliance_student WHERE class_id = ?",
|
|
classId
|
|
);
|
|
auto studentIds = allStudents.map!(s => s.id).array;
|
|
// Check that if a desk is assigned to a student, that it's one from this class.
|
|
foreach (entry; payload.entries) {
|
|
if (!entry.studentId.isNull && !canFind(studentIds, entry.studentId.get)) {
|
|
return Optional!string.of("Desks cannot be assigned to students that don't belong to this class.");
|
|
}
|
|
}
|
|
|
|
// Check that each student in the class is assigned a desk.
|
|
ushort[] takenDesks;
|
|
foreach (student; allStudents) {
|
|
ushort[] assignedDesks;
|
|
foreach (entry; payload.entries) {
|
|
if (!entry.studentId.isNull && entry.studentId.get() == student.id) {
|
|
assignedDesks ~= entry.deskNumber;
|
|
}
|
|
}
|
|
if (assignedDesks.length != 1) {
|
|
if (assignedDesks.length > 1) {
|
|
return Optional!string.of("A student is assigned to more than one desk.");
|
|
} else {
|
|
return Optional!string.of("Not all students are assigned to a desk.");
|
|
}
|
|
}
|
|
if (canFind(takenDesks, assignedDesks[0])) {
|
|
return Optional!string.of("Cannot assign more than one student to the same desk.");
|
|
}
|
|
takenDesks ~= assignedDesks[0];
|
|
}
|
|
return Optional!string.empty();
|
|
}
|
|
|
|
void getDeskAssignments(ref HttpRequestContext ctx) {
|
|
User user = getUserOrThrow(ctx);
|
|
auto cls = getClassOrThrow(ctx, user);
|
|
auto db = getDb();
|
|
auto deskAssignments = findAll!(ClassroomComplianceDeskAssignment)(
|
|
db,
|
|
"
|
|
SELECT d.* FROM classroom_compliance_desk_assignment d
|
|
WHERE class_id = ?
|
|
ORDER BY desk_number ASC
|
|
",
|
|
cls.id
|
|
);
|
|
writeJsonBody(ctx, deskAssignments);
|
|
}
|
|
|
|
void removeAllDeskAssignments(ref HttpRequestContext ctx) {
|
|
User user = getUserOrThrow(ctx);
|
|
auto cls = getClassOrThrow(ctx, user);
|
|
auto db = getDb();
|
|
db.execute(
|
|
"DELETE FROM classroom_compliance_desk_assignment WHERE class_id = ?",
|
|
cls.id
|
|
);
|
|
}
|
|
|
|
void createEntry(ref HttpRequestContext ctx) {
|
|
User user = getUserOrThrow(ctx);
|
|
auto cls = getClassOrThrow(ctx, user);
|
|
|
|
}
|