Add workout logging so users can track sets, reps, weight, and a
"felt easy?" toggle inline from the workout day view via HTMX.
Sessions auto-create on first log. History page shows past sessions
with detailed per-exercise breakdowns.
New services: WorkoutSessionService, LogService
New routes: POST /log, /log/{id}/edit, /log/{id}/delete, GET /history, /history/{id}
New templates: log_form, log_entry, session_card, log_history, session_detail
Modified: exercise_card (inline logging), nav (History link), workouts route (session context)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
132 lines
3.7 KiB
Python
132 lines
3.7 KiB
Python
"""Service layer for workout session management.
|
|
|
|
Handles creation, retrieval, and updates for workout sessions.
|
|
A session represents a single workout on a specific date for a user.
|
|
"""
|
|
|
|
from datetime import date
|
|
from typing import Optional
|
|
|
|
import structlog
|
|
from sqlmodel import Session, select
|
|
|
|
from app.models.workout_session import WorkoutSession
|
|
|
|
logger = structlog.get_logger(__name__)
|
|
|
|
|
|
class WorkoutSessionService:
|
|
"""Handles CRUD operations for WorkoutSession records.
|
|
|
|
Args:
|
|
session: An active SQLModel Session.
|
|
"""
|
|
|
|
def __init__(self, session: Session) -> None:
|
|
self._session = session
|
|
|
|
def get_or_create_session(
|
|
self,
|
|
user_id: int,
|
|
workout_day_id: int,
|
|
session_date: date,
|
|
) -> WorkoutSession:
|
|
"""Get an existing session or create a new one.
|
|
|
|
If a session already exists for this user + day + date combo,
|
|
return it. Otherwise, create a new one. This allows logging
|
|
to start automatically without an explicit "start session" step.
|
|
|
|
Args:
|
|
user_id: The user's ID.
|
|
workout_day_id: The workout day's ID.
|
|
session_date: The date of the workout.
|
|
|
|
Returns:
|
|
The existing or newly created WorkoutSession.
|
|
"""
|
|
statement = select(WorkoutSession).where(
|
|
WorkoutSession.user_id == user_id,
|
|
WorkoutSession.workout_day_id == workout_day_id,
|
|
WorkoutSession.date == session_date,
|
|
)
|
|
existing = self._session.exec(statement).first()
|
|
|
|
if existing:
|
|
return existing
|
|
|
|
ws = WorkoutSession(
|
|
user_id=user_id,
|
|
workout_day_id=workout_day_id,
|
|
date=session_date,
|
|
)
|
|
self._session.add(ws)
|
|
self._session.commit()
|
|
self._session.refresh(ws)
|
|
logger.info(
|
|
"workout_session_created",
|
|
user_id=user_id,
|
|
day_id=workout_day_id,
|
|
date=str(session_date),
|
|
)
|
|
return ws
|
|
|
|
def list_sessions(
|
|
self,
|
|
user_id: int,
|
|
limit: int = 50,
|
|
) -> list[WorkoutSession]:
|
|
"""List workout sessions for a user, most recent first.
|
|
|
|
Args:
|
|
user_id: The user's ID.
|
|
limit: Maximum number of sessions to return.
|
|
|
|
Returns:
|
|
List of WorkoutSession records, ordered by date descending.
|
|
"""
|
|
statement = (
|
|
select(WorkoutSession)
|
|
.where(WorkoutSession.user_id == user_id)
|
|
.order_by(WorkoutSession.date.desc())
|
|
.limit(limit)
|
|
)
|
|
return list(self._session.exec(statement).all())
|
|
|
|
def get_session_by_id(self, session_id: int) -> Optional[WorkoutSession]:
|
|
"""Retrieve a workout session by primary key.
|
|
|
|
Args:
|
|
session_id: The session ID.
|
|
|
|
Returns:
|
|
The WorkoutSession record, or None if not found.
|
|
"""
|
|
return self._session.get(WorkoutSession, session_id)
|
|
|
|
def update_session(self, session_id: int, **kwargs) -> WorkoutSession:
|
|
"""Update fields on an existing workout session.
|
|
|
|
Args:
|
|
session_id: The session ID.
|
|
**kwargs: Field names and new values.
|
|
|
|
Returns:
|
|
The updated WorkoutSession record.
|
|
|
|
Raises:
|
|
ValueError: If the session is not found.
|
|
"""
|
|
ws = self.get_session_by_id(session_id)
|
|
if ws is None:
|
|
raise ValueError(f"WorkoutSession with id {session_id} not found")
|
|
|
|
for key, value in kwargs.items():
|
|
if hasattr(ws, key):
|
|
setattr(ws, key, value)
|
|
|
|
self._session.add(ws)
|
|
self._session.commit()
|
|
self._session.refresh(ws)
|
|
return ws
|