module api_modules.classroom_compliance.api_student; import handy_httpd; import ddbc; import std.json; import api_modules.auth : User, getUserOrThrow; import api_modules.classroom_compliance.model; import api_modules.classroom_compliance.util; import db; import data_utils; void createStudent(ref HttpRequestContext ctx) { Connection conn = getDb(); scope(exit) conn.close(); User user = getUserOrThrow(ctx, conn); auto cls = getClassOrThrow(ctx, conn, user); if (cls.archived) throw new HttpStatusException(HttpStatus.FORBIDDEN, "Class is archived."); struct StudentPayload { string name; ushort deskNumber; bool removed; } auto payload = readJsonPayload!(StudentPayload)(ctx); bool studentExists = recordExists( conn, "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("A student with that name already exists in this class."); return; } bool deskAlreadyOccupied = payload.deskNumber != 0 && recordExists( conn, "SELECT id FROM classroom_compliance_student WHERE class_id = ? AND desk_number = ?", cls.id, payload.deskNumber ); if (deskAlreadyOccupied) { ctx.response.status = HttpStatus.BAD_REQUEST; ctx.response.writeBodyString("There is already a student assigned to that desk number."); return; } ulong studentId = insertOne( conn, "INSERT INTO classroom_compliance_student (name, class_id, desk_number, removed) VALUES (?, ?, ?, ?) RETURNING id", payload.name, cls.id, payload.deskNumber, payload.removed ); auto student = findOne( conn, "SELECT * FROM classroom_compliance_student WHERE id = ?", &ClassroomComplianceStudent.parse, studentId ).orElseThrow(); writeJsonBody(ctx, student); } void getStudents(ref HttpRequestContext ctx) { Connection conn = getDb(); scope(exit) conn.close(); User user = getUserOrThrow(ctx, conn); auto cls = getClassOrThrow(ctx, conn, user); auto students = findAll( conn, "SELECT * FROM classroom_compliance_student WHERE class_id = ? ORDER BY name ASC", &ClassroomComplianceStudent.parse, cls.id ); writeJsonBody(ctx, students); } void getStudent(ref HttpRequestContext ctx) { Connection conn = getDb(); scope(exit) conn.close(); User user = getUserOrThrow(ctx, conn); auto student = getStudentOrThrow(ctx, conn, user); writeJsonBody(ctx, student); } void updateStudent(ref HttpRequestContext ctx) { Connection conn = getDb(); scope(exit) conn.close(); User user = getUserOrThrow(ctx, conn); auto cls = getClassOrThrow(ctx, conn, user); if (cls.archived) throw new HttpStatusException(HttpStatus.FORBIDDEN, "Class is archived."); auto student = getStudentOrThrow(ctx, conn, user); struct StudentUpdatePayload { string name; ushort deskNumber; bool removed; } auto payload = readJsonPayload!(StudentUpdatePayload)(ctx); // If there is nothing to update, quit. if ( payload.name == student.name && payload.deskNumber == student.deskNumber && payload.removed == student.removed ) return; // Check that the new name doesn't already exist. bool newNameExists = payload.name != student.name && recordExists( conn, "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("A student with that name already exists in this class."); return; } // Check that if a new desk number is assigned, that it's not already assigned to anyone else. bool newDeskOccupied = payload.deskNumber != 0 && payload.deskNumber != student.deskNumber && recordExists( conn, "SELECT id FROM classroom_compliance_student WHERE class_id = ? AND desk_number = ?", student.classId, payload.deskNumber ); if (newDeskOccupied) { ctx.response.status = HttpStatus.BAD_REQUEST; ctx.response.writeBodyString("That desk is already assigned to another student."); return; } update( conn, "UPDATE classroom_compliance_student SET name = ?, desk_number = ?, removed = ? WHERE id = ?", payload.name, payload.deskNumber, payload.removed, student.id ); auto updatedStudent = findOne( conn, "SELECT * FROM classroom_compliance_student WHERE id = ?", &ClassroomComplianceStudent.parse, student.id ).orElseThrow(); writeJsonBody(ctx, updatedStudent); } void deleteStudent(ref HttpRequestContext ctx) { Connection conn = getDb(); scope(exit) conn.close(); User user = getUserOrThrow(ctx, conn); auto cls = getClassOrThrow(ctx, conn, user); if (cls.archived) throw new HttpStatusException(HttpStatus.FORBIDDEN, "Class is archived."); auto student = getStudentOrThrow(ctx, conn, user); update( conn, "DELETE FROM classroom_compliance_student WHERE id = ? AND class_id = ?", student.id, student.classId ); } void getStudentEntries(ref HttpRequestContext ctx) { Connection conn = getDb(); scope(exit) conn.close(); User user = getUserOrThrow(ctx, conn); auto student = getStudentOrThrow(ctx, conn, user); const query = import("source/api_modules/classroom_compliance/queries/find_entries_by_student.sql"); PreparedStatement ps = conn.prepareStatement(query); scope(exit) ps.close(); ps.setUlong(1, student.id); ResultSet rs = ps.executeQuery(); ClassroomComplianceEntry entry; ClassroomComplianceEntryChecklistItem[] checklistItems; JSONValue responseArray = JSONValue.emptyArray; bool hasNextRow = rs.next(); while (hasNextRow) { entry.date = rs.getDate(1); entry.classId = rs.getUlong(2); entry.createdAt = rs.getUlong(3); entry.absent = rs.getBoolean(4); entry.comment = rs.getString(5); entry.studentId = student.id; bool hasChecklistItem = !rs.isNull(6); if (hasChecklistItem) { checklistItems ~= ClassroomComplianceEntryChecklistItem( rs.getString(6), rs.getBoolean(7), rs.getString(8) ); } hasNextRow = rs.next(); bool shouldSaveEntry = !hasNextRow || rs.getDate(1) != entry.date; if (shouldSaveEntry) { JSONValue obj = JSONValue.emptyObject; obj.object["date"] = JSONValue(entry.date.toISOExtString()); obj.object["createdAt"] = JSONValue(entry.createdAt); obj.object["absent"] = JSONValue(entry.absent); obj.object["comment"] = JSONValue(entry.comment); obj.object["checklistItems"] = JSONValue.emptyArray; foreach (item; checklistItems) { JSONValue ckObj = JSONValue.emptyObject; ckObj.object["item"] = JSONValue(item.item); ckObj.object["checked"] = JSONValue(item.checked); ckObj.object["category"] = JSONValue(item.category); obj.object["checklistItems"].array ~= ckObj; } responseArray.array ~= obj; checklistItems = []; } } ctx.response.writeBodyString(responseArray.toJSON(), "application/json"); } void getStudentOverview(ref HttpRequestContext ctx) { Connection conn = getDb(); scope(exit) conn.close(); User user = getUserOrThrow(ctx, conn); auto student = getStudentOrThrow(ctx, conn, user); const ulong entryCount = count( conn, "SELECT COUNT(DISTINCT(class_id, date)) FROM classroom_compliance_entry WHERE student_id = ?", student.id ); if (entryCount == 0) { ctx.response.status = HttpStatus.NOT_FOUND; ctx.response.writeBodyString("No entries for this student."); return; } const ulong absenceCount = count( conn, "SELECT COUNT(DISTINCT(class_id, date)) FROM classroom_compliance_entry WHERE student_id = ? AND absent = true", student.id ); // Calculate derived statistics. const ulong attendanceCount = entryCount - absenceCount; double attendanceRate = attendanceCount / cast(double) entryCount; JSONValue response = JSONValue.emptyObject; response.object["attendanceRate"] = JSONValue(attendanceRate); // response.object["phoneComplianceRate"] = JSONValue(phoneComplianceRate); // response.object["behaviorScore"] = JSONValue(behaviorScore); response.object["entryCount"] = JSONValue(entryCount); ctx.response.writeBodyString(response.toJSON(), "application/json"); } void moveStudentToOtherClass(ref HttpRequestContext ctx) { Connection conn = getDb(); scope(exit) conn.close(); conn.setAutoCommit(false); User user = getUserOrThrow(ctx, conn); auto cls = getClassOrThrow(ctx, conn, user); if (cls.archived) throw new HttpStatusException(HttpStatus.FORBIDDEN, "Class is archived."); auto student = getStudentOrThrow(ctx, conn, user); struct Payload { ulong classId; } Payload payload = readJsonPayload!(Payload)(ctx); if (payload.classId == student.classId) { return; // Quit if the student is already in the desired class. } // Check that the desired class exists, and belongs to the user. bool newClassIdValid = recordExists( conn, "SELECT id FROM classroom_compliance_class WHERE user_id = ? and id = ?", user.id, payload.classId ); if (!newClassIdValid) { ctx.response.status = HttpStatus.BAD_REQUEST; ctx.response.writeBodyString("Invalid class was selected."); return; } // Check that the new class doesn't already have a student with the same name. bool studentNameExistsInNewClass = recordExists( conn, "SELECT id FROM classroom_compliance_student WHERE class_id = ? AND name = ?", payload.classId, student.name ); if (studentNameExistsInNewClass) { ctx.response.status = HttpStatus.BAD_REQUEST; ctx.response.writeBodyString("A student in the selected class has the same name as this one."); return; } // All good, so update the student's class to the desired one, and reset their desk. update( conn, "UPDATE classroom_compliance_student SET class_id = ?, desk_number = 0 WHERE id = ?", payload.classId, student.id ); conn.commit(); // We just return 200 OK, no response body. } void getStudentLabels(ref HttpRequestContext ctx) { Connection conn = getDb(); scope(exit) conn.close(); User user = getUserOrThrow(ctx, conn); auto student = getStudentOrThrow(ctx, conn, user); string[] labels = findAll( conn, "SELECT label FROM classroom_compliance_student_label WHERE student_id = ? ORDER BY label ASC", (r) => r.getString(1), student.id ); writeJsonBody(ctx, labels); } void updateStudentLabels(ref HttpRequestContext ctx) { Connection conn = getDb(); scope(exit) conn.close(); User user = getUserOrThrow(ctx, conn); auto student = getStudentOrThrow(ctx, conn, user); string[] labels = readJsonPayload!(string[])(ctx); conn.setAutoCommit(false); update(conn, "DELETE FROM classroom_compliance_student_label WHERE student_id = ?", student.id); PreparedStatement ps = conn.prepareStatement( "INSERT INTO classroom_compliance_student_label (student_id, label) VALUES (?, ?)" ); foreach (label; labels) { ps.setUlong(1, student.id); ps.setString(2, label); ps.executeUpdate(); } conn.commit(); }