import io import json from pathlib import Path from flask import ( Blueprint, abort, current_app, flash, redirect, request, render_template, send_from_directory, url_for, ) from flask_babel import _ from flask_login import current_user, login_required from extensions import db from models import Analysis analyses_bp = Blueprint("analyses", __name__, url_prefix="/analyses") def _can_view(analysis: Analysis) -> bool: if analysis.is_public: return True return current_user.is_authenticated and analysis.user_id == current_user.id @analyses_bp.route("/") def detail(analysis_id: int): a = db.session.get(Analysis, analysis_id) if a is None: abort(404) if not _can_view(a): abort(403) # Re-generate charts from the stored CSV storage_root = current_app.config["STORAGE_ROOT"] csv_path = Path(storage_root) / a.csv_path if not csv_path.exists(): abort(410) # CSV was deleted from analyzer.parser import parse_csv from analyzer.grouper import detect_groups from analyzer.stats import compute_group_stats from analyzer.charts import render_group_charts, render_overview_chart csv_bytes = csv_path.read_bytes() df = parse_csv(io.BytesIO(csv_bytes)) groups = detect_groups(df) group_stats = compute_group_stats(groups) charts = render_group_charts( groups, y_min=a.overall_stats["min_speed"], y_max=a.overall_stats["max_speed"], ) overview_chart = render_overview_chart(group_stats) groups_display = list(zip(group_stats, charts)) return render_template( "analyses/detail.html", analysis=a, overall=a.overall_stats, groups_display=groups_display, overview_chart=overview_chart, has_pdf=bool(a.pdf_path and (Path(storage_root) / a.pdf_path).exists()), ) @analyses_bp.route("//delete", methods=["POST"]) @login_required def delete(analysis_id: int): a = db.session.get(Analysis, analysis_id) if a is None: abort(404) if a.user_id != current_user.id: abort(403) back = url_for("sessions.detail", session_id=a.session_id) if a.session_id \ else url_for("dashboard.index") storage_root = current_app.config["STORAGE_ROOT"] for path_attr in ("csv_path", "pdf_path"): rel = getattr(a, path_attr, None) if rel: try: (Path(storage_root) / rel).unlink(missing_ok=True) except Exception: pass db.session.delete(a) db.session.commit() flash(_("Analysis deleted."), "success") return redirect(back) @analyses_bp.route("//rename", methods=["POST"]) @login_required def rename(analysis_id: int): a = db.session.get(Analysis, analysis_id) if a is None: abort(404) if a.user_id != current_user.id: abort(403) title = request.form.get("title", "").strip() if title: a.title = title[:255] db.session.commit() flash(_("Title updated."), "success") back = (url_for("sessions.detail", session_id=a.session_id) if a.session_id else url_for("analyses.detail", analysis_id=a.id)) return redirect(back) @analyses_bp.route("//regroup", methods=["POST"]) @login_required def regroup(analysis_id: int): a = db.session.get(Analysis, analysis_id) if a is None: abort(404) if a.user_id != current_user.id: abort(403) try: outlier_factor = float(request.form.get("outlier_factor", 5)) outlier_factor = max(1.0, min(20.0, outlier_factor)) except (TypeError, ValueError): outlier_factor = 5.0 manual_splits_raw = request.form.get("manual_splits", "").strip() manual_splits = None if manual_splits_raw: try: parsed = json.loads(manual_splits_raw) if isinstance(parsed, list): manual_splits = [int(x) for x in parsed] except (json.JSONDecodeError, ValueError, TypeError): pass storage_root = current_app.config["STORAGE_ROOT"] csv_path = Path(storage_root) / a.csv_path if not csv_path.exists(): abort(410) 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 from storage import _to_python csv_bytes = csv_path.read_bytes() df = parse_csv(io.BytesIO(csv_bytes)) groups = detect_groups(df, outlier_factor=outlier_factor, manual_splits=manual_splits) overall = compute_overall_stats(df) group_stats = compute_group_stats(groups) # Preserve existing notes old_stats = a.group_stats or [] for i, gs in enumerate(group_stats): if i < len(old_stats) and old_stats[i].get("note"): gs["note"] = old_stats[i]["note"] 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) if a.pdf_path: pdf_path = Path(storage_root) / a.pdf_path try: pdf_path.write_bytes(pdf_bytes) except Exception: pass a.grouping_outlier_factor = outlier_factor a.grouping_manual_splits = manual_splits a.group_stats = _to_python(group_stats) a.overall_stats = _to_python(overall) a.shot_count = int(overall.get("count", 0)) a.group_count = len(group_stats) db.session.commit() flash(_("Regrouped."), "success") back = (url_for("sessions.detail", session_id=a.session_id) if a.session_id else url_for("analyses.detail", analysis_id=a.id)) return redirect(back) @analyses_bp.route("//groups//note", methods=["POST"]) @login_required def save_group_note(analysis_id: int, group_index: int): a = db.session.get(Analysis, analysis_id) if a is None: abort(404) if a.user_id != current_user.id: abort(403) note = request.form.get("note", "").strip() group_stats = list(a.group_stats or []) if 0 <= group_index < len(group_stats): group_stats[group_index] = dict(group_stats[group_index]) group_stats[group_index]["note"] = note or None a.group_stats = group_stats db.session.commit() flash(_("Note saved."), "success") back = (url_for("sessions.detail", session_id=a.session_id) if a.session_id else url_for("analyses.detail", analysis_id=a.id)) return redirect(back) @analyses_bp.route("//pdf") def download_pdf(analysis_id: int): a = db.session.get(Analysis, analysis_id) if a is None: abort(404) if not _can_view(a): abort(403) if not a.pdf_path: abort(404) storage_root = current_app.config["STORAGE_ROOT"] pdf_dir = Path(storage_root) / Path(a.pdf_path).parent filename = Path(a.pdf_path).name return send_from_directory(pdf_dir, filename, as_attachment=True, download_name=f"{a.title}.pdf")