169 lines
5.1 KiB
Python
169 lines
5.1 KiB
Python
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)
|