feat: add CSV export of workout history to dashboard
Add date range picker and download button to the dashboard page, backed by GET /dashboard/export endpoint that returns a StreamingResponse CSV file. Completes Phase 4 (final V2 improvement). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,19 +1,26 @@
|
||||
"""Progress dashboard routes.
|
||||
|
||||
Displays summary statistics, volume charts, and per-exercise progress.
|
||||
Displays summary statistics, volume charts, per-exercise progress,
|
||||
and CSV export of workout history.
|
||||
"""
|
||||
|
||||
import csv
|
||||
import io
|
||||
import json
|
||||
import re
|
||||
from datetime import date, timedelta
|
||||
from typing import Optional
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi import APIRouter, Depends, Query, Request
|
||||
from fastapi.responses import HTMLResponse, StreamingResponse
|
||||
from sqlmodel import Session
|
||||
|
||||
from app.database import get_db_session
|
||||
from app.models.user import User
|
||||
from app.services.analytics_service import AnalyticsService
|
||||
from app.services.exercise_service import ExerciseService
|
||||
from app.services.export_service import ExportService
|
||||
from app.services.progression_service import ProgressionService
|
||||
from app.utils.auth import require_active_profile
|
||||
|
||||
@@ -36,6 +43,10 @@ async def dashboard(
|
||||
exercise_service = ExerciseService(session)
|
||||
exercises = exercise_service.list_exercises()
|
||||
|
||||
today = date.today()
|
||||
export_start = (today - timedelta(days=30)).isoformat()
|
||||
export_end = today.isoformat()
|
||||
|
||||
templates = request.app.state.templates
|
||||
return templates.TemplateResponse("pages/dashboard.html", {
|
||||
"request": request,
|
||||
@@ -43,9 +54,52 @@ async def dashboard(
|
||||
"volume_data_json": json.dumps(volume_data),
|
||||
"exercises": exercises,
|
||||
"active_profile": profile,
|
||||
"export_start_date": export_start,
|
||||
"export_end_date": export_end,
|
||||
})
|
||||
|
||||
|
||||
@router.get("/export")
|
||||
async def export_csv(
|
||||
request: Request,
|
||||
start_date: Optional[str] = Query(None),
|
||||
end_date: Optional[str] = Query(None),
|
||||
session: Session = Depends(get_db_session),
|
||||
profile: User = Depends(require_active_profile),
|
||||
):
|
||||
"""Export workout history as a CSV file download."""
|
||||
today = date.today()
|
||||
default_start = today - timedelta(days=30)
|
||||
|
||||
try:
|
||||
parsed_start = date.fromisoformat(start_date) if start_date else default_start
|
||||
except ValueError:
|
||||
parsed_start = default_start
|
||||
|
||||
try:
|
||||
parsed_end = date.fromisoformat(end_date) if end_date else today
|
||||
except ValueError:
|
||||
parsed_end = today
|
||||
|
||||
export_service = ExportService(session)
|
||||
rows = export_service.get_export_rows(profile.id, parsed_start, parsed_end)
|
||||
|
||||
output = io.StringIO()
|
||||
headers = ["date", "workout_type", "exercise", "set_number", "reps", "weight", "felt_easy"]
|
||||
writer = csv.DictWriter(output, fieldnames=headers)
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
|
||||
safe_name = re.sub(r"[^a-z0-9_]", "", profile.display_name.lower().replace(" ", "_"))
|
||||
filename = f"sneakyswole_{safe_name}_{parsed_start.isoformat()}_to_{parsed_end.isoformat()}.csv"
|
||||
|
||||
return StreamingResponse(
|
||||
iter([output.getvalue()]),
|
||||
media_type="text/csv",
|
||||
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/exercise/{exercise_id}", response_class=HTMLResponse)
|
||||
async def exercise_progress(
|
||||
exercise_id: int,
|
||||
|
||||
Reference in New Issue
Block a user