first commit
This commit is contained in:
168
backend/main.py
Normal file
168
backend/main.py
Normal file
@@ -0,0 +1,168 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user