Skip to main content

Overview

The Grade Management system enables teachers to enter and update student grades for their courses, while students can view their grades and academic performance. The system includes security controls to ensure teachers can only grade their own courses.

Grade Entry

Teachers enter grades for enrolled students

Grade Updates

Modify existing grades using REPLACE INTO

Student Access

Students view their grades by subject

Bulletin Generation

Calculate averages and academic status

Grade Architecture

Database Schema

CREATE TABLE grades (
    student_id INT NOT NULL,
    course_id INT NOT NULL,
    grade DECIMAL(5,2) NOT NULL,
    PRIMARY KEY (student_id, course_id),
    FOREIGN KEY (student_id) REFERENCES students(id) ON DELETE CASCADE,
    FOREIGN KEY (course_id) REFERENCES courses(id) ON DELETE CASCADE
);
Schema Design:
  • Composite Primary Key: One grade per student per course
  • Decimal Type: Stores grades like 15.50, 18.75 (precision for calculations)
  • Cascade Deletion: Grades deleted when student or course is removed
  • Update Pattern: Using REPLACE INTO for insert-or-update behavior

Teacher Workflows

Viewing Teacher’s Courses

Teachers first see their assigned courses:
// GradeController.php - myCourses()
public function myCourses()
{
    Auth::teacher(); // Only teachers can access

    $teacher_id = $_SESSION['user']['id'];
    $courses = $this->courseModel->getByTeacher($teacher_id);

    require __DIR__ . '/../views/grades/my_courses.php';
}
// Course.php - getByTeacher()
public function getByTeacher($teacher_id)
{
    $stmt = $this->db->prepare("
        SELECT courses.id, subjects.name
        FROM courses
        JOIN subjects ON courses.subject_id = subjects.id
        WHERE courses.teacher_id = ?
    ");
    $stmt->execute([$teacher_id]);
    return $stmt->fetchAll();
}
  1. Teacher logs in and navigates to Grades section
  2. System shows only courses assigned to this teacher
  3. Teacher selects a course (e.g., “Mathematics”)
  4. System displays all enrolled students with current grades
  5. Teacher can enter or update grades

Viewing Enrolled Students

After selecting a course, teachers see the roster:
// GradeController.php - students()
public function students($course_id)
{
    Auth::teacher();

    $teacher_id = $_SESSION['user']['id'];

    // Security: Ensure course belongs to this teacher
    if (!$this->courseModel->belongsToTeacher($course_id, $teacher_id)) {
        header("Location: /grades");
        exit;
    }

    $students = $this->gradeModel->getStudentsByCourse($course_id);

    require __DIR__ . '/../views/grades/students.php';
}
// Grade.php - getStudentsByCourse()
public function getStudentsByCourse($course_id)
{
    $stmt = $this->db->prepare("
        SELECT students.id, users.name, grades.grade
        FROM enrollments
        JOIN students ON enrollments.student_id = students.id
        JOIN users ON students.id = users.id
        LEFT JOIN grades 
          ON grades.student_id = students.id 
         AND grades.course_id = enrollments.course_id
        WHERE enrollments.course_id = ?
    ");
    $stmt->execute([$course_id]);
    return $stmt->fetchAll();
}
Key Components:
  1. FROM enrollments: Start with enrolled students only
  2. JOIN students: Get student IDs
  3. JOIN users: Get student names
  4. LEFT JOIN grades: Include students without grades (NULL)
Result:
  • All enrolled students appear in the list
  • Students with grades show current grade value
  • Students without grades show NULL (ready for entry)
  • Teachers cannot see non-enrolled students
Security Check: The belongsToTeacher() verification prevents teachers from accessing courses they don’t teach by manually modifying the URL.

Entering and Updating Grades

Teachers can enter new grades or update existing ones:
// GradeController.php - save()
public function save()
{
    Auth::teacher();

    $teacher_id = $_SESSION['user']['id'];
    $course_id = $_POST['course_id'];

    // Security: Ensure course belongs to teacher
    if (!$this->courseModel->belongsToTeacher($course_id, $teacher_id)) {
        header("Location: /grades");
        exit;
    }

    $this->gradeModel->save(
        $_POST['student_id'],
        $course_id,
        $_POST['grade']
    );

    header("Location: /grades/course?course_id=" . $course_id);
    exit;
}
// Grade.php - save()
public function save($student_id, $course_id, $grade)
{
    $stmt = $this->db->prepare("
        REPLACE INTO grades (student_id, course_id, grade)
        VALUES (?, ?, ?)
    ");
    $stmt->execute([$student_id, $course_id, $grade]);
}
REPLACE INTO Behavior:
  • If grade doesn’t exist: INSERT new record
  • If grade exists: DELETE old record and INSERT new one
  • Effectively updates the grade without needing separate INSERT/UPDATE logic
  • Works because of the composite primary key (student_id, course_id)

Student Workflows

Viewing Personal Grades

Students can view all their grades:
// Grade.php - getStudentGrades()
public function getStudentGrades($student_id)
{
    $stmt = $this->db->prepare("
        SELECT subjects.name AS subject, grades.grade
        FROM grades
        JOIN courses ON grades.course_id = courses.id
        JOIN subjects ON courses.subject_id = subjects.id
        WHERE grades.student_id = ?
    ");
    $stmt->execute([$student_id]);
    return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
Example Output:
[
    ['subject' => 'Mathematics', 'grade' => '16.50'],
    ['subject' => 'Physics', 'grade' => '14.00'],
    ['subject' => 'English', 'grade' => '18.00']
]

Bulletin Data with Teacher Information

Enhanced grade view including teacher names:
// Grade.php - getBulletinData()
public function getBulletinData($student_id)
{
    $sql = "
        SELECT 
            subjects.name AS subject,
            users.name AS teacher,
            grades.grade
        FROM grades
        JOIN courses ON grades.course_id = courses.id
        JOIN subjects ON courses.subject_id = subjects.id
        JOIN users ON courses.teacher_id = users.id
        WHERE grades.student_id = ?
    ";

    $stmt = $this->db->prepare($sql);
    $stmt->execute([$student_id]);
    return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
Example Output:
[
    [
        'subject' => 'Mathematics',
        'teacher' => 'Mr. Smith',
        'grade' => '16.50'
    ],
    [
        'subject' => 'Physics',
        'teacher' => 'Dr. Johnson',
        'grade' => '14.00'
    ]
]
getStudentGrades():
  • Returns subject and grade only
  • Simpler query, faster execution
  • Used for quick grade listings
getBulletinData():
  • Returns subject, teacher, and grade
  • More comprehensive data
  • Used for formal bulletins/transcripts
  • Same data but with teacher attribution

Grade Calculations

Average Calculation

Bulletin controller calculates student averages:
// BulletinController.php - show()
public function show()
{
    Auth::check();

    // Determine student ID (own ID for students, parameter for admin/teacher)
    if ($_SESSION['user']['role'] === 'student') {
        $student_id = $_SESSION['user']['id'];
    } else {
        if (!isset($_GET['student_id'])) {
            die("Student ID is required");
        }
        $student_id = $_GET['student_id'];
    }

    $student = $this->userModel->findById($student_id);
    $grades = $this->gradeModel->getBulletinData($student_id);

    // Calculate average
    $total = 0;
    $count = count($grades);

    foreach ($grades as $g) {
        $total += $g['grade'];
    }

    $average = $count > 0 ? round($total / $count, 2) : 0;
    $status = $average >= 10 ? 'VALIDÉ' : 'NON VALIDÉ';

    require __DIR__ . '/../views/bulletin/show.php';
}
Grading System:
  • Grades are numerical (e.g., 0-20 scale, common in French systems)
  • Average calculated as simple mean of all course grades
  • Passing Grade: 10.00 or higher
  • Status: VALIDÉ (passed) or NON VALIDÉ (failed)
  • Rounded to 2 decimal places for display

Example Calculation

// Student has 3 grades:
$grades = [
    ['subject' => 'Math', 'grade' => 16.50],
    ['subject' => 'Physics', 'grade' => 14.00],
    ['subject' => 'English', 'grade' => 18.00]
];

$total = 16.50 + 14.00 + 18.00; // = 48.50
$count = 3;
$average = round(48.50 / 3, 2); // = 16.17
$status = 16.17 >= 10 ? 'VALIDÉ' : 'NON VALIDÉ'; // = 'VALIDÉ'

Security Measures

Course Ownership Verification

Critical security check used in multiple methods:
// Course.php - belongsToTeacher()
public function belongsToTeacher($course_id, $teacher_id)
{
    $stmt = $this->db->prepare(
        "SELECT COUNT(*) FROM courses WHERE id = ? AND teacher_id = ?"
    );
    $stmt->execute([$course_id, $teacher_id]);
    return $stmt->fetchColumn() > 0;
}
Used before:
  • Viewing course roster
  • Entering grades
  • Updating grades
Prevents:
  • Teachers accessing other teachers’ courses
  • Unauthorized grade modifications
  • URL manipulation attacks

Role-Based Access Control

// Auth.php - teacher()
public static function teacher()
{
    self::check(); // Verify logged in
    if ($_SESSION['user']['role'] !== 'teacher') {
        self::redirectLogin();
    }
}

// Auth.php - student()
public static function student()
{
    self::check();
    if ($_SESSION['user']['role'] !== 'student') {
        self::redirectLogin();
    }
}

Teacher Permissions

  • View assigned courses only
  • Enter grades for enrolled students
  • Update existing grades
  • Cannot access other teachers’ courses

Student Permissions

  • View own grades only
  • View own bulletin
  • Cannot view other students’ data
  • Read-only access to grades

Complete Grade Model

// Grade.php - Full implementation
class Grade
{
    private $db;

    public function __construct()
    {
        $this->db = Database::getInstance()->getConnection();
    }

    // Get enrolled students for a course with their grades
    public function getStudentsByCourse($course_id)
    {
        $stmt = $this->db->prepare("
            SELECT students.id, users.name, grades.grade
            FROM enrollments
            JOIN students ON enrollments.student_id = students.id
            JOIN users ON students.id = users.id
            LEFT JOIN grades 
              ON grades.student_id = students.id 
             AND grades.course_id = enrollments.course_id
            WHERE enrollments.course_id = ?
        ");
        $stmt->execute([$course_id]);
        return $stmt->fetchAll();
    }

    // Insert or update grade (REPLACE INTO)
    public function save($student_id, $course_id, $grade)
    {
        $stmt = $this->db->prepare("
            REPLACE INTO grades (student_id, course_id, grade)
            VALUES (?, ?, ?)
        ");
        $stmt->execute([$student_id, $course_id, $grade]);
    }

    // Get all grades for a student (subject and grade only)
    public function getStudentGrades($student_id)
    {
        $stmt = $this->db->prepare("
            SELECT subjects.name AS subject, grades.grade
            FROM grades
            JOIN courses ON grades.course_id = courses.id
            JOIN subjects ON courses.subject_id = subjects.id
            WHERE grades.student_id = ?
        ");
        $stmt->execute([$student_id]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    // Get bulletin data (subject, teacher, grade)
    public function getBulletinData($student_id)
    {
        $sql = "
            SELECT 
                subjects.name AS subject,
                users.name AS teacher,
                grades.grade
            FROM grades
            JOIN courses ON grades.course_id = courses.id
            JOIN subjects ON courses.subject_id = subjects.id
            JOIN users ON courses.teacher_id = users.id
            WHERE grades.student_id = ?
        ";

        $stmt = $this->db->prepare($sql);
        $stmt->execute([$student_id]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
}

API Endpoints

Teacher Endpoints

MethodEndpointAccessDescription
GET/gradesTeacherList teacher’s courses
GET/grades/course?course_id={id}TeacherView students in course
POST/grades/saveTeacherEnter/update grade

Student Endpoints

MethodEndpointAccessDescription
GET/bulletin/showStudentView own bulletin with grades
GET/bulletin/show?student_id={id}AdminView any student’s bulletin

Use Cases

Context: Mr. Smith teaches Mathematics and needs to enter gradesFlow:
  1. Mr. Smith logs in as teacher
  2. Navigates to Grades section → sees “Mathematics” course
  3. Clicks on Mathematics
  4. System verifies: belongsToTeacher(math_course_id, smith_id) → true
  5. System shows enrolled students:
    • John Doe (current grade: NULL)
    • Jane Smith (current grade: 15.50)
  6. Mr. Smith enters 16.50 for John Doe
  7. System calls: save(john_id, math_course_id, 16.50)
  8. Database executes: REPLACE INTO grades...
  9. Grade saved successfully
  10. John can now see Mathematics: 16.50 in his bulletin
Context: Dr. Johnson needs to correct a Physics gradeFlow:
  1. Dr. Johnson views Physics course roster
  2. Sees Jane Smith has grade: 12.00
  3. Realizes it should be 14.00
  4. Enters 14.00 in the grade field
  5. Submits form
  6. System calls: save(jane_id, physics_course_id, 14.00)
  7. REPLACE INTO:
    • Deletes old record (jane_id, physics_course_id, 12.00)
    • Inserts new record (jane_id, physics_course_id, 14.00)
  8. Grade updated successfully
  9. Jane’s bulletin now shows Physics: 14.00
Context: John Doe wants to check his gradesFlow:
  1. John logs in as student
  2. Navigates to Bulletin
  3. System gets student_id from session: john_id
  4. Calls: getBulletinData(john_id)
  5. Returns:
    • Mathematics (Mr. Smith): 16.50
    • Physics (Dr. Johnson): 14.00
    • English (Ms. Williams): 18.00
  6. System calculates:
    • Total: 48.50
    • Count: 3
    • Average: 16.17
    • Status: VALIDÉ (16.17 >= 10)
  7. Bulletin displays:
    • All course grades with teachers
    • Average: 16.17
    • Status: VALIDÉ
Context: Teacher tries to access another teacher’s courseFlow:
  1. Mr. Smith (teaches Math) manually types URL: /grades/course?course_id=5 (Physics, taught by Dr. Johnson)
  2. System calls: belongsToTeacher(5, smith_id)
  3. Query: SELECT COUNT(*) FROM courses WHERE id=5 AND teacher_id=smith_id
  4. Result: 0 (course 5 is not assigned to Smith)
  5. Method returns: false
  6. System redirects to: /grades
  7. Mr. Smith sees only his own courses
  8. Unauthorized access prevented

Data Relationships

┌──────────┐
│ Students │
└────┬─────┘

     │ student_id

┌──────────┐      course_id      ┌──────────┐
│  Grades  │◄────────────────────│ Courses  │
│──────────│                     │──────────│
│student_id│                     │    id    │
│course_id │                     │subject_id│
│  grade   │                     │teacher_id│
└──────────┘                     └────┬─────┘


                              Used in Bulletin
Grade Dependencies:
  • Requires enrollment (student must be enrolled in course)
  • Requires course (links to teacher and subject)
  • One grade per student per course (composite PK)
  • Used in bulletin for average calculations

Best Practices

Security

  • Always verify course ownership
  • Validate teacher has permission
  • Use role-based access control
  • Prevent URL manipulation attacks

Data Integrity

  • Use DECIMAL for precise grade storage
  • Implement composite primary keys
  • Use REPLACE INTO for updates
  • Maintain referential integrity

User Experience

  • Show current grades when entering new ones
  • Display teacher names in student views
  • Calculate and display averages
  • Provide clear pass/fail status

Performance

  • Use LEFT JOIN for optional grades
  • Index foreign keys
  • Minimize query complexity
  • Cache frequently accessed data

Build docs developers (and LLMs) love