Files
SneakySwole/app/services/workout_session_service.py
Phillip Tarrant e35b78ae87 feat: add Phase 4 Logging & Tracking — inline set logging, history views
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>
2026-02-24 12:12:23 -06:00

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