Files
ShooterHub/blueprints/api/analyses.py

179 lines
5.1 KiB
Python

import base64
import io
from pathlib import Path
from flask import Blueprint, current_app, request
from flask_jwt_extended import jwt_required
from sqlalchemy import func, select
from extensions import db
from models import Analysis
from .utils import (
created, err, no_content, ok,
current_api_user, serialize_analysis,
)
analyses_bp = Blueprint("api_analyses", __name__, url_prefix="/analyses")
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _remove_analysis_files(analysis: Analysis, storage_root: str) -> None:
root = Path(storage_root)
for path_attr in ("csv_path", "pdf_path"):
rel = getattr(analysis, path_attr, None)
if rel:
try:
root.joinpath(rel).unlink(missing_ok=True)
except Exception:
pass
# ---------------------------------------------------------------------------
# Routes
# ---------------------------------------------------------------------------
@analyses_bp.post("/upload")
@jwt_required(optional=True)
def upload():
from analyzer.parser import parse_csv
from analyzer.grouper import detect_groups
from analyzer.stats import compute_overall_stats, compute_group_stats
from analyzer.charts import render_group_charts, render_overview_chart
from analyzer.pdf_report import generate_pdf
file = request.files.get("csv_file")
if not file or not file.filename:
return err("No csv_file provided.", 400)
try:
csv_bytes = file.read()
df = parse_csv(io.BytesIO(csv_bytes))
groups = detect_groups(df)
overall = compute_overall_stats(df)
group_stats = compute_group_stats(groups)
charts = render_group_charts(groups, y_min=overall["min_speed"], y_max=overall["max_speed"])
overview_chart = render_overview_chart(group_stats)
pdf_bytes = generate_pdf(overall, group_stats, charts, overview_chart)
except ValueError as e:
return err(str(e), 422)
pdf_b64 = base64.b64encode(pdf_bytes).decode()
saved_id = None
user = current_api_user()
if user:
from storage import save_analysis
saved_id = save_analysis(
user=user,
csv_bytes=csv_bytes,
pdf_bytes=pdf_bytes,
overall=overall,
group_stats=group_stats,
filename=file.filename or "upload.csv",
)
return ok({
"overall_stats": overall,
"group_stats": group_stats,
"charts": charts,
"overview_chart": overview_chart,
"pdf_b64": pdf_b64,
"saved_id": saved_id,
})
@analyses_bp.get("/")
@jwt_required()
def list_analyses():
user = current_api_user()
if not user:
return err("User not found.", 404)
try:
page = max(1, int(request.args.get("page", 1)))
per_page = min(100, max(1, int(request.args.get("per_page", 20))))
except (TypeError, ValueError):
page, per_page = 1, 20
total = db.session.scalar(
select(func.count()).select_from(Analysis)
.where(Analysis.user_id == user.id)
) or 0
analyses = db.session.scalars(
select(Analysis)
.where(Analysis.user_id == user.id)
.order_by(Analysis.created_at.desc())
.offset((page - 1) * per_page)
.limit(per_page)
).all()
return ok({
"data": [serialize_analysis(a) for a in analyses],
"total": total,
"page": page,
"per_page": per_page,
})
@analyses_bp.get("/<int:analysis_id>")
@jwt_required(optional=True)
def get_analysis(analysis_id: int):
a = db.session.get(Analysis, analysis_id)
if not a:
return err("Analysis not found.", 404)
user = current_api_user()
is_owner = user and a.user_id == user.id
if not a.is_public and not is_owner:
return err("Access denied.", 403)
return ok(serialize_analysis(a))
@analyses_bp.delete("/<int:analysis_id>")
@jwt_required()
def delete_analysis(analysis_id: int):
user = current_api_user()
if not user:
return err("User not found.", 404)
a = db.session.get(Analysis, analysis_id)
if not a:
return err("Analysis not found.", 404)
if a.user_id != user.id:
return err("Access denied.", 403)
storage_root = current_app.config["STORAGE_ROOT"]
_remove_analysis_files(a, storage_root)
db.session.delete(a)
db.session.commit()
return no_content()
@analyses_bp.patch("/<int:analysis_id>/visibility")
@jwt_required()
def toggle_visibility(analysis_id: int):
user = current_api_user()
if not user:
return err("User not found.", 404)
a = db.session.get(Analysis, analysis_id)
if not a:
return err("Analysis not found.", 404)
if a.user_id != user.id:
return err("Access denied.", 403)
body = request.get_json(silent=True) or {}
if "is_public" not in body:
return err("is_public field is required.", 400)
a.is_public = bool(body["is_public"])
db.session.commit()
return ok(serialize_analysis(a))