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>
This commit is contained in:
@@ -17,6 +17,7 @@ from sqlmodel import SQLModel, Session
|
|||||||
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
||||||
from starlette.requests import Request as StarletteRequest
|
from starlette.requests import Request as StarletteRequest
|
||||||
from starlette.responses import Response
|
from starlette.responses import Response
|
||||||
|
from fastapi.responses import RedirectResponse
|
||||||
|
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
from app.config import get_settings
|
from app.config import get_settings
|
||||||
@@ -35,7 +36,7 @@ from app.routes.schedule import router as schedule_router
|
|||||||
from app.services.seed_service import SeedService
|
from app.services.seed_service import SeedService
|
||||||
from app.services.auth_service import AuthService
|
from app.services.auth_service import AuthService
|
||||||
from app.services.user_service import UserService
|
from app.services.user_service import UserService
|
||||||
from app.utils.auth import SESSION_COOKIE_NAME
|
from app.utils.auth import SESSION_COOKIE_NAME, NotAuthenticatedError
|
||||||
|
|
||||||
logger = structlog.get_logger(__name__)
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
@@ -91,6 +92,11 @@ def create_app() -> FastAPI:
|
|||||||
version="0.1.0",
|
version="0.1.0",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Redirect unauthenticated requests to login
|
||||||
|
@app.exception_handler(NotAuthenticatedError)
|
||||||
|
async def _not_authenticated_handler(request, exc):
|
||||||
|
return RedirectResponse(url="/login", status_code=302)
|
||||||
|
|
||||||
# Mount static files
|
# Mount static files
|
||||||
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
|
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
|
||||||
|
|
||||||
|
|||||||
@@ -48,23 +48,28 @@ class AnalyticsService:
|
|||||||
Dict with keys: total_sessions, total_volume, total_sets,
|
Dict with keys: total_sessions, total_volume, total_sets,
|
||||||
current_streak, last_workout_date.
|
current_streak, last_workout_date.
|
||||||
"""
|
"""
|
||||||
sessions = self._session.exec(
|
all_sessions = self._session.exec(
|
||||||
select(WorkoutSession)
|
select(WorkoutSession)
|
||||||
.where(WorkoutSession.user_id == user_id)
|
.where(WorkoutSession.user_id == user_id)
|
||||||
.order_by(WorkoutSession.date.desc())
|
.order_by(WorkoutSession.date.desc())
|
||||||
).all()
|
).all()
|
||||||
total_sessions = len(sessions)
|
|
||||||
|
|
||||||
|
# Only count sessions that still have log entries
|
||||||
|
sessions = []
|
||||||
total_volume = 0.0
|
total_volume = 0.0
|
||||||
total_sets = 0
|
total_sets = 0
|
||||||
for ws in sessions:
|
for ws in all_sessions:
|
||||||
logs = self._session.exec(
|
logs = self._session.exec(
|
||||||
select(WorkoutLog).where(WorkoutLog.session_id == ws.id)
|
select(WorkoutLog).where(WorkoutLog.session_id == ws.id)
|
||||||
).all()
|
).all()
|
||||||
|
if not logs:
|
||||||
|
continue
|
||||||
|
sessions.append(ws)
|
||||||
for log_entry in logs:
|
for log_entry in logs:
|
||||||
total_sets += 1
|
total_sets += 1
|
||||||
weight = _weight_to_float(log_entry.weight_used)
|
weight = _weight_to_float(log_entry.weight_used)
|
||||||
total_volume += log_entry.reps_completed * weight
|
total_volume += log_entry.reps_completed * weight
|
||||||
|
total_sessions = len(sessions)
|
||||||
|
|
||||||
current_streak = 0
|
current_streak = 0
|
||||||
if sessions:
|
if sessions:
|
||||||
@@ -162,6 +167,9 @@ class AnalyticsService:
|
|||||||
select(WorkoutLog).where(WorkoutLog.session_id == ws.id)
|
select(WorkoutLog).where(WorkoutLog.session_id == ws.id)
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
|
if not logs:
|
||||||
|
continue
|
||||||
|
|
||||||
day_volume = sum(
|
day_volume = sum(
|
||||||
log_entry.reps_completed * _weight_to_float(log_entry.weight_used)
|
log_entry.reps_completed * _weight_to_float(log_entry.weight_used)
|
||||||
for log_entry in logs
|
for log_entry in logs
|
||||||
|
|||||||
@@ -149,6 +149,8 @@ class LogService:
|
|||||||
def delete_log(self, log_id: int) -> None:
|
def delete_log(self, log_id: int) -> None:
|
||||||
"""Delete a log entry.
|
"""Delete a log entry.
|
||||||
|
|
||||||
|
Removes the log and cleans up the parent session if no logs remain.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
log_id: The log entry ID.
|
log_id: The log entry ID.
|
||||||
|
|
||||||
@@ -159,10 +161,22 @@ class LogService:
|
|||||||
if log is None:
|
if log is None:
|
||||||
raise ValueError(f"WorkoutLog with id {log_id} not found")
|
raise ValueError(f"WorkoutLog with id {log_id} not found")
|
||||||
|
|
||||||
|
session_id = log.session_id
|
||||||
self._session.delete(log)
|
self._session.delete(log)
|
||||||
self._session.commit()
|
self._session.commit()
|
||||||
logger.info("log_deleted", log_id=log_id)
|
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(
|
def get_latest_logs_for_exercise(
|
||||||
self,
|
self,
|
||||||
user_id: int,
|
user_id: int,
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
{% set next_set = logs|length + 1 %}
|
{% set next_set = logs|length + 1 %}
|
||||||
{% include "partials/log_entry.html" %}
|
{% include "partials/log_entry.html" %}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
{% set exercise_id = exercise.id %}
|
||||||
{% set next_set = 1 %}
|
{% set next_set = 1 %}
|
||||||
{% include "partials/log_form.html" %}
|
{% include "partials/log_form.html" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<!-- Inline logging form, included inside each exercise_card.html -->
|
<!-- Inline logging form, included inside each exercise_card.html -->
|
||||||
<form hx-post="/log"
|
<form hx-post="/log"
|
||||||
hx-target="#logs-exercise-{{ exercise.id }}"
|
hx-target="#logs-exercise-{{ exercise_id }}"
|
||||||
hx-swap="innerHTML"
|
hx-swap="innerHTML"
|
||||||
style="margin-bottom:0;">
|
style="margin-bottom:0;">
|
||||||
<input type="hidden" name="exercise_id" value="{{ exercise.id }}">
|
<input type="hidden" name="exercise_id" value="{{ exercise_id }}">
|
||||||
<input type="hidden" name="workout_day_id" value="{{ workout_day_id }}">
|
<input type="hidden" name="workout_day_id" value="{{ workout_day_id }}">
|
||||||
<input type="hidden" name="set_number" value="{{ next_set|default(1) }}">
|
<input type="hidden" name="set_number" value="{{ next_set|default(1) }}">
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ from app.services.auth_service import AuthService
|
|||||||
|
|
||||||
logger = structlog.get_logger(__name__)
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class NotAuthenticatedError(Exception):
|
||||||
|
"""Raised when a request lacks valid authentication."""
|
||||||
|
|
||||||
# Cookie name for the admin session
|
# Cookie name for the admin session
|
||||||
SESSION_COOKIE_NAME = "session"
|
SESSION_COOKIE_NAME = "session"
|
||||||
|
|
||||||
@@ -77,11 +81,7 @@ def _login_redirect():
|
|||||||
"""Create a redirect exception to the login page.
|
"""Create a redirect exception to the login page.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
An HTTPException-compatible RedirectResponse.
|
A NotAuthenticatedError handled by a registered exception handler
|
||||||
|
in main.py that sends a 302 redirect to /login.
|
||||||
"""
|
"""
|
||||||
from fastapi import HTTPException
|
return NotAuthenticatedError()
|
||||||
|
|
||||||
response = RedirectResponse(url="/login", status_code=303)
|
|
||||||
exc = HTTPException(status_code=303, detail="Not authenticated")
|
|
||||||
exc.response = response
|
|
||||||
return exc
|
|
||||||
|
|||||||
Reference in New Issue
Block a user