Add dropdown exercise selection to Doris Barbell logs
This commit is contained in:
@@ -47,8 +47,9 @@ Tech stack: FastAPI, Jinja2, JSON state store for V1 bootstrap, Docker Compose o
|
|||||||
- Homepage now renders current metrics, recent workouts, exercise bests, and placeholder template/routine scaffolding.
|
- Homepage now renders current metrics, recent workouts, exercise bests, and placeholder template/routine scaffolding.
|
||||||
- Homepage now renders John's imported five-day split as the default routine board.
|
- Homepage now renders John's imported five-day split as the default routine board.
|
||||||
- Homepage now includes mobile-friendly HTML forms for quick weight entry, body measurements, exercise templates, routine drafts, and quick workout logging.
|
- Homepage now includes mobile-friendly HTML forms for quick weight entry, body measurements, exercise templates, routine drafts, and quick workout logging.
|
||||||
|
- Routine log sheets now support dropdown exercise selection with set-by-set rep and weight entry for seeded program days.
|
||||||
- Homepage now includes John-focused recent history snapshots so the app is useful before charts exist.
|
- Homepage now includes John-focused recent history snapshots so the app is useful before charts exist.
|
||||||
- Tests currently cover health, seeded users, BMI calculation, imperial measurement logging, exercise template creation, routine creation, history ordering, structured workout logging, progression summaries, homepage render, and form-post redirects.
|
- Tests currently cover health, seeded users, BMI calculation, imperial measurement logging, exercise template creation, routine creation, history ordering, structured workout logging, progression summaries, homepage render, routine log flows, and form-post redirects.
|
||||||
|
|
||||||
## Next build slices
|
## Next build slices
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ The current scaffold already supports:
|
|||||||
- exercise template creation/listing
|
- exercise template creation/listing
|
||||||
- routine creation/listing on top of John's imported base split
|
- routine creation/listing on top of John's imported base split
|
||||||
- structured workout logging with nested exercises and sets
|
- structured workout logging with nested exercises and sets
|
||||||
|
- dedicated routine log sheets with dropdown exercise selection plus set-by-set reps and weight entry
|
||||||
- history endpoints for weight, measurements, and workouts
|
- history endpoints for weight, measurements, and workouts
|
||||||
- exercise progression summaries with max weight and estimated 1RM per exercise
|
- exercise progression summaries with max weight and estimated 1RM per exercise
|
||||||
- mobile-friendly homepage with HTML forms for:
|
- mobile-friendly homepage with HTML forms for:
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ def routine_log_sheet(request: Request, routine_id: str):
|
|||||||
'title': f"Log {routine['name']}",
|
'title': f"Log {routine['name']}",
|
||||||
'routine': routine,
|
'routine': routine,
|
||||||
'routine_sheet': _build_routine_log_sheet(routine),
|
'routine_sheet': _build_routine_log_sheet(routine),
|
||||||
|
'exercise_options': _exercise_options_for_routine(request, routine),
|
||||||
'routine_action_label': _routine_action_label(routine['name']),
|
'routine_action_label': _routine_action_label(routine['name']),
|
||||||
'default_performed_at': _default_performed_at_value(),
|
'default_performed_at': _default_performed_at_value(),
|
||||||
},
|
},
|
||||||
@@ -285,6 +286,24 @@ def _build_routine_log_sheet(routine: dict) -> list[dict]:
|
|||||||
return sheet
|
return sheet
|
||||||
|
|
||||||
|
|
||||||
|
def _exercise_options_for_routine(request: Request, routine: dict) -> list[str]:
|
||||||
|
routine_names = [exercise['exercise_name'] for exercise in routine.get('exercises', []) if exercise.get('exercise_name')]
|
||||||
|
template_names = [
|
||||||
|
template['name']
|
||||||
|
for template in request.app.state.store.list_exercise_templates()
|
||||||
|
if template.get('name')
|
||||||
|
]
|
||||||
|
ordered = []
|
||||||
|
seen = set()
|
||||||
|
for name in [*routine_names, *template_names]:
|
||||||
|
normalized = name.strip().lower()
|
||||||
|
if normalized in seen:
|
||||||
|
continue
|
||||||
|
ordered.append(name)
|
||||||
|
seen.add(normalized)
|
||||||
|
return ordered
|
||||||
|
|
||||||
|
|
||||||
def _default_reps_value(target_reps: str | None) -> str:
|
def _default_reps_value(target_reps: str | None) -> str:
|
||||||
if target_reps is None:
|
if target_reps is None:
|
||||||
return ''
|
return ''
|
||||||
|
|||||||
@@ -52,13 +52,18 @@
|
|||||||
|
|
||||||
{% 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 }}_name" value="{{ exercise.exercise_name }}">
|
|
||||||
<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 }}">
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
<h3>{{ exercise.exercise_name }}</h3>
|
<label>Exercise
|
||||||
|
<select name="exercise_{{ exercise.exercise_index }}_name">
|
||||||
|
{% for option_name in exercise_options %}
|
||||||
|
<option value="{{ option_name }}" {% if option_name == exercise.exercise_name %}selected{% endif %}>{{ option_name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
<p class="muted">Target reps: {{ exercise.target_reps or 'enter manually' }}</p>
|
<p class="muted">Target reps: {{ exercise.target_reps or 'enter manually' }}</p>
|
||||||
<div class="grid cols-2 compact-grid">
|
<div class="grid cols-2 compact-grid">
|
||||||
{% for set in exercise.sets %}
|
{% for set in exercise.sets %}
|
||||||
|
|||||||
@@ -388,6 +388,9 @@ def test_seeded_routine_can_render_prefilled_log_sheet(tmp_path: Path) -> None:
|
|||||||
assert 'Thursday: Shoulders' in body
|
assert 'Thursday: Shoulders' in body
|
||||||
assert 'Shoulder Press' in body
|
assert 'Shoulder Press' in body
|
||||||
assert 'Save shoulder day workout' in body
|
assert 'Save shoulder day workout' in body
|
||||||
|
assert 'name="exercise_0_name"' in body
|
||||||
|
assert '<option value="Shoulder Press" selected>' in body
|
||||||
|
assert 'value="Upright Row"' in body
|
||||||
assert 'name="exercise_0_set_0_reps"' in body
|
assert 'name="exercise_0_set_0_reps"' in body
|
||||||
assert 'name="exercise_0_set_0_weight_lbs"' in body
|
assert 'name="exercise_0_set_0_weight_lbs"' in body
|
||||||
assert 'value="10"' in body
|
assert 'value="10"' in body
|
||||||
@@ -417,7 +420,7 @@ def test_seeded_routine_log_submission_creates_structured_workout(tmp_path: Path
|
|||||||
'exercise_0_set_1_weight_lbs': '80',
|
'exercise_0_set_1_weight_lbs': '80',
|
||||||
'exercise_0_set_2_reps': '8',
|
'exercise_0_set_2_reps': '8',
|
||||||
'exercise_0_set_2_weight_lbs': '90',
|
'exercise_0_set_2_weight_lbs': '90',
|
||||||
'exercise_1_name': 'Arnold Press',
|
'exercise_1_name': 'Upright Row',
|
||||||
'exercise_1_set_count': '3',
|
'exercise_1_set_count': '3',
|
||||||
'exercise_1_set_0_reps': '10',
|
'exercise_1_set_0_reps': '10',
|
||||||
'exercise_1_set_0_weight_lbs': '35',
|
'exercise_1_set_0_weight_lbs': '35',
|
||||||
@@ -441,6 +444,7 @@ def test_seeded_routine_log_submission_creates_structured_workout(tmp_path: Path
|
|||||||
assert payload[0]['total_volume_lbs'] == 3240
|
assert payload[0]['total_volume_lbs'] == 3240
|
||||||
assert payload[0]['exercises'][0]['exercise_name'] == 'Shoulder Press'
|
assert payload[0]['exercises'][0]['exercise_name'] == 'Shoulder Press'
|
||||||
assert payload[0]['exercises'][0]['sets'][2] == {'reps': 8, 'weight_lbs': 90.0}
|
assert payload[0]['exercises'][0]['sets'][2] == {'reps': 8, 'weight_lbs': 90.0}
|
||||||
|
assert payload[0]['exercises'][1]['exercise_name'] == 'Upright Row'
|
||||||
|
|
||||||
|
|
||||||
def test_existing_state_gets_seeded_routine_backfill(tmp_path: Path) -> None:
|
def test_existing_state_gets_seeded_routine_backfill(tmp_path: Path) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user