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); }