Build the core user-facing experience with admin login (bcrypt + signed session cookies), profile switcher, workout day viewer with warmups and exercise cards, and HTMX-powered exercise browser with search/filter. - AuthService with bcrypt password verification and itsdangerous session tokens - Auth dependency redirects to /login (303) for unauthenticated requests - NavContextMiddleware injects admin/profiles/active_profile into all templates - Profile management (list, switch, edit) with cookie-based active profile - Workout day viewer shows warmups + exercises + per-user programming targets - Exercise browser with HTMX filter dropdowns (no page reloads) - Flash message partial for success/error feedback - 12 new tests (66 total passing) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
87 lines
2.6 KiB
Python
87 lines
2.6 KiB
Python
"""Exercise browser routes with HTMX search/filter support.
|
|
|
|
All filtering is done via HTMX partial responses — no JSON APIs.
|
|
"""
|
|
|
|
import structlog
|
|
from fastapi import APIRouter, Depends, Query, Request
|
|
from fastapi.responses import HTMLResponse
|
|
from sqlmodel import Session
|
|
|
|
from app.database import get_db_session
|
|
from app.models.user import User
|
|
from app.services.exercise_service import ExerciseService
|
|
from app.utils.auth import get_current_admin_user
|
|
|
|
logger = structlog.get_logger(__name__)
|
|
|
|
router = APIRouter(prefix="/exercises", tags=["exercises"])
|
|
|
|
|
|
@router.get("", response_class=HTMLResponse)
|
|
async def exercise_browser(
|
|
request: Request,
|
|
session: Session = Depends(get_db_session),
|
|
admin: User = Depends(get_current_admin_user),
|
|
):
|
|
"""Render the exercise browser page with all exercises.
|
|
|
|
Args:
|
|
request: The incoming HTTP request.
|
|
session: Database session.
|
|
admin: The authenticated admin user.
|
|
|
|
Returns:
|
|
Rendered exercise browser page.
|
|
"""
|
|
exercise_service = ExerciseService(session)
|
|
exercises = exercise_service.list_exercises()
|
|
workout_days = exercise_service.list_workout_days()
|
|
|
|
# Collect unique muscle groups for the filter dropdown
|
|
muscle_groups = sorted(set(ex.muscle_group for ex in exercises))
|
|
|
|
templates = request.app.state.templates
|
|
return templates.TemplateResponse("pages/exercise_browser.html", {
|
|
"request": request,
|
|
"exercises": exercises,
|
|
"workout_days": workout_days,
|
|
"muscle_groups": muscle_groups,
|
|
"admin": admin,
|
|
})
|
|
|
|
|
|
@router.get("/search", response_class=HTMLResponse)
|
|
async def exercise_search(
|
|
request: Request,
|
|
session: Session = Depends(get_db_session),
|
|
admin: User = Depends(get_current_admin_user),
|
|
workout_day: str = Query(default="", alias="workout_day"),
|
|
muscle_group: str = Query(default="", alias="muscle_group"),
|
|
):
|
|
"""Return filtered exercise list as an HTMX partial.
|
|
|
|
Called via hx-get from the exercise browser filter dropdowns.
|
|
|
|
Args:
|
|
request: The incoming HTTP request.
|
|
session: Database session.
|
|
admin: The authenticated admin user.
|
|
workout_day: Filter by workout day name.
|
|
muscle_group: Filter by muscle group.
|
|
|
|
Returns:
|
|
Rendered exercise list partial HTML.
|
|
"""
|
|
exercise_service = ExerciseService(session)
|
|
exercises = exercise_service.list_exercises(
|
|
workout_day=workout_day or None,
|
|
muscle_group=muscle_group or None,
|
|
)
|
|
|
|
templates = request.app.state.templates
|
|
return templates.TemplateResponse("partials/exercise_list.html", {
|
|
"request": request,
|
|
"exercises": exercises,
|
|
})
|