222 lines
7.1 KiB
Python
222 lines
7.1 KiB
Python
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("/<int:analysis_id>")
|
|
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("/<int:analysis_id>/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("/<int:analysis_id>/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("/<int:analysis_id>/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("/<int:analysis_id>/groups/<int:group_index>/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("/<int:analysis_id>/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")
|