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); 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 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 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); auto entries = findAll( conn, "SELECT * FROM classroom_compliance_entry WHERE student_id = ? ORDER BY date DESC", &ClassroomComplianceEntry.parse, student.id ); JSONValue response = JSONValue.emptyArray; foreach (entry; entries) response.array ~= entry.toJsonObj(); ctx.response.writeBodyString(response.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(id) 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(id) FROM classroom_compliance_entry WHERE student_id = ? AND absent = true", student.id ); const ulong phoneNoncomplianceCount = count( conn, "SELECT COUNT(id) FROM classroom_compliance_entry WHERE phone_compliant = FALSE AND student_id = ?", student.id ); const behaviorCountQuery = " SELECT COUNT(id) FROM classroom_compliance_entry WHERE student_id = ? AND behavior_rating = ? "; const ulong behaviorGoodCount = count(conn, behaviorCountQuery, student.id, 3); const ulong behaviorMediocreCount = count(conn, behaviorCountQuery, student.id, 2); const ulong behaviorPoorCount = count(conn, behaviorCountQuery, student.id, 1); // Calculate derived statistics. const ulong attendanceCount = entryCount - absenceCount; double attendanceRate = attendanceCount / cast(double) entryCount; double phoneComplianceRate = (attendanceCount - phoneNoncomplianceCount) / cast(double) attendanceCount; double behaviorScore = ( behaviorGoodCount * 1.0 + behaviorMediocreCount * 0.5 ) / attendanceCount; 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(); User user = getUserOrThrow(ctx, conn); 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; } // 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 ); // We just return 200 OK, no response body. }