from fastapi import APIRouter, Depends, HTTPException, status, Query from typing import List, Optional from models import GradeCreate, GradeUpdate, GradeResponse, GradeInDB, UserInDB from database import get_database from routes.auth_routes import get_current_user from bson import ObjectId from datetime import datetime router = APIRouter(prefix="/grades", tags=["grades"]) @router.post("", response_model=GradeResponse, status_code=status.HTTP_201_CREATED) async def create_grade( grade: GradeCreate, current_user: UserInDB = Depends(get_current_user) ): """Create a new grade/test entry""" db = get_database() # Verify subject exists and belongs to user if not ObjectId.is_valid(grade.subject_id): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid subject ID" ) subject = await db.subjects.find_one({ "_id": ObjectId(grade.subject_id), "user_id": current_user.id }) if not subject: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Subject not found" ) # Verify category exists in subject category_names = [cat["name"] for cat in subject.get("grading_categories", [])] if grade.category_name not in category_names: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Category '{grade.category_name}' not found in subject. Available: {', '.join(category_names)}" ) grade_in_db = GradeInDB( **grade.model_dump(), user_id=current_user.id ) grade_dict = grade_in_db.model_dump(by_alias=True) grade_dict["_id"] = ObjectId(grade_dict["_id"]) result = await db.grades.insert_one(grade_dict) grade_dict["_id"] = str(result.inserted_id) return GradeResponse(**grade_dict) @router.get("", response_model=List[GradeResponse]) async def get_grades( subject_id: Optional[str] = Query(None), current_user: UserInDB = Depends(get_current_user) ): """Get all grades for the current user, optionally filtered by subject""" db = get_database() query = {"user_id": current_user.id} if subject_id: if not ObjectId.is_valid(subject_id): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid subject ID" ) query["subject_id"] = subject_id cursor = db.grades.find(query).sort("date", -1) # Most recent first grades = await cursor.to_list(length=None) for grade in grades: grade["_id"] = str(grade["_id"]) return [GradeResponse(**grade) for grade in grades] @router.get("/{grade_id}", response_model=GradeResponse) async def get_grade( grade_id: str, current_user: UserInDB = Depends(get_current_user) ): """Get a specific grade""" db = get_database() if not ObjectId.is_valid(grade_id): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid grade ID" ) grade = await db.grades.find_one({ "_id": ObjectId(grade_id), "user_id": current_user.id }) if not grade: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Grade not found" ) grade["_id"] = str(grade["_id"]) return GradeResponse(**grade) @router.put("/{grade_id}", response_model=GradeResponse) async def update_grade( grade_id: str, grade_update: GradeUpdate, current_user: UserInDB = Depends(get_current_user) ): """Update a grade""" db = get_database() if not ObjectId.is_valid(grade_id): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid grade ID" ) # Check if grade exists and belongs to user existing_grade = await db.grades.find_one({ "_id": ObjectId(grade_id), "user_id": current_user.id }) if not existing_grade: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Grade not found" ) # If updating category, verify it exists in subject if grade_update.category_name: subject = await db.subjects.find_one({ "_id": ObjectId(existing_grade["subject_id"]), "user_id": current_user.id }) if subject: category_names = [cat["name"] for cat in subject.get("grading_categories", [])] if grade_update.category_name not in category_names: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Category '{grade_update.category_name}' not found in subject" ) # Update only provided fields update_data = grade_update.model_dump(exclude_unset=True) if update_data: update_data["updated_at"] = datetime.utcnow() await db.grades.update_one( {"_id": ObjectId(grade_id)}, {"$set": update_data} ) # Fetch updated grade updated_grade = await db.grades.find_one({"_id": ObjectId(grade_id)}) updated_grade["_id"] = str(updated_grade["_id"]) return GradeResponse(**updated_grade) @router.delete("/{grade_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_grade( grade_id: str, current_user: UserInDB = Depends(get_current_user) ): """Delete a grade""" db = get_database() if not ObjectId.is_valid(grade_id): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid grade ID" ) # Check if grade exists and belongs to user grade = await db.grades.find_one({ "_id": ObjectId(grade_id), "user_id": current_user.id }) if not grade: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Grade not found" ) # Delete grade await db.grades.delete_one({"_id": ObjectId(grade_id)}) return None