Skip to content

Commit 83d5701

Browse files
orabeCopilot
andcommitted
Implement student management features: add Student model, CRUD operations, and update certificate handling
Co-authored-by: Copilot <copilot@github.com>
1 parent e3fa581 commit 83d5701

11 files changed

Lines changed: 292 additions & 20 deletions

File tree

backend/app/crud.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,39 @@ def generate_certificate_id(year: int) -> str:
1010
def get_certificate_by_public_id(db: Session, certificate_id: str):
1111
return db.query(models.Certificate).filter(models.Certificate.certificate_id == certificate_id).first()
1212

13+
14+
def get_student_by_id(db: Session, student_id: str):
15+
return db.query(models.Student).filter(models.Student.student_id == student_id).first()
16+
17+
18+
def get_or_create_student(db: Session, student_id: str, student_name: str):
19+
student = get_student_by_id(db, student_id)
20+
if student:
21+
if student_name and student.student_name != student_name:
22+
student.student_name = student_name
23+
db.commit()
24+
db.refresh(student)
25+
return student
26+
27+
student = models.Student(student_id=student_id, student_name=student_name)
28+
db.add(student)
29+
db.commit()
30+
db.refresh(student)
31+
return student
32+
1333
def create_certificate(db: Session, cert_in: schemas.CertificateCreate):
1434
year = datetime.utcnow().year
1535

36+
get_or_create_student(db, cert_in.student_id, cert_in.student_name)
37+
1638
while True:
1739
cert_id = generate_certificate_id(year)
1840
if not get_certificate_by_public_id(db, cert_id):
1941
break
2042

2143
cert = models.Certificate(
2244
certificate_id=cert_id,
45+
student_id=cert_in.student_id,
2346
student_name=cert_in.student_name,
2447
course_title=cert_in.course_title,
2548
completion_date=cert_in.completion_date,
@@ -55,3 +78,17 @@ def delete_certificate(db: Session, certificate_id: str):
5578
db.delete(cert)
5679
db.commit()
5780
return cert
81+
82+
83+
def delete_certificates_by_student_id(db: Session, student_id: str) -> int:
84+
"""Delete all certificates belonging to a given student_id. Returns number deleted."""
85+
certs = db.query(models.Certificate).filter(models.Certificate.student_id == student_id).all()
86+
if not certs:
87+
return 0
88+
89+
count = len(certs)
90+
for cert in certs:
91+
db.delete(cert)
92+
93+
db.commit()
94+
return count

backend/app/database.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626

2727

2828
def ensure_certificate_schema(bind=None):
29-
"""Create the certificates table and add any missing columns.
29+
"""Create the student and certificates tables and add any missing columns.
3030
3131
This keeps older PostgreSQL/SQLite databases compatible when the
3232
certificate model gains new optional fields.
@@ -37,12 +37,28 @@ def ensure_certificate_schema(bind=None):
3737
Base.metadata.create_all(bind=target)
3838

3939
inspector = inspect(target)
40+
if "students" not in inspector.get_table_names():
41+
return
42+
4043
if "certificates" not in inspector.get_table_names():
4144
return
4245

46+
student_columns = {column["name"] for column in inspector.get_columns("students")}
4347
existing_columns = {column["name"] for column in inspector.get_columns("certificates")}
4448
missing_statements = []
4549

50+
if "student_id" not in student_columns:
51+
missing_statements.append("ALTER TABLE students ADD COLUMN student_id VARCHAR")
52+
if "student_name" not in student_columns:
53+
missing_statements.append("ALTER TABLE students ADD COLUMN student_name VARCHAR")
54+
if "created_at" not in student_columns:
55+
missing_statements.append("ALTER TABLE students ADD COLUMN created_at TIMESTAMP")
56+
if "updated_at" not in student_columns:
57+
missing_statements.append("ALTER TABLE students ADD COLUMN updated_at TIMESTAMP")
58+
59+
if "student_id" not in existing_columns:
60+
missing_statements.append("ALTER TABLE certificates ADD COLUMN student_id VARCHAR")
61+
4662
if "attendance_percentage" not in existing_columns:
4763
missing_statements.append("ALTER TABLE certificates ADD COLUMN attendance_percentage INTEGER")
4864
if "assignment_completion_percentage" not in existing_columns:

backend/app/main.py

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ def verify_certificate(certificate_id: str, db: Session = Depends(get_db)):
6868
return {
6969
"status": "valid",
7070
"certificate_id": cert.certificate_id,
71+
"student_id": cert.student_id,
7172
"student_name": cert.student_name,
7273
"course_title": cert.course_title,
7374
"course_link": cert.course_link,
@@ -95,20 +96,69 @@ def list_certificates(
9596
api_key: str = Depends(security.verify_api_key)
9697
):
9798
certs = db.query(models.Certificate).all()
98-
return certs
99+
return [
100+
{
101+
"certificate_id": cert.certificate_id,
102+
"student_id": cert.student_id,
103+
"student_name": cert.student_name,
104+
"course_title": cert.course_title,
105+
"course_link": cert.course_link,
106+
"completion_date": cert.completion_date,
107+
"duration_hours": cert.duration_hours,
108+
"attendance_percentage": cert.attendance_percentage,
109+
"assignment_completion_percentage": cert.assignment_completion_percentage,
110+
"course_level": cert.course_level,
111+
"course_format": cert.course_format,
112+
"instruction_language": cert.instruction_language,
113+
"issuer": cert.issuer,
114+
"instructor": cert.instructor,
115+
"created_at": cert.created_at,
116+
"updated_at": cert.updated_at,
117+
}
118+
for cert in certs
119+
]
99120

100121

101-
@app.delete("/admin/certificates/{certificate_id}", response_model=schemas.CertificateDeleteResponse)
102-
def delete_certificate(
103-
certificate_id: str,
122+
@app.get("/admin/students/{student_id}", response_model=schemas.StudentLookupResponse)
123+
def get_student_by_student_id(
124+
student_id: str,
104125
db: Session = Depends(get_db),
105126
api_key: str = Depends(security.verify_api_key)
106127
):
107-
cert = crud.delete_certificate(db, certificate_id)
108-
if not cert:
109-
raise HTTPException(status_code=404, detail="Certificate not found")
128+
student = crud.get_student_by_id(db, student_id)
129+
if not student:
130+
raise HTTPException(status_code=404, detail="Student not found")
131+
132+
certificates = (
133+
db.query(models.Certificate)
134+
.filter(models.Certificate.student_id == student_id)
135+
.order_by(models.Certificate.created_at.desc())
136+
.all()
137+
)
138+
139+
return {
140+
"student": {
141+
"student_id": student.student_id,
142+
"student_name": student.student_name,
143+
"created_at": student.created_at,
144+
"updated_at": student.updated_at,
145+
},
146+
"certificates": certificates,
147+
}
148+
149+
150+
@app.delete("/admin/certificates/student/{student_id}", response_model=schemas.CertificateDeleteResponse)
151+
def delete_certificates_for_student(
152+
student_id: str,
153+
db: Session = Depends(get_db),
154+
api_key: str = Depends(security.verify_api_key)
155+
):
156+
deleted_count = crud.delete_certificates_by_student_id(db, student_id)
157+
if deleted_count == 0:
158+
raise HTTPException(status_code=404, detail="No certificates found for student")
110159

111160
return {
112-
"message": "Certificate deleted",
113-
"certificate_id": certificate_id,
161+
"message": "Certificates deleted for student",
162+
"student_id": student_id,
163+
"deleted_count": deleted_count,
114164
}

backend/app/models.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
1-
from sqlalchemy import Column, Integer, String, DateTime
1+
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey
2+
from sqlalchemy.orm import relationship
23
from sqlalchemy.sql import func
34
from .database import Base
45

6+
class Student(Base):
7+
__tablename__ = "students"
8+
student_id = Column(String, primary_key=True, index=True, nullable=False)
9+
student_name = Column(String, nullable=False)
10+
created_at = Column(DateTime(timezone=True), server_default=func.now())
11+
updated_at = Column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now())
12+
513
class Certificate(Base):
614
__tablename__ = "certificates"
715
id = Column(Integer, primary_key=True, index=True)
816
certificate_id = Column(String, unique=True, index=True, nullable=False)
17+
student_id = Column(String, ForeignKey("students.student_id"), index=True, nullable=False)
918
student_name = Column(String, nullable=False)
1019
course_title = Column(String, nullable=False)
1120
completion_date = Column(String, nullable=False)
@@ -19,6 +28,7 @@ class Certificate(Base):
1928

2029
issuer = Column(String, default="MathCodeLab", nullable=False)
2130
instructor = Column(String, default="Mohammad Orabe", nullable=False)
31+
student = relationship("Student")
2232
created_at = Column(DateTime(timezone=True), server_default=func.now())
2333
updated_at = Column(DateTime(timezone=True), onupdate=func.now(), server_default=func.now())
2434

backend/app/schemas.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from typing import Optional
33

44
class CertificateBase(BaseModel):
5+
student_id: str
56
student_name: str
67
course_title: str
78
completion_date: str
@@ -22,12 +23,24 @@ class CertificateOut(CertificateBase):
2223
certificate_id: str
2324
class Config:
2425
from_attributes = True
25-
course_link: Optional[str] = None
26+
27+
28+
class StudentOut(BaseModel):
29+
student_id: str
30+
student_name: str
31+
created_at: Optional[str] = None
32+
updated_at: Optional[str] = None
33+
34+
35+
class StudentLookupResponse(BaseModel):
36+
student: StudentOut
37+
certificates: list[CertificateOut]
2638

2739
class CertificateVerificationResponse(BaseModel):
2840
status: str
2941
certificate_id: str
3042
verification_url: Optional[str] = None
43+
student_id: Optional[str] = None
3144
student_name: Optional[str] = None
3245
course_title: Optional[str] = None
3346
course_link: Optional[str] = None
@@ -46,6 +59,7 @@ class CertificateVerificationResponse(BaseModel):
4659

4760
class CertificateDeleteResponse(BaseModel):
4861
message: str
49-
certificate_id: str
62+
student_id: str
63+
deleted_count: int
5064

5165
# Revocation support removed: no revoke schema

backend/scripts/create_certificate.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import sys
22
import os
33
from datetime import datetime
4-
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
4+
from dotenv import load_dotenv
5+
6+
PROJECT_ROOT = os.path.dirname(os.path.dirname(__file__))
7+
load_dotenv(os.path.join(PROJECT_ROOT, ".env"))
8+
sys.path.append(PROJECT_ROOT)
9+
510
from app import database, models, crud, schemas
611
from sqlalchemy.orm import Session
712

@@ -39,6 +44,7 @@ def main():
3944
db = next(database.get_db())
4045
print("Enter certificate details:")
4146

47+
student_id = input("Student ID (primary key, e.g. STU-2026-001): ").strip()
4248
student_name = input("Student name: ")
4349
course_title = input("Course title: ")
4450
completion_date = prompt_date("Completion date (YYYY-MM-DD): ")
@@ -50,6 +56,7 @@ def main():
5056
instruction_language = input("Instruction language (e.g. English, German, Arabic): ")
5157
course_link = input("Course link (optional, e.g. https://...): ").strip() or None
5258
cert_in = schemas.CertificateCreate(
59+
student_id=student_id,
5360
student_name=student_name,
5461
course_title=course_title,
5562
completion_date=completion_date,
@@ -58,8 +65,7 @@ def main():
5865
assignment_completion_percentage=assignment_completion_percentage,
5966
course_level=course_level,
6067
course_format=course_format,
61-
instruction_language=instruction_language
62-
,
68+
instruction_language=instruction_language,
6369
course_link=course_link
6470
)
6571
cert = crud.create_certificate(db, cert_in)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import argparse
2+
import os
3+
import sys
4+
5+
from dotenv import load_dotenv
6+
from sqlalchemy.orm import Session
7+
8+
PROJECT_ROOT = os.path.dirname(os.path.dirname(__file__))
9+
load_dotenv(os.path.join(PROJECT_ROOT, ".env"))
10+
sys.path.append(PROJECT_ROOT)
11+
12+
from app import crud, database
13+
14+
15+
def remove_certificates_by_student(student_id: str):
16+
database.ensure_certificate_schema()
17+
db: Session = next(database.get_db())
18+
try:
19+
deleted_count = crud.delete_certificates_by_student_id(db, student_id)
20+
if deleted_count == 0:
21+
print(f"No certificates found for student_id: {student_id}")
22+
return 1
23+
24+
print(f"Deleted {deleted_count} certificate(s) for student_id: {student_id}")
25+
return 0
26+
except Exception as exc:
27+
print("Error while deleting certificates:")
28+
print(exc)
29+
return 1
30+
finally:
31+
db.close()
32+
33+
34+
def main():
35+
parser = argparse.ArgumentParser(description="Delete certificates by student_id (bulk)")
36+
parser.add_argument("student_id", help="Student ID, e.g. S12345")
37+
args = parser.parse_args()
38+
39+
raise SystemExit(remove_certificates_by_student(args.student_id.strip()))
40+
41+
42+
if __name__ == "__main__":
43+
main()

backend/scripts/seed_demo_data.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ def seed():
99
db = next(database.get_db())
1010
# Valid certificate
1111
cert1 = schemas.CertificateCreate(
12+
student_id="STU-2026-001",
1213
student_name="Alice Example",
1314
course_title="Python Programming Basics",
1415
completion_date="2026-04-20",
@@ -17,6 +18,7 @@ def seed():
1718
c1 = crud.create_certificate(db, cert1)
1819
# Another demo certificate
1920
cert2 = schemas.CertificateCreate(
21+
student_id="STU-2026-002",
2022
student_name="Bob Example",
2123
course_title="Data Science Intro",
2224
completion_date="2026-03-15",

backend/scripts/view_database.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,18 @@
44
from dotenv import load_dotenv
55
from sqlalchemy.orm import Session
66

7-
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
7+
PROJECT_ROOT = os.path.dirname(os.path.dirname(__file__))
8+
load_dotenv(os.path.join(PROJECT_ROOT, ".env"))
9+
sys.path.append(PROJECT_ROOT)
810

11+
from app import database
912
from app.database import SessionLocal
1013
from app.models import Certificate
1114

1215

13-
load_dotenv()
14-
15-
1616
def view_database(limit: int = 50):
17+
# Ensure missing optional columns are added before selecting all model fields.
18+
database.ensure_certificate_schema()
1719
db: Session = SessionLocal()
1820
try:
1921
certificates = (
@@ -32,6 +34,7 @@ def view_database(limit: int = 50):
3234
for cert in certificates:
3335
print("=" * 50)
3436
print(f"Certificate ID : {cert.certificate_id}")
37+
print(f"Student ID : {cert.student_id}")
3538
print(f"Student Name : {cert.student_name}")
3639
print(f"Course Title : {cert.course_title}")
3740
print(f"Completion Date: {cert.completion_date}")

0 commit comments

Comments
 (0)