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:
80
app/services/export_service.py
Normal file
80
app/services/export_service.py
Normal file
@@ -0,0 +1,80 @@
|
||||
"""Export service for generating CSV-ready workout history data.
|
||||
|
||||
Queries workout logs joined with sessions, days, and exercises
|
||||
to produce flat rows suitable for CSV export.
|
||||
"""
|
||||
|
||||
from datetime import date
|
||||
|
||||
import structlog
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from app.models.exercise import Exercise
|
||||
from app.models.workout_log import WorkoutLog
|
||||
from app.models.workout_session import WorkoutSession
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class ExportService:
|
||||
"""Builds export-ready workout history rows.
|
||||
|
||||
Args:
|
||||
session: An active SQLModel Session.
|
||||
"""
|
||||
|
||||
def __init__(self, session: Session) -> None:
|
||||
self._session = session
|
||||
|
||||
def get_export_rows(
|
||||
self, user_id: int, start_date: date, end_date: date,
|
||||
) -> list[dict]:
|
||||
"""Get flat workout log rows for CSV export.
|
||||
|
||||
Args:
|
||||
user_id: The user whose data to export.
|
||||
start_date: Inclusive start of date range.
|
||||
end_date: Inclusive end of date range.
|
||||
|
||||
Returns:
|
||||
List of dicts with keys: date, workout_type, exercise,
|
||||
set_number, reps, weight, felt_easy.
|
||||
"""
|
||||
sessions = self._session.exec(
|
||||
select(WorkoutSession)
|
||||
.where(
|
||||
WorkoutSession.user_id == user_id,
|
||||
WorkoutSession.date >= start_date,
|
||||
WorkoutSession.date <= end_date,
|
||||
)
|
||||
.order_by(WorkoutSession.date.asc())
|
||||
).all()
|
||||
|
||||
if not sessions:
|
||||
return []
|
||||
|
||||
# Pre-load exercises for name lookup
|
||||
exercises = self._session.exec(select(Exercise)).all()
|
||||
exercise_map = {e.id: e for e in exercises}
|
||||
|
||||
rows = []
|
||||
for ws in sessions:
|
||||
logs = self._session.exec(
|
||||
select(WorkoutLog)
|
||||
.where(WorkoutLog.session_id == ws.id)
|
||||
.order_by(WorkoutLog.exercise_id, WorkoutLog.set_number)
|
||||
).all()
|
||||
|
||||
for log_entry in logs:
|
||||
exercise = exercise_map.get(log_entry.exercise_id)
|
||||
rows.append({
|
||||
"date": ws.date.isoformat(),
|
||||
"workout_type": exercise.workout_day if exercise else "Unknown",
|
||||
"exercise": exercise.name if exercise else "Unknown",
|
||||
"set_number": log_entry.set_number,
|
||||
"reps": log_entry.reps_completed,
|
||||
"weight": log_entry.weight_used,
|
||||
"felt_easy": "Yes" if log_entry.felt_easy else "No",
|
||||
})
|
||||
|
||||
return rows
|
||||
Reference in New Issue
Block a user