Files
SneakySwole/app/services/log_service.py
Phillip Tarrant 215ce90404 fix: resolve template errors, orphaned sessions, and auth redirects
- Fix exercise_id undefined error in log_form.html by using scalar
  exercise_id instead of exercise.id object reference
- Clean up orphaned WorkoutSession records when all logs are deleted
- Filter empty sessions from dashboard stats (sessions, volume, streak)
- Replace broken HTTPException auth redirect with custom exception
  handler that properly returns 302 to /login

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 14:00:34 -06:00

210 lines
6.1 KiB
Python

"""Service layer for workout log (set-level) data access.
Handles CRUD for individual set logs within workout sessions.
"""
from typing import Optional
import structlog
from sqlmodel import Session, select
from app.models.workout_log import WorkoutLog
from app.models.workout_session import WorkoutSession
logger = structlog.get_logger(__name__)
class LogService:
"""Handles CRUD operations for WorkoutLog records.
Args:
session: An active SQLModel Session.
"""
def __init__(self, session: Session) -> None:
self._session = session
def create_log(
self,
session_id: int,
exercise_id: int,
set_number: int,
reps_completed: int,
weight_used: str,
felt_easy: bool,
notes: Optional[str] = None,
) -> WorkoutLog:
"""Create a new set log entry.
Args:
session_id: FK to workout_sessions.
exercise_id: FK to exercises.
set_number: Which set (1, 2, 3...).
reps_completed: Actual reps performed.
weight_used: Weight as string (e.g., "30 lbs").
felt_easy: Whether the set felt easy.
notes: Optional notes for this set.
Returns:
The newly created WorkoutLog record.
"""
log = WorkoutLog(
session_id=session_id,
exercise_id=exercise_id,
set_number=set_number,
reps_completed=reps_completed,
weight_used=weight_used,
felt_easy=felt_easy,
notes=notes,
)
self._session.add(log)
self._session.commit()
self._session.refresh(log)
logger.info(
"log_created",
session_id=session_id,
exercise_id=exercise_id,
set=set_number,
)
return log
def list_logs_for_session(self, session_id: int) -> list[WorkoutLog]:
"""List all log entries for a workout session.
Args:
session_id: The workout session ID.
Returns:
List of WorkoutLog records ordered by exercise and set number.
"""
statement = (
select(WorkoutLog)
.where(WorkoutLog.session_id == session_id)
.order_by(WorkoutLog.exercise_id, WorkoutLog.set_number)
)
return list(self._session.exec(statement).all())
def list_logs_for_exercise(
self,
session_id: int,
exercise_id: int,
) -> list[WorkoutLog]:
"""List log entries for a specific exercise within a session.
Args:
session_id: The workout session ID.
exercise_id: The exercise ID.
Returns:
List of WorkoutLog records for this exercise, ordered by set.
"""
statement = (
select(WorkoutLog)
.where(
WorkoutLog.session_id == session_id,
WorkoutLog.exercise_id == exercise_id,
)
.order_by(WorkoutLog.set_number)
)
return list(self._session.exec(statement).all())
def get_log_by_id(self, log_id: int) -> Optional[WorkoutLog]:
"""Retrieve a log entry by primary key.
Args:
log_id: The log entry ID.
Returns:
The WorkoutLog record, or None if not found.
"""
return self._session.get(WorkoutLog, log_id)
def update_log(self, log_id: int, **kwargs) -> WorkoutLog:
"""Update fields on an existing log entry.
Args:
log_id: The log entry ID.
**kwargs: Field names and new values.
Returns:
The updated WorkoutLog record.
Raises:
ValueError: If the log is not found.
"""
log = self.get_log_by_id(log_id)
if log is None:
raise ValueError(f"WorkoutLog with id {log_id} not found")
for key, value in kwargs.items():
if hasattr(log, key):
setattr(log, key, value)
self._session.add(log)
self._session.commit()
self._session.refresh(log)
logger.info("log_updated", log_id=log_id, fields=list(kwargs.keys()))
return log
def delete_log(self, log_id: int) -> None:
"""Delete a log entry.
Removes the log and cleans up the parent session if no logs remain.
Args:
log_id: The log entry ID.
Raises:
ValueError: If the log is not found.
"""
log = self.get_log_by_id(log_id)
if log is None:
raise ValueError(f"WorkoutLog with id {log_id} not found")
session_id = log.session_id
self._session.delete(log)
self._session.commit()
logger.info("log_deleted", log_id=log_id)
# Clean up orphaned session if no logs remain
remaining = self._session.exec(
select(WorkoutLog).where(WorkoutLog.session_id == session_id)
).first()
if remaining is None:
ws = self._session.get(WorkoutSession, session_id)
if ws:
self._session.delete(ws)
self._session.commit()
logger.info("session_deleted_empty", session_id=session_id)
def get_latest_logs_for_exercise(
self,
user_id: int,
exercise_id: int,
limit: int = 10,
) -> list[WorkoutLog]:
"""Get the most recent log entries for an exercise across sessions.
Used by the progression engine (Phase 5) to determine
what the user last did for this exercise.
Args:
user_id: The user's ID.
exercise_id: The exercise ID.
limit: Maximum number of logs to return.
Returns:
List of recent WorkoutLog records, newest first.
"""
statement = (
select(WorkoutLog)
.join(WorkoutSession, WorkoutLog.session_id == WorkoutSession.id)
.where(
WorkoutSession.user_id == user_id,
WorkoutLog.exercise_id == exercise_id,
)
.order_by(WorkoutSession.date.desc(), WorkoutLog.set_number)
.limit(limit)
)
return list(self._session.exec(statement).all())