Add Doris Barbell live workout logger
This commit is contained in:
@@ -140,6 +140,86 @@ def routine_log_sheet(request: Request, routine_id: str):
|
||||
)
|
||||
|
||||
|
||||
@router.get('/routines/{routine_id}/live')
|
||||
def live_workout_logger(request: Request, routine_id: str):
|
||||
routine = _get_routine_or_404(request, routine_id)
|
||||
return templates.TemplateResponse(
|
||||
request=request,
|
||||
name='live_workout.html',
|
||||
context=_live_workout_context(request, routine),
|
||||
)
|
||||
|
||||
|
||||
@router.post('/routines/{routine_id}/live/start', status_code=303)
|
||||
def start_live_workout_session(
|
||||
request: Request,
|
||||
routine_id: str,
|
||||
user_id: str = Form(...),
|
||||
performed_at: str = Form(...),
|
||||
):
|
||||
routine = _get_routine_or_404(request, routine_id)
|
||||
if routine['user_id'] != user_id:
|
||||
raise HTTPException(status_code=400, detail='Routine does not belong to that user.')
|
||||
request.app.state.store.start_active_workout_session(
|
||||
{
|
||||
'routine_id': routine['id'],
|
||||
'routine_name': routine['name'],
|
||||
'user_id': user_id,
|
||||
'performed_at': performed_at,
|
||||
}
|
||||
)
|
||||
return RedirectResponse(url=f"/routines/{routine_id}/live", status_code=status.HTTP_303_SEE_OTHER)
|
||||
|
||||
|
||||
@router.post('/routines/{routine_id}/live/add-exercise', status_code=303)
|
||||
async def add_live_workout_exercise(
|
||||
request: Request,
|
||||
routine_id: str,
|
||||
user_id: str = Form(...),
|
||||
performed_at: str = Form(...),
|
||||
exercise_name: str = Form(...),
|
||||
set_count: int = Form(...),
|
||||
):
|
||||
routine = _get_routine_or_404(request, routine_id)
|
||||
if routine['user_id'] != user_id:
|
||||
raise HTTPException(status_code=400, detail='Routine does not belong to that user.')
|
||||
active_session = request.app.state.store.get_active_workout_session(routine_id, user_id)
|
||||
if active_session is None:
|
||||
request.app.state.store.start_active_workout_session(
|
||||
{
|
||||
'routine_id': routine['id'],
|
||||
'routine_name': routine['name'],
|
||||
'user_id': user_id,
|
||||
'performed_at': performed_at,
|
||||
}
|
||||
)
|
||||
exercise = await _parse_live_workout_exercise_form(request, routine, exercise_name, set_count)
|
||||
request.app.state.store.add_active_workout_exercise(routine_id, user_id, exercise)
|
||||
return RedirectResponse(url=f"/routines/{routine_id}/live", status_code=status.HTTP_303_SEE_OTHER)
|
||||
|
||||
|
||||
@router.post('/routines/{routine_id}/live/finish', status_code=303)
|
||||
def finish_live_workout_session(
|
||||
request: Request,
|
||||
routine_id: str,
|
||||
user_id: str = Form(...),
|
||||
performed_at: str = Form(...),
|
||||
notes: str = Form(''),
|
||||
):
|
||||
routine = _get_routine_or_404(request, routine_id)
|
||||
if routine['user_id'] != user_id:
|
||||
raise HTTPException(status_code=400, detail='Routine does not belong to that user.')
|
||||
active_session = request.app.state.store.get_active_workout_session(routine_id, user_id)
|
||||
if active_session is None:
|
||||
raise HTTPException(status_code=400, detail='No live workout session in progress.')
|
||||
if active_session['performed_at'] != performed_at:
|
||||
raise HTTPException(status_code=400, detail='Performed-at timestamp does not match the active session.')
|
||||
if not active_session.get('exercises'):
|
||||
raise HTTPException(status_code=400, detail='Log at least one exercise before finishing the session.')
|
||||
request.app.state.store.finish_active_workout_session(routine_id, user_id, notes.strip() or None)
|
||||
return RedirectResponse(url='/', status_code=status.HTTP_303_SEE_OTHER)
|
||||
|
||||
|
||||
@router.post('/workouts/routine-log', status_code=303)
|
||||
async def log_seeded_routine_workout(
|
||||
request: Request,
|
||||
@@ -168,6 +248,11 @@ async def log_seeded_routine_workout(
|
||||
def _dashboard_context(request: Request) -> dict:
|
||||
store = request.app.state.store
|
||||
summary = store.dashboard_summary()
|
||||
routines_with_live_state = []
|
||||
for routine in summary['routines']:
|
||||
active_session = store.get_active_workout_session(routine['id'], routine['user_id'])
|
||||
routines_with_live_state.append({**routine, 'live_session': active_session})
|
||||
summary['routines'] = routines_with_live_state
|
||||
return {
|
||||
'title': 'Doris Barbell',
|
||||
'summary': summary,
|
||||
@@ -178,6 +263,49 @@ def _dashboard_context(request: Request) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _live_workout_context(request: Request, routine: dict) -> dict:
|
||||
active_session = request.app.state.store.get_active_workout_session(routine['id'], routine['user_id'])
|
||||
routine_sheet = _build_routine_log_sheet(request, routine)
|
||||
remaining_exercises = _remaining_live_exercises(routine_sheet, active_session)
|
||||
logged_exercises = []
|
||||
if active_session is not None:
|
||||
for exercise in active_session.get('exercises', []):
|
||||
logged_exercises.append(
|
||||
{
|
||||
'exercise_name': exercise['exercise_name'],
|
||||
'set_count': len(exercise.get('sets', [])),
|
||||
'notes': exercise.get('notes'),
|
||||
}
|
||||
)
|
||||
return {
|
||||
'title': f"Live log · {routine['name']}",
|
||||
'routine': routine,
|
||||
'routine_sheet': routine_sheet,
|
||||
'active_session': active_session,
|
||||
'logged_exercises': logged_exercises,
|
||||
'remaining_exercises': remaining_exercises,
|
||||
'default_performed_at': active_session['performed_at'] if active_session else _default_performed_at_value(),
|
||||
'routine_action_label': _routine_action_label(routine['name']),
|
||||
}
|
||||
|
||||
|
||||
def _remaining_live_exercises(routine_sheet: list[dict], active_session: dict | None) -> list[dict]:
|
||||
completed_counts: dict[str, int] = {}
|
||||
if active_session is not None:
|
||||
for exercise in active_session.get('exercises', []):
|
||||
normalized_name = exercise['exercise_name'].strip().lower()
|
||||
completed_counts[normalized_name] = completed_counts.get(normalized_name, 0) + 1
|
||||
remaining = []
|
||||
for exercise in routine_sheet:
|
||||
normalized_name = exercise['exercise_name'].strip().lower()
|
||||
completed_count = completed_counts.get(normalized_name, 0)
|
||||
if completed_count > 0:
|
||||
completed_counts[normalized_name] = completed_count - 1
|
||||
continue
|
||||
remaining.append(exercise)
|
||||
return remaining
|
||||
|
||||
|
||||
def _optional_float(value: str) -> float | None:
|
||||
cleaned = value.strip()
|
||||
if not cleaned:
|
||||
@@ -345,6 +473,58 @@ def _routine_action_label(routine_name: str) -> str:
|
||||
return day_aliases.get(label, label)
|
||||
|
||||
|
||||
async def _parse_live_workout_exercise_form(
|
||||
request: Request,
|
||||
routine: dict,
|
||||
exercise_name: str,
|
||||
set_count: int,
|
||||
) -> dict:
|
||||
form = await request.form()
|
||||
sheet = _build_routine_log_sheet(request, routine)
|
||||
matching_exercise = next(
|
||||
(exercise for exercise in sheet if exercise['exercise_name'].strip().lower() == exercise_name.strip().lower()),
|
||||
None,
|
||||
)
|
||||
if matching_exercise is None:
|
||||
raise HTTPException(status_code=400, detail=f'Unknown routine exercise: {exercise_name}')
|
||||
default_reps_raw = matching_exercise.get('default_reps', '')
|
||||
is_band_exercise = bool(matching_exercise.get('is_band'))
|
||||
sets = []
|
||||
for set_index in range(set_count):
|
||||
reps_raw = str(form.get(f'set_{set_index}_reps', '')).strip()
|
||||
weight_raw = str(form.get(f'set_{set_index}_weight_lbs', '')).strip()
|
||||
if not reps_raw and not weight_raw:
|
||||
if is_band_exercise and default_reps_raw.isdigit():
|
||||
reps_raw = default_reps_raw
|
||||
else:
|
||||
continue
|
||||
if not reps_raw and default_reps_raw.isdigit():
|
||||
reps_raw = default_reps_raw
|
||||
if not reps_raw:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f'Exercise {exercise_name} set {set_index + 1} needs reps.',
|
||||
)
|
||||
if not weight_raw and not is_band_exercise:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f'Exercise {exercise_name} set {set_index + 1} needs both reps and weight.',
|
||||
)
|
||||
try:
|
||||
reps = int(reps_raw)
|
||||
weight_lbs = None if not weight_raw else float(weight_raw)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f'Exercise {exercise_name} set {set_index + 1} has invalid reps or weight.',
|
||||
) from exc
|
||||
sets.append({'reps': reps, 'weight_lbs': weight_lbs})
|
||||
if not sets:
|
||||
raise HTTPException(status_code=400, detail=f'Log at least one set for {exercise_name}.')
|
||||
exercise_notes = str(form.get('exercise_notes', '')).strip() or None
|
||||
return {'exercise_name': matching_exercise['exercise_name'], 'notes': exercise_notes, 'sets': sets}
|
||||
|
||||
|
||||
async def _parse_structured_routine_form(request: Request, routine: dict, exercise_count: int) -> list[dict]:
|
||||
form = await request.form()
|
||||
routine_exercises = routine.get('exercises', [])
|
||||
|
||||
Reference in New Issue
Block a user