Question 4
==
You are tasked with creating a report to track students' performance across different modules. The student portal
has the following database tables
Students
- ○ student_id
- ○ first_name
- ○ last_name
- ○ date_of_birth
- ○ grade_level
Subjects
- ○ subject_id
- ○ subject_name
Enrollments
- ○ student_id
- ○ subject_id
- ○ enrollment_date
Grades
- ○ student_id
- ○ subject_id ○ grade
- ○ grade_date
A. Write an SQL query to generate a report that lists all students, their enrolled subjects, and their most recent grade. The report should include
- a. Student's first name, last name, and grade level.
- b. Subject name.
- c. The most recent grade for each student in each subject (if available).
B. What would you do to handle cases where no grade is available for a student in a particular module?
C. Can you write a query that ranks students based on their most recent grade for each module?
### Solution
To follow along:
Git clone the following repository: https://github.com/x0can/LMS-API/tree/main
Run `cd LMS-API`
```pip3 install -r requirements.txt```
in db.connection.py
```python=
import sqlite3
def get_connection():
"""Establish a connection to the SQLite database."""
conn = sqlite3.connect('_courses_.db')
conn.row_factory = sqlite3.Row # Access rows as dictionaries
return conn
```
Create a model to aid in creating the following tables `Students`, `Subjects`, `Enrollments`, `Grades`
Here is the proposed SQL schema
Assumptions: All `*_id` for the tables are integers
```sql=
CREATE TABLE IF NOT EXISTS Students (
student_id INTEGER PRIMARY KEY AUTOINCREMENT,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
date_of_birth TEXT NOT NULL,
grade_level TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS Subjects (
subject_id INTEGER PRIMARY KEY AUTOINCREMENT,
subject_name TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS Enrollments (
enrollment_id INTEGER PRIMARY KEY AUTOINCREMENT,
student_id INTEGER,
subject_id INTEGER,
enrollment_date TEXT NOT NULL,
FOREIGN KEY(student_id) REFERENCES Students(student_id),
FOREIGN KEY(subject_id) REFERENCES Subjects(subject_id)
);
CREATE TABLE IF NOT EXISTS Grades (
grade_id INTEGER PRIMARY KEY AUTOINCREMENT,
student_id INTEGER,
subject_id INTEGER,
grade TEXT NOT NULL,
grade_date TEXT NOT NULL,
FOREIGN KEY(student_id) REFERENCES Students(student_id),
FOREIGN KEY(subject_id) REFERENCES Subjects(subject_id)
);
```
Then let's now update `models.course_sql_operations.py` using the above schema
```python=
from db.connection import get_connection
class CourseOperations:
def __init__(self):
self.conn = get_connection()
self.cursor = get_connection().cursor()
def create_tables(self):
"""Create the database tables."""
self.cursor.executescript("""
CREATE TABLE IF NOT EXISTS Students (
student_id INTEGER PRIMARY KEY AUTOINCREMENT,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
date_of_birth TEXT NOT NULL,
grade_level TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS Subjects (
subject_id INTEGER PRIMARY KEY AUTOINCREMENT,
subject_name TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS Enrollments (
enrollment_id INTEGER PRIMARY KEY AUTOINCREMENT,
student_id INTEGER,
subject_id INTEGER,
enrollment_date TEXT NOT NULL,
FOREIGN KEY(student_id) REFERENCES Students(student_id),
FOREIGN KEY(subject_id) REFERENCES Subjects(subject_id)
);
CREATE TABLE IF NOT EXISTS Grades (
grade_id INTEGER PRIMARY KEY AUTOINCREMENT,
student_id INTEGER,
subject_id INTEGER,
grade TEXT NOT NULL,
grade_date TEXT NOT NULL,
FOREIGN KEY(student_id) REFERENCES Students(student_id),
FOREIGN KEY(subject_id) REFERENCES Subjects(subject_id)
);
""")
self.conn.commit()
def clean_up(self):
"""Delete all existing data from the database."""
self.cursor.executescript("""
DELETE FROM Grades;
DELETE FROM Enrollments;
DELETE FROM Subjects;
DELETE FROM Students;
""")
self.conn.commit()
def close(self):
"""Close the database connection."""
self.cursor.close()
self.conn.close()
```
To test this if it works, we will create a faker named `course_operations_faker.py`, inside `/db`
```
db
...
course_operations_faker.py
...
```
```python=
from faker import Faker
import random
from db.connection import get_connection
fake = Faker()
def create_sample_students(num_students=10):
conn = get_connection()
cursor = conn.cursor()
try:
for _ in range(num_students):
first_name = fake.first_name()
last_name = fake.last_name()
date_of_birth = fake.date_of_birth(minimum_age=14, maximum_age=18).strftime('%Y-%m-%d')
grade_level = fake.random_int(min=9, max=12)
cursor.execute("""
INSERT INTO Students (first_name, last_name, date_of_birth, grade_level)
VALUES (?, ?, ?, ?)
""", (first_name, last_name, date_of_birth, grade_level))
conn.commit()
finally:
cursor.close()
conn.close()
def create_sample_subjects():
conn = get_connection()
cursor = conn.cursor()
try:
subjects = ['Math', 'Science', 'English', 'History', 'Geography']
for subject in subjects:
cursor.execute("""
INSERT INTO Subjects (subject_name)
VALUES (?)
""", (subject,))
conn.commit()
finally:
cursor.close()
conn.close()
def create_sample_enrollments(num_students=10, num_subjects=5):
conn = get_connection()
cursor = conn.cursor()
try:
for student_id in range(1, num_students + 1):
for subject_id in range(1, num_subjects + 1):
cursor.execute("""
INSERT INTO Enrollments (student_id, subject_id, enrollment_date)
VALUES (?, ?, ?)
""", (student_id, subject_id, fake.date_this_decade().strftime('%Y-%m-%d')))
conn.commit()
finally:
cursor.close()
conn.close()
def create_sample_grades(num_students=10, num_subjects=5):
conn = get_connection()
cursor = conn.cursor()
try:
for student_id in range(1, num_students + 1):
for subject_id in range(1, num_subjects + 1):
grade = random.choice(['A', 'B', 'C', 'D'])
cursor.execute("""
INSERT INTO Grades (student_id, subject_id, grade, grade_date)
VALUES (?, ?, ?, ?)
""", (student_id, subject_id, grade, fake.date_this_decade().strftime('%Y-%m-%d')))
conn.commit()
finally:
cursor.close()
conn.close()
```
finally, we can connect our new logic to `main.py`
```python=
from db import (
create_sample_enrollments, create_sample_grades, create_sample_students, create_sample_subjects)
from models.course_sql_operations import CourseOperations
from flask import Flask
from routes import configure_routes
from config import Config
from db.student_queries import get_student_performance
# Initialize Flask app
app = Flask(__name__)
app.config.from_object(Config)
# Configure routes
configure_routes(app)
if __name__ == "__main__":
course_operations = CourseOperations()
course_operations.create_tables()
# course_operations.clean_up()
print("Populating database with sample data...")
create_sample_students(10)
create_sample_subjects()
create_sample_enrollments(10, 5)
create_sample_grades(10, 5)
print("Database with sample data created")
course_operations.close()
app.run(debug=True)
```
Run `python3 main.py`
If you see the following statement on the terminal
`Database with sample data created`
Everything worked 🎊
Next we need to create an SQL Query that will handle some key functionalities listed below [here](#features)
After which we will connect it to an endpoint to test if it works
Inside `db` folder, let's add a query handler for `SQL`.
### SQL Query
student_queries.py
```python=
def get_student_performance():
query = """
SELECT
s.first_name,
s.last_name,
s.grade_level,
sub.subject_name,
g.grade,
CASE
WHEN g.grade = 'A' THEN 1
WHEN g.grade = 'B' THEN 2
WHEN g.grade = 'C' THEN 3
WHEN g.grade = 'D' THEN 4
ELSE 5
END AS grade_rank
FROM
Students s
JOIN
Enrollments e ON s.student_id = e.student_id
JOIN
Subjects sub ON e.subject_id = sub.subject_id
LEFT JOIN
Grades g ON s.student_id = g.student_id AND sub.subject_id = g.subject_id
AND g.grade_date = (
SELECT MAX(grade_date)
FROM Grades
WHERE student_id = s.student_id
AND subject_id = sub.subject_id
)
ORDER BY
grade_rank, s.student_id, sub.subject_name;
"""
return query
```
### features
### 1. Handle cases where no grade is available for a student in a particular module
- The query uses a `LEFT JOIN` between Grades and the other tables, to ensure that students who do not have grades for specific subjects will still appear in the result with `NULL` for the grade.
- If a student has no grade in a subject, their grade column will contain `NULL`, and the ranking logic (based on grade) will be skipped.
### 2. Rank students based on their most recent grade for each module
- It uses `CASE` statement to map grades to numerical ranks ('A' = 1, 'B' = 2, 'C' = 3, 'D' = 4), which allows the students to be ranked based on their most recent grade.
- The query orders the results by `grade_rank`, so students with the best grades (e.g., A = 1) will be ranked higher.
- Use `ORDER BY` to ensure that the students are sorted by their grade rank first, then by their student ID and subject name.
Now in main.py, let's create a simple endpoint to test this logic
```python=
# ... Previous Logic
from db.connection import get_connection
@app.route("/api/v1/report", methods=["GET"])
def fetch_report():
"""Fetch and display the student performance report."""
conn = get_connection()
cursor = conn.cursor()
try:
query = get_student_performance()
cursor.execute(query)
results = cursor.fetchall()
report = [
{
"first_name": row["first_name"],
"last_name": row["last_name"],
"grade_level": row["grade_level"],
"subject_name": row["subject_name"],
"grade": row["grade"],
}
for row in results
]
return {"data": report}, 200
finally:
cursor.close()
conn.close()
if __name__ == "__main__":
#...Previous Logic
```
Run `Python3 main.py` incase your previous session closed.
Then navigate to
```
GET http://localhost:5000/api/v1/report
```
You should see a json response
Here is a snapshot of how it should look
```json
{
"data": [
{
"first_name": "Derek",
"grade": "A",
"grade_level": "11",
"last_name": "Thomas",
"subject_name": "History"
},
{
"first_name": "Derek",
"grade": "A",
"grade_level": "11",
"last_name": "Thomas",
"subject_name": "English"
},
{
"first_name": "Andrea",
"grade": "A",
"grade_level": "11",
"last_name": "Fox",
"subject_name": "English"
},
.......
```