Merge branch 'fix/log-form-exercise-undefined'
This commit is contained in:
@@ -17,6 +17,7 @@ from sqlmodel import SQLModel, Session
|
||||
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
||||
from starlette.requests import Request as StarletteRequest
|
||||
from starlette.responses import Response
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
from app.models.user import User
|
||||
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.auth_service import AuthService
|
||||
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__)
|
||||
|
||||
@@ -91,6 +92,11 @@ def create_app() -> FastAPI:
|
||||
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
|
||||
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,
|
||||
current_streak, last_workout_date.
|
||||
"""
|
||||
sessions = self._session.exec(
|
||||
all_sessions = self._session.exec(
|
||||
select(WorkoutSession)
|
||||
.where(WorkoutSession.user_id == user_id)
|
||||
.order_by(WorkoutSession.date.desc())
|
||||
).all()
|
||||
total_sessions = len(sessions)
|
||||
|
||||
# Only count sessions that still have log entries
|
||||
sessions = []
|
||||
total_volume = 0.0
|
||||
total_sets = 0
|
||||
for ws in sessions:
|
||||
for ws in all_sessions:
|
||||
logs = self._session.exec(
|
||||
select(WorkoutLog).where(WorkoutLog.session_id == ws.id)
|
||||
).all()
|
||||
if not logs:
|
||||
continue
|
||||
sessions.append(ws)
|
||||
for log_entry in logs:
|
||||
total_sets += 1
|
||||
weight = _weight_to_float(log_entry.weight_used)
|
||||
total_volume += log_entry.reps_completed * weight
|
||||
total_sessions = len(sessions)
|
||||
|
||||
current_streak = 0
|
||||
if sessions:
|
||||
@@ -162,6 +167,9 @@ class AnalyticsService:
|
||||
select(WorkoutLog).where(WorkoutLog.session_id == ws.id)
|
||||
).all()
|
||||
|
||||
if not logs:
|
||||
continue
|
||||
|
||||
day_volume = sum(
|
||||
log_entry.reps_completed * _weight_to_float(log_entry.weight_used)
|
||||
for log_entry in logs
|
||||
|
||||
@@ -149,6 +149,8 @@ class LogService:
|
||||
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.
|
||||
|
||||
@@ -159,10 +161,22 @@ class LogService:
|
||||
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,
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
{% set next_set = logs|length + 1 %}
|
||||
{% include "partials/log_entry.html" %}
|
||||
{% else %}
|
||||
{% set exercise_id = exercise.id %}
|
||||
{% set next_set = 1 %}
|
||||
{% include "partials/log_form.html" %}
|
||||
{% endif %}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<!-- Inline logging form, included inside each exercise_card.html -->
|
||||
<form hx-post="/log"
|
||||
hx-target="#logs-exercise-{{ exercise.id }}"
|
||||
hx-target="#logs-exercise-{{ exercise_id }}"
|
||||
hx-swap="innerHTML"
|
||||
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="set_number" value="{{ next_set|default(1) }}">
|
||||
|
||||
|
||||
@@ -17,6 +17,10 @@ from app.services.auth_service import AuthService
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class NotAuthenticatedError(Exception):
|
||||
"""Raised when a request lacks valid authentication."""
|
||||
|
||||
# Cookie name for the admin session
|
||||
SESSION_COOKIE_NAME = "session"
|
||||
|
||||
@@ -77,11 +81,7 @@ def _login_redirect():
|
||||
"""Create a redirect exception to the login page.
|
||||
|
||||
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
|
||||
|
||||
response = RedirectResponse(url="/login", status_code=303)
|
||||
exc = HTTPException(status_code=303, detail="Not authenticated")
|
||||
exc.response = response
|
||||
return exc
|
||||
return NotAuthenticatedError()
|
||||
|
||||
Reference in New Issue
Block a user