from fastapi import FastAPI, HTTPException, Depends from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse, StreamingResponse from contextlib import asynccontextmanager import os from pathlib import Path from io import StringIO import csv from typing import Optional from config import settings from database import connect_to_mongo, close_mongo_connection, get_database from routes import auth_routes, subject_routes, grade_routes, period_routes, teacher_grade_routes from routes.auth_routes import get_current_user from models import UserInDB @asynccontextmanager async def lifespan(app: FastAPI): """Application lifespan events""" # Startup await connect_to_mongo() yield # Shutdown await close_mongo_connection() app = FastAPI( title="Grademaxxing API", description="Grade management system API", version="1.0.0", lifespan=lifespan ) # CORS middleware app.add_middleware( CORSMiddleware, allow_origins=settings.cors_origins_list, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # API routes app.include_router(auth_routes.router, prefix="/api") app.include_router(subject_routes.router, prefix="/api") app.include_router(grade_routes.router, prefix="/api") app.include_router(period_routes.router, prefix="/api") app.include_router(teacher_grade_routes.router, prefix="/api") @app.get("/api/health") async def health_check(): """Health check endpoint""" return {"status": "healthy", "version": "1.0.0"} @app.get("/api/export/grades") async def export_grades_csv( period_id: Optional[str] = None, current_user: UserInDB = Depends(get_current_user) ): """Export all grades as CSV, optionally filtered by period""" db = get_database() # Get all subjects for the user subjects_cursor = db.subjects.find({"user_id": current_user.id}) subjects = await subjects_cursor.to_list(length=None) subject_dict = {str(subject["_id"]): subject for subject in subjects} # Build grade query grade_query = {"user_id": current_user.id} # If period_id is provided, filter grades by date range if period_id: period = await db.periods.find_one({"_id": period_id, "user_id": current_user.id}) if period: grade_query["date"] = { "$gte": period["start_date"], "$lte": period["end_date"] } # Get all grades grades_cursor = db.grades.find(grade_query).sort("date", -1) grades = await grades_cursor.to_list(length=None) # Create CSV in memory output = StringIO() writer = csv.writer(output) # Write header writer.writerow([ "Subject", "Category", "Grade Name", "Grade", "Max Grade", "Percentage", "Weight in Category", "Date", "Notes" ]) # Write grade rows for grade in grades: subject = subject_dict.get(grade["subject_id"]) subject_name = subject["name"] if subject else "Unknown Subject" # Calculate percentage percentage = (grade["grade"] / grade["max_grade"] * 100) if grade["max_grade"] > 0 else 0 writer.writerow([ subject_name, grade.get("category_name", ""), grade.get("name", ""), grade["grade"], grade["max_grade"], f"{percentage:.2f}%", grade.get("weight_in_category", 1.0), grade["date"].strftime("%Y-%m-%d") if "date" in grade else "", grade.get("notes", "") ]) # Prepare response output.seek(0) filename = f"grades_export_{current_user.username}.csv" if period_id: period = await db.periods.find_one({"_id": period_id, "user_id": current_user.id}) if period: filename = f"grades_{period['name'].replace(' ', '_')}.csv" return StreamingResponse( iter([output.getvalue()]), media_type="text/csv", headers={"Content-Disposition": f"attachment; filename={filename}"} ) # Serve static files (React SPA) static_dir = Path(__file__).parent.parent / "build" / "client" if static_dir.exists(): app.mount("/assets", StaticFiles(directory=static_dir / "assets"), name="assets") @app.get("/{full_path:path}") async def serve_spa(full_path: str): """Serve React SPA for all non-API routes""" # Check if the file exists file_path = static_dir / full_path if file_path.is_file(): return FileResponse(file_path) # For all other routes, serve index.html (SPA routing) index_path = static_dir / "index.html" if index_path.exists(): return FileResponse(index_path) raise HTTPException(status_code=404, detail="Not found") else: print(f"Warning: Static directory not found at {static_dir}") print("Run the frontend build first to serve the UI") if __name__ == "__main__": import uvicorn uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)