from typing import Any, List, Optional from datetime import datetime from fastapi import APIRouter, Depends, HTTPException, Path, Response, status from sqlalchemy.orm import Session from app import crud, models, schemas from app.api.v1.deps import get_db, get_current_active_user, get_teacher_user router = APIRouter() @router.get("/", response_model=List[schemas.ExamResult]) def read_exam_results( db: Session = Depends(get_db), skip: int = 0, limit: int = 100, exam_id: Optional[int] = None, student_id: Optional[int] = None, current_user: models.User = Depends(get_current_active_user), ) -> Any: """ Retrieve exam results. """ # Check permissions is_admin = any(role.name == "admin" for role in db.query(models.Role).all() if role.id == current_user.role_id) is_teacher = db.query(models.Teacher).filter(models.Teacher.user_id == current_user.id).first() is not None # Students can only see their own results if not (is_admin or is_teacher): student = db.query(models.Student).filter(models.Student.user_id == current_user.id).first() if not student: raise HTTPException( status_code=403, detail="You don't have permission to view these exam results", ) student_id = student.id # Apply filters if exam_id and student_id: exam_result = crud.exam_result.get_by_student_and_exam( db, student_id=student_id, exam_id=exam_id ) return [exam_result] if exam_result else [] elif exam_id: return crud.exam_result.get_by_exam(db, exam_id=exam_id) elif student_id: return crud.exam_result.get_by_student(db, student_id=student_id) else: return crud.exam_result.get_multi(db, skip=skip, limit=limit) @router.post("/", response_model=schemas.ExamResult) def create_exam_result( *, db: Session = Depends(get_db), exam_result_in: schemas.ExamResultCreate, current_user: models.User = Depends(get_current_active_user), ) -> Any: """ Create new exam result (start an exam). """ # Check if the exam exists exam = crud.exam.get(db=db, id=exam_result_in.exam_id) if not exam: raise HTTPException( status_code=404, detail=f"Exam with ID {exam_result_in.exam_id} not found", ) # Check if the student exists student = crud.student.get(db=db, id=exam_result_in.student_id) if not student: raise HTTPException( status_code=404, detail=f"Student with ID {exam_result_in.student_id} not found", ) # Check if the current user is the student or has permission is_admin = any(role.name == "admin" for role in db.query(models.Role).all() if role.id == current_user.role_id) is_teacher = db.query(models.Teacher).filter(models.Teacher.user_id == current_user.id).first() is not None if not (is_admin or is_teacher): if student.user_id != current_user.id: raise HTTPException( status_code=403, detail="You don't have permission to start an exam for this student", ) # Check if the exam is available now = datetime.utcnow() if not exam.is_active: raise HTTPException( status_code=400, detail="This exam is not active", ) if exam.start_time and exam.start_time > now: raise HTTPException( status_code=400, detail="This exam is not yet available", ) if exam.end_time and exam.end_time < now: raise HTTPException( status_code=400, detail="This exam has expired", ) # Check if there's already an active attempt existing_attempt = crud.exam_result.get_active_exam_attempt( db, student_id=student.id, exam_id=exam.id ) if existing_attempt: raise HTTPException( status_code=400, detail="There is already an active attempt for this exam", ) # Create the exam result exam_result = crud.exam_result.create(db=db, obj_in=exam_result_in) return exam_result @router.get("/{result_id}", response_model=schemas.ExamResultWithAnswers) def read_exam_result( *, db: Session = Depends(get_db), result_id: int = Path(..., title="The ID of the exam result to get"), current_user: models.User = Depends(get_current_active_user), ) -> Any: """ Get exam result by ID with answers. """ exam_result = crud.exam_result.get(db=db, id=result_id) if not exam_result: raise HTTPException( status_code=404, detail="Exam result not found", ) # Check permissions is_admin = any(role.name == "admin" for role in db.query(models.Role).all() if role.id == current_user.role_id) is_teacher = db.query(models.Teacher).filter(models.Teacher.user_id == current_user.id).first() is not None # Students can only see their own results if not (is_admin or is_teacher): student = db.query(models.Student).filter(models.Student.user_id == current_user.id).first() if not student or student.id != exam_result.student_id: raise HTTPException( status_code=403, detail="You don't have permission to view this exam result", ) # Get answers for this exam result answers = crud.student_answer.get_by_exam_result(db=db, exam_result_id=result_id) # Create a combined response result = schemas.ExamResultWithAnswers.from_orm(exam_result) result.answers = answers return result @router.put("/{result_id}", response_model=schemas.ExamResult) def update_exam_result( *, db: Session = Depends(get_db), result_id: int = Path(..., title="The ID of the exam result to update"), result_in: schemas.ExamResultUpdate, current_user: models.User = Depends(get_current_active_user), ) -> Any: """ Update an exam result. """ exam_result = crud.exam_result.get(db=db, id=result_id) if not exam_result: raise HTTPException( status_code=404, detail="Exam result not found", ) # Check permissions is_admin = any(role.name == "admin" for role in db.query(models.Role).all() if role.id == current_user.role_id) is_teacher = db.query(models.Teacher).filter(models.Teacher.user_id == current_user.id).first() is not None # Only teacher and admin can update results directly if not (is_admin or is_teacher): raise HTTPException( status_code=403, detail="You don't have permission to update this exam result", ) exam_result = crud.exam_result.update(db=db, db_obj=exam_result, obj_in=result_in) return exam_result @router.post("/{result_id}/complete", response_model=schemas.ExamResult) def complete_exam( *, db: Session = Depends(get_db), result_id: int = Path(..., title="The ID of the exam result to complete"), current_user: models.User = Depends(get_current_active_user), ) -> Any: """ Complete an exam and calculate the score. """ exam_result = crud.exam_result.get(db=db, id=result_id) if not exam_result: raise HTTPException( status_code=404, detail="Exam result not found", ) # Check if already completed if exam_result.is_completed: raise HTTPException( status_code=400, detail="This exam is already completed", ) # Check permissions is_admin = any(role.name == "admin" for role in db.query(models.Role).all() if role.id == current_user.role_id) is_teacher = db.query(models.Teacher).filter(models.Teacher.user_id == current_user.id).first() is not None # Students can only complete their own exams if not (is_admin or is_teacher): student = db.query(models.Student).filter(models.Student.user_id == current_user.id).first() if not student or student.id != exam_result.student_id: raise HTTPException( status_code=403, detail="You don't have permission to complete this exam", ) # Get the exam details exam = crud.exam.get(db=db, id=exam_result.exam_id) if not exam: raise HTTPException( status_code=404, detail="Exam not found", ) # Get all questions for this exam questions = crud.question.get_by_exam_id(db=db, exam_id=exam.id) # Calculate the maximum possible score max_score = sum(q.points for q in questions) # Get all student answers for this exam answers = crud.student_answer.get_by_exam_result(db=db, exam_result_id=exam_result.id) # Calculate the score score = sum(a.points_earned for a in answers) # Complete the exam exam_result = crud.exam_result.complete_exam( db=db, db_obj=exam_result, score=score, max_score=max_score ) return exam_result @router.delete("/{result_id}", response_model=None, status_code=status.HTTP_204_NO_CONTENT) def delete_exam_result( *, db: Session = Depends(get_db), result_id: int = Path(..., title="The ID of the exam result to delete"), current_user: models.User = Depends(get_teacher_user), ) -> Any: """ Delete an exam result. """ exam_result = crud.exam_result.get(db=db, id=result_id) if not exam_result: raise HTTPException( status_code=404, detail="Exam result not found", ) # Only admin can delete results is_admin = any(role.name == "admin" for role in db.query(models.Role).all() if role.id == current_user.role_id) if not is_admin: raise HTTPException( status_code=403, detail="Only administrators can delete exam results", ) crud.exam_result.remove(db=db, id=result_id) return Response(status_code=status.HTTP_204_NO_CONTENT)