Default routine log reps from targets
Some checks failed
secret-guardrails / artifact-secret-scan (push) Has been cancelled
secret-guardrails / gitleaks (push) Has been cancelled

This commit is contained in:
Fizzlepoof
2026-05-28 20:55:22 +00:00
parent 64e7ac82cf
commit dd4134b1ce
3 changed files with 52 additions and 2 deletions

View File

@@ -159,7 +159,7 @@ async def log_seeded_routine_workout(
'performed_at': performed_at, 'performed_at': performed_at,
'routine_name': routine_name.strip(), 'routine_name': routine_name.strip(),
'notes': notes.strip() or None, 'notes': notes.strip() or None,
'exercises': await _parse_structured_routine_form(request, exercise_count), 'exercises': await _parse_structured_routine_form(request, routine, exercise_count),
} }
request.app.state.store.add_workout(payload) request.app.state.store.add_workout(payload)
return RedirectResponse(url='/', status_code=status.HTTP_303_SEE_OTHER) return RedirectResponse(url='/', status_code=status.HTTP_303_SEE_OTHER)
@@ -272,6 +272,7 @@ def _build_routine_log_sheet(routine: dict) -> list[dict]:
'exercise_name': exercise['exercise_name'], 'exercise_name': exercise['exercise_name'],
'target_sets': target_sets, 'target_sets': target_sets,
'target_reps': exercise.get('target_reps'), 'target_reps': exercise.get('target_reps'),
'default_reps': default_reps,
'notes': exercise.get('notes'), 'notes': exercise.get('notes'),
'sets': [ 'sets': [
{ {
@@ -328,8 +329,9 @@ def _routine_action_label(routine_name: str) -> str:
return day_aliases.get(label, label) return day_aliases.get(label, label)
async def _parse_structured_routine_form(request: Request, exercise_count: int) -> list[dict]: async def _parse_structured_routine_form(request: Request, routine: dict, exercise_count: int) -> list[dict]:
form = await request.form() form = await request.form()
routine_exercises = routine.get('exercises', [])
exercises = [] exercises = []
for exercise_index in range(exercise_count): for exercise_index in range(exercise_count):
exercise_name = str(form.get(f'exercise_{exercise_index}_name', '')).strip() exercise_name = str(form.get(f'exercise_{exercise_index}_name', '')).strip()
@@ -339,12 +341,18 @@ async def _parse_structured_routine_form(request: Request, exercise_count: int)
set_count = int(str(form.get(f'exercise_{exercise_index}_set_count', '0')).strip()) set_count = int(str(form.get(f'exercise_{exercise_index}_set_count', '0')).strip())
except ValueError as exc: except ValueError as exc:
raise HTTPException(status_code=400, detail='Invalid set count in routine log form.') from exc raise HTTPException(status_code=400, detail='Invalid set count in routine log form.') from exc
target_reps = None
if exercise_index < len(routine_exercises):
target_reps = routine_exercises[exercise_index].get('target_reps')
default_reps_raw = _default_reps_value(target_reps)
sets = [] sets = []
for set_index in range(set_count): for set_index in range(set_count):
reps_raw = str(form.get(f'exercise_{exercise_index}_set_{set_index}_reps', '')).strip() reps_raw = str(form.get(f'exercise_{exercise_index}_set_{set_index}_reps', '')).strip()
weight_raw = str(form.get(f'exercise_{exercise_index}_set_{set_index}_weight_lbs', '')).strip() weight_raw = str(form.get(f'exercise_{exercise_index}_set_{set_index}_weight_lbs', '')).strip()
if not reps_raw and not weight_raw: if not reps_raw and not weight_raw:
continue continue
if not reps_raw and weight_raw and default_reps_raw.isdigit():
reps_raw = default_reps_raw
if not reps_raw or not weight_raw: if not reps_raw or not weight_raw:
raise HTTPException( raise HTTPException(
status_code=400, status_code=400,

View File

@@ -53,6 +53,7 @@
{% for exercise in routine_sheet %} {% for exercise in routine_sheet %}
<section class="evidence-slab"> <section class="evidence-slab">
<input type="hidden" name="exercise_{{ exercise.exercise_index }}_set_count" value="{{ exercise.target_sets }}"> <input type="hidden" name="exercise_{{ exercise.exercise_index }}_set_count" value="{{ exercise.target_sets }}">
<input type="hidden" name="exercise_{{ exercise.exercise_index }}_default_reps" value="{{ exercise.default_reps }}">
<div class="case-legend"> <div class="case-legend">
<span class="section-label">Exercise {{ loop.index }}</span> <span class="section-label">Exercise {{ loop.index }}</span>
<span class="pill">{{ exercise.target_sets }} sets</span> <span class="pill">{{ exercise.target_sets }} sets</span>

View File

@@ -464,6 +464,47 @@ def test_seeded_routine_log_submission_creates_structured_workout(tmp_path: Path
assert payload[0]['exercises'][1]['exercise_name'] == 'Upright Row' assert payload[0]['exercises'][1]['exercise_name'] == 'Upright Row'
def test_seeded_routine_log_uses_numeric_target_reps_when_reps_left_blank(tmp_path: Path) -> None:
client = make_client(tmp_path)
summary = client.get('/api/dashboard/summary').json()
shoulders = next(routine for routine in summary['routines'] if routine['name'] == 'Thursday: Shoulders')
response = client.post(
'/workouts/routine-log',
data={
'user_id': 'john',
'routine_id': shoulders['id'],
'performed_at': '2026-05-28T12:00:00',
'routine_name': shoulders['name'],
'notes': 'Use default target reps when left blank.',
'exercise_count': '1',
'exercise_0_name': 'Shoulder Press',
'exercise_0_set_count': '3',
'exercise_0_set_0_reps': '',
'exercise_0_set_0_weight_lbs': '45',
'exercise_0_set_1_reps': '',
'exercise_0_set_1_weight_lbs': '45',
'exercise_0_set_2_reps': '',
'exercise_0_set_2_weight_lbs': '45',
},
follow_redirects=False,
)
assert response.status_code == 303
assert response.headers['location'] == '/'
workout_history = client.get('/api/history/john/workouts')
assert workout_history.status_code == 200
payload = workout_history.json()
assert payload[0]['routine_name'] == 'Thursday: Shoulders'
assert payload[0]['exercises'][0]['sets'] == [
{'reps': 10, 'weight_lbs': 45.0},
{'reps': 10, 'weight_lbs': 45.0},
{'reps': 10, 'weight_lbs': 45.0},
]
def test_existing_state_gets_seeded_routine_backfill(tmp_path: Path) -> None: def test_existing_state_gets_seeded_routine_backfill(tmp_path: Path) -> None:
state_path = tmp_path / 'state.json' state_path = tmp_path / 'state.json'
state_path.write_text( state_path.write_text(