Integrate session note and tag editing and center progress controls

This commit is contained in:
Peter Stockings
2026-09-06 22:38:11 +10:00
parent d5e3e1d930
commit dc0f41cd27
12 changed files with 156 additions and 110 deletions
+3 -3
View File
@@ -84,7 +84,7 @@ def get_person_notes(person_id):
def get_workout_note_edit_form(person_id, workout_id):
"""Returns the form to edit a specific workout note."""
note = _fetch_workout_note(person_id, workout_id) # Use local helper
return render_template('partials/workout_note.html', person_id=person_id, workout_id=workout_id, note=note, is_edit=True)
return render_template('partials/session_note.html' if request.args.get('view') == 'session' else 'partials/workout_note.html', person_id=person_id, workout_id=workout_id, note=note, is_edit=True)
@notes_bp.route("/person/<int:person_id>/workout/<int:workout_id>/note", methods=['PUT'])
@validate_workout
@@ -93,11 +93,11 @@ def update_workout_note(person_id, workout_id):
note = request.form.get('note')
_update_workout_note_for_person(person_id, workout_id, note) # Use local helper
db.activityRequest.log(current_user.id, 'UPDATE_NOTE', 'workout', workout_id, f"Updated note for workout {workout_id}")
return render_template('partials/workout_note.html', person_id=person_id, workout_id=workout_id, note=note)
return render_template('partials/session_note.html' if request.args.get('view') == 'session' else 'partials/workout_note.html', person_id=person_id, workout_id=workout_id, note=note)
@notes_bp.route("/person/<int:person_id>/workout/<int:workout_id>/note", methods=['GET'])
@validate_workout
def get_workout_note(person_id, workout_id):
"""Returns the display partial for a specific workout note."""
note = _fetch_workout_note(person_id, workout_id) # Use local helper
return render_template('partials/workout_note.html', person_id=person_id, workout_id=workout_id, note=note)
return render_template('partials/session_note.html' if request.args.get('view') == 'session' else 'partials/workout_note.html', person_id=person_id, workout_id=workout_id, note=note)
+2 -2
View File
@@ -186,7 +186,7 @@ def add_tag_to_workout(workout_id):
# Render the partial with the complete list of tags (selected and unselected)
return render_template('partials/workout_tags_list.html', tags=all_person_tags, person_id=person_id, workout_id=workout_id)
return render_template('partials/workout_tag_editor.html' if request.args.get('view') == 'session' else 'partials/workout_tags_list.html', tags=all_person_tags, person_id=person_id, workout_id=workout_id)
@tags_bp.route("/workout/<int:workout_id>/new", methods=['POST'])
@login_required
@@ -271,4 +271,4 @@ def create_new_tag_for_workout(workout_id):
# Render the partial with the complete list of tags
# Use 'tags' as the variable name consistent with the other endpoint and likely the partial
return render_template('partials/workout_tags_list.html', tags=all_person_tags, person_id=person_id, workout_id=workout_id)
return render_template('partials/workout_tag_editor.html' if request.args.get('view') == 'session' else 'partials/workout_tags_list.html', tags=all_person_tags, person_id=person_id, workout_id=workout_id)
+5
View File
@@ -32,3 +32,8 @@
.forecast-method{margin:14px 0;border-top:1px solid #e2e8f0;padding-top:12px}.forecast-method summary{font-size:12px;color:#475569;cursor:pointer;font-weight:500}.forecast-method p{margin:8px 0;font-size:12px;line-height:1.7;color:#64748b}
.forecast-facts{display:flex;flex-wrap:wrap;gap:8px 18px;margin:10px 0;font-size:11px;color:#64748b}.forecast-facts strong{color:#334155}
@media(max-width:520px){.progress-heading{flex-wrap:wrap;gap:7px}.progress-title{flex:1 1 180px}.progress-actions{margin-left:auto}.forecast-summary{padding:12px}.forecast-summary-grid{gap:10px}.forecast-primary{font-size:21px}.forecast-horizon-summary{padding-left:10px}.forecast-secondary{font-size:18px}.forecast-secondary span{display:block}.forecast-chart{height:190px}.forecast-readout-values{grid-template-columns:1fr 1fr}.forecast-readout-values>div:last-child{grid-column:1/-1;display:flex;align-items:center;gap:8px}.forecast-readout{min-height:120px}.forecast-chart-hint{font-size:9px}.forecast-table td,.forecast-table th{padding:9px}.exercise-progress>.inline-flex{display:flex}.exercise-progress>.inline-flex>div{padding:7px 12px;font-size:12px}}
/* Keep the exercise's achievements and time controls visually centered. */
.progress-badges{display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:6px;margin:6px 0 12px}
.progress-badges:empty{display:none}
.progress-epochs,.forecast-controls{justify-content:center}
+26 -6
View File
@@ -22,9 +22,7 @@
.workout-add-button:hover{background:#066975}
.workout-workspace button:focus-visible,.workout-workspace summary:focus-visible{outline:2px solid #0891a0;outline-offset:3px}
.workout-button{display:flex;align-items:center;gap:16px;padding:10px 14px;border:1px solid #dbe3e9;background:#fff;border-radius:8px;font-size:12px;cursor:pointer;list-style:none}
.workout-menu{position:relative;flex-shrink:0}
.workout-menu-content{position:absolute;right:0;top:calc(100% + 8px);z-index:20;padding:6px;background:#fff;border:1px solid #e1e7ec;border-radius:10px;min-width:180px;box-shadow:0 8px 24px #182c4214}
.workout-delete{color:#b42335;padding:10px;width:100%;text-align:left;border-radius:6px;font-size:13px}
.workout-delete{color:#8c5960;flex-shrink:0}.workout-delete:hover{color:#b42335}
.workout-delete:hover{background:#fff1f2}
.workout-results>.workout-section-heading{padding:22px 24px 18px;margin:0}
.workout-table{width:100%;border-collapse:collapse;table-layout:fixed;text-align:left}
@@ -55,9 +53,6 @@
.workout-disclosure[open]>summary:after{content:'−'}
.workout-disclosure>summary .workout-subtle{display:block;margin-top:3px}
.workout-disclosure-body{padding:0 20px 20px}
.workout-disclosure-body [id^='workout-note-'] a{float:none;padding-left:0}
.workout-disclosure-body [id^='tag-form-w-']>div{display:block}
.workout-disclosure-body [id^='tag-form-w-']>div>div{width:100%;padding:8px 0;margin:0}
.workout-breakdown h3{font-size:11px;font-weight:650;color:#64748b;margin:0 0 12px}
.workout-distribution-bar{display:flex;gap:2px;height:7px;border-radius:4px;overflow:hidden;margin-bottom:14px}
.workout-distribution-legend{display:grid;gap:11px;font-size:12px}
@@ -82,3 +77,28 @@
@media(max-width:1100px){.workout-layout{grid-template-columns:minmax(0,1fr) 260px;gap:16px}.workout-logger{padding:20px}.workout-set-fields{grid-template-columns:minmax(0,1fr) 76px 90px;gap:10px}.workout-set-submit{grid-column:1/-1}.workout-add-button{width:100%}}
@media(max-width:900px){.workout-layout{grid-template-columns:minmax(0,1fr)}.workout-sidebar{grid-template-columns:repeat(2,minmax(0,1fr));align-items:start}.workout-set-fields{grid-template-columns:minmax(140px,1fr) 84px 104px auto}.workout-set-submit{grid-column:auto}}
@media(max-width:600px){.workout-workspace{padding:4px 0 24px}.workout-workspace nav{margin-bottom:20px}.workout-header{gap:8px;align-items:flex-start;margin-bottom:18px}.workout-header h1{font-size:24px}.workout-header h1 span{display:block;font-size:16px;letter-spacing:0;margin-top:4px}.workout-button{gap:8px;padding:8px 10px;font-size:11px}.workout-main,.workout-sidebar{gap:14px}.workout-sidebar{grid-template-columns:minmax(0,1fr)}.workout-logger{padding:18px 16px}.workout-section-heading{margin-bottom:18px}.workout-section-heading>.workout-subtle{display:none}.workout-set-fields{grid-template-columns:1fr 1fr;gap:14px 10px}.workout-set-field:first-child,.workout-set-submit{grid-column:1/-1}.workout-results>.workout-section-heading{padding:18px 14px}.workout-table>thead th{padding:10px 12px}.workout-table>thead th:first-child{width:49%}.workout-table>thead th:last-child{width:44px}.workout-table .workout-set-row>td{padding:16px 12px;font-size:12px}.workout-table .workout-set-row>td:last-child{padding:6px 0}.workout-table .workout-set-row>td:last-child>div{flex-direction:column}.workout-exercise-tools{gap:8px}.workout-exercise-tools button{font-size:10px;min-height:30px}.workout-exercise-tools svg{display:none}.workout-table .extra-content-container:not(:empty){padding:14px 10px}.workout-workspace .forecast-chart{height:190px}.workout-summary dl{margin:14px 0}.workout-summary{padding:18px 20px}}
/* Direct session editing: the card itself is the control. */
.workout-note-trigger{display:block;width:100%;padding:20px;text-align:left;border-radius:14px}
.workout-note-trigger:hover{background:#f8fafc}
.workout-inline-heading{display:flex;align-items:center;justify-content:space-between;gap:12px;font-size:14px;font-weight:650;margin-bottom:7px}
.workout-inline-action{font-size:11px;font-weight:500;color:#087f8c}
.workout-note-preview{display:block;white-space:pre-wrap;overflow-wrap:anywhere;font-size:13px;line-height:1.7;color:#475569;max-height:160px;overflow:auto}
.workout-note-editor{padding:20px}
.workout-note-editor>label,.workout-tags-card>h2{display:block;font-size:14px;font-weight:650;margin-bottom:12px}
.workout-note-editor textarea,.workout-tag-create input{width:100%;min-width:0;background:#f8fafc;border:1px solid #dbe3e9;border-radius:8px;padding:10px 12px;font-size:13px;color:#253449}
.workout-note-editor textarea{display:block;resize:vertical;line-height:1.6;min-height:132px}
.workout-note-editor textarea:focus,.workout-tag-create input:focus{outline:2px solid #0891a0;outline-offset:2px}
.workout-editor-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:12px}
.workout-editor-actions button{min-height:38px;font-size:12px;padding:8px 12px}
.workout-editor-error{font-size:12px;line-height:1.5;color:#b42335;margin-top:10px}
.workout-tags-card{padding:20px}
.workout-tag-choices{display:flex;flex-wrap:wrap;gap:8px;max-height:180px;overflow:auto;padding:4px;margin:8px -4px 12px}
.workout-tag-choice{display:flex;align-items:center;gap:6px;padding:7px 9px;border:1px solid #dbe3e9;border-radius:8px;font-size:12px;color:#64748b;cursor:pointer;overflow-wrap:anywhere;max-width:100%}
.workout-tag-choice:has(input:checked){background:#edf7f8;border-color:#9acbd0;color:#086e79}
.workout-tag-choice:hover{border-color:#0891a0}
.workout-tag-choice input{width:13px;height:13px;flex-shrink:0;accent-color:#087f8c}
.workout-tag-choice:has(input:focus-visible){outline:2px solid #0891a0;outline-offset:2px}
.workout-tag-create{display:flex;gap:8px}
.workout-tag-create button{flex-shrink:0;padding:8px 10px;color:#087f8c}
.workout-tag-editor input:disabled,.workout-tag-editor button:disabled,.workout-note-editor button:disabled{opacity:.55;cursor:wait}
+2 -2
View File
@@ -15,10 +15,10 @@
<link href="/static/css/tailwind.css" rel="stylesheet">
<link href="/static/css/style.css" rel="stylesheet">
<link href="/static/css/workout.css?v=1" rel="stylesheet">
<link href="/static/css/workout.css?v=2" rel="stylesheet">
<script src="/static/js/htmx.min.js" defer></script>
<script src="/static/js/hyperscript.min.js" defer></script>
<link rel="stylesheet" href="/static/css/forecast.css?v=2">
<link rel="stylesheet" href="/static/css/forecast.css?v=3">
<script src="/static/js/forecast.js?v=2" defer></script>
</head>
+2 -2
View File
@@ -4,13 +4,13 @@
<div class="progress-heading">
<div class="progress-title">
<h4>{{ title }}</h4>
<div hx-get="{{ url_for('workout.get_topset_achievements', person_id=person_id, workout_id=latest_workout_id, topset_id=latest_topset_id) }}"
hx-trigger="load" hx-target="this" hx-swap="innerHTML"></div>
</div>
<button class="progress-action" title="Show Progress Graph"
hx-get="{{ url_for('get_exercise_progress_for_user', person_id=person_id, exercise_id=exercise_id, epoch=selected_epoch, forecast='true' if forecast_enabled else 'false', forecast_weeks=forecast_weeks, forecast_reps=forecast_target_reps, min_date=min_date, max_date=max_date) }}"
hx-target="#exercise-history-container" hx-swap="outerHTML">Graph</button>
</div>
<div class="progress-badges" aria-label="Progress achievements" hx-get="{{ url_for('workout.get_topset_achievements', person_id=person_id, workout_id=latest_workout_id, topset_id=latest_topset_id) }}"
hx-trigger="load" hx-target="this" hx-swap="innerHTML"></div>
{% if best_fit_formula and not forecast_enabled %}
<h2 class="text-xs font-semibold text-blue-200 mb-4 text-center">
{{ best_fit_formula.kg_per_week }} kg/week, {{ best_fit_formula.kg_per_month }} kg/month
+28
View File
@@ -0,0 +1,28 @@
<section id="workout-note-{{ workout_id }}" class="workout-card workout-note-card"
hx-target="this" hx-swap="outerHTML">
{% if is_edit|default(false) %}
<form class="workout-note-editor"
hx-put="{{ url_for('notes.update_workout_note', person_id=person_id, workout_id=workout_id, view='session') }}"
hx-disabled-elt="find button"
_="on htmx:responseError remove .hidden from #note-error-{{ workout_id }}">
<label for="session-note-input-{{ workout_id }}">Session notes</label>
<textarea id="session-note-input-{{ workout_id }}" name="note" rows="5"
placeholder="Cues, energy, how it felt…"
_="init call me.focus() end">{{ note or '' }}</textarea>
<p id="note-error-{{ workout_id }}" class="workout-editor-error hidden" role="alert">Could not save your note. Please try again.</p>
<div class="workout-editor-actions">
<button type="button" class="workout-button"
hx-get="{{ url_for('notes.get_workout_note', person_id=person_id, workout_id=workout_id, view='session') }}">Cancel</button>
<button type="submit" class="workout-add-button">Save note</button>
</div>
</form>
{% else %}
<button type="button" class="workout-note-trigger"
hx-get="{{ url_for('notes.get_workout_note_edit_form', person_id=person_id, workout_id=workout_id, view='session') }}"
aria-label="{{ 'Edit session notes' if note else 'Add session notes' }}">
<span class="workout-inline-heading">Session notes <span class="workout-inline-action">{{ 'Edit' if note else '+ Add' }}</span></span>
{% if note %}<span class="workout-note-preview">{% for line in note.split('\n') %}{{ line }}{% if not loop.last %}<br>{% endif %}{% endfor %}</span>
{% else %}<span class="workout-subtle">Cues, energy, how it felt</span>{% endif %}
</button>
{% endif %}
</section>
+2 -2
View File
@@ -64,8 +64,6 @@ M{% for value, position in lower_points %}{{ (((position * vb_width) + margin) |
<div class="progress-heading">
<div class="progress-title">
<h4>{{ title }}</h4>
<div hx-get="{{ url_for('workout.get_topset_achievements', person_id=person_id, workout_id=latest_workout_id, topset_id=latest_topset_id) }}"
hx-trigger="load" hx-target="this" hx-swap="innerHTML"></div>
</div>
<div class="progress-actions">
<button class="progress-action" title="History and forecast tables"
@@ -78,6 +76,8 @@ M{% for value, position in lower_points %}{{ (((position * vb_width) + margin) |
hx-target="#svg-plot-{{ unique_id }}" hx-swap="outerHTML">Forecast {{ 'on' if forecast_enabled else 'off' }}</button>
</div>
</div>
<div class="progress-badges" aria-label="Progress achievements" hx-get="{{ url_for('workout.get_topset_achievements', person_id=person_id, workout_id=latest_workout_id, topset_id=latest_topset_id) }}"
hx-trigger="load" hx-target="this" hx-swap="innerHTML"></div>
<h2 class="text-xs font-semibold text-blue-200 mb-1 text-center">
{% if best_fit_formula and not forecast_enabled %}
{{ best_fit_formula.kg_per_week }} kg/week, {{ best_fit_formula.kg_per_month }} kg/month
@@ -0,0 +1,28 @@
<div id="workout-tag-editor-{{ workout_id }}" class="workout-tag-editor">
<form hx-post="{{ url_for('tags.add_tag_to_workout', workout_id=workout_id, view='session') }}"
hx-trigger="change" hx-target="#workout-tag-editor-{{ workout_id }}" hx-swap="outerHTML"
hx-disabled-elt="#workout-tag-editor-{{ workout_id }} input, #workout-tag-editor-{{ workout_id }} button"
_="on htmx:responseError remove .hidden from #tag-error-{{ workout_id }}">
<fieldset>
<legend class="workout-subtle">Select tags for this session</legend>
<div class="workout-tag-choices">
{% for tag in tags %}
<label class="workout-tag-choice">
<input id="workout-{{ workout_id }}-tag-{{ tag.tag_id }}" type="checkbox" name="tag_id" value="{{ tag.tag_id }}" {% if tag.is_selected %}checked{% endif %}>
<span>{{ tag.tag_name }}</span>
</label>
{% else %}<p class="workout-subtle">Create your first tag below.</p>{% endfor %}
</div>
</fieldset>
</form>
<form class="workout-tag-create"
hx-post="{{ url_for('tags.create_new_tag_for_workout', workout_id=workout_id, view='session') }}"
hx-target="#workout-tag-editor-{{ workout_id }}" hx-swap="outerHTML"
hx-disabled-elt="#workout-tag-editor-{{ workout_id }} input, #workout-tag-editor-{{ workout_id }} button"
_="on htmx:responseError remove .hidden from #tag-error-{{ workout_id }}">
<label for="new-workout-tag-{{ workout_id }}" class="sr-only">New tag name</label>
<input id="new-workout-tag-{{ workout_id }}" type="text" name="tag_name" placeholder="New tag…" required>
<button type="submit" class="workout-button">Add tag</button>
</form>
<p id="tag-error-{{ workout_id }}" class="workout-editor-error hidden" role="alert">Could not save tags. Please try again.</p>
</div>
+4 -73
View File
@@ -1,73 +1,4 @@
<div class="flex">
<div id="tag-wrapper-w-{{ workout_id }}">
{{ render_partial('partials/workout_tags_list.html', tags=tags)}}
</div>
<span id="tag-form-show-w-{{ workout_id }}" class="cursor-pointer" _="on click
remove .hidden from #tag-form-w-{{ workout_id }}
then remove .hidden from #tag-form-hide-w-{{ workout_id }}
then add .hidden">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
class="w-6 h-6" data-darkreader-inline-stroke="" style="--darkreader-inline-stroke: currentColor;">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"></path>
</svg>
</span>
<span id="tag-form-hide-w-{{ workout_id }}" _="on click
add .hidden to #tag-form-w-{{ workout_id }}
then remove .hidden from #tag-form-show-w-{{ workout_id }}
then add .hidden" class="cursor-pointer hidden">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15" />
</svg>
</span>
</div>
<div id="tag-form-w-{{ workout_id }}" class="hidden pt-2 pb-3">
<span class="text-base font-normal text-gray-500">Add tag to workout</span>
<div class="flex pt-2 flex-col sm:flex-row pb-2">
<div class="w-full md:w-1/2 px-3 mb-6 md:mb-0">
<label class="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2" for="grid-state">
Tag
</label>
<div class="relative">
<div class="w-full">
{{ render_partial('partials/custom_select.html',
name='tag_id',
options=tags,
multiple=true,
search=true,
placeholder='Select tags',
hx_post=url_for('tags.add_tag_to_workout', workout_id=workout_id),
hx_target='#tag-wrapper-w-' + workout_id|string)
}}
</div>
</div>
</div>
<div class="w-full md:w-1/2 px-3 mb-6 md:mb-0">
<label class="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2">
New tag name
</label>
<div class="flex">
<input
class="appearance-none block w-full bg-gray-200 text-gray-700 border border-gray-200 rounded py-3 px-4 leading-tight focus:outline-none focus:bg-white focus:border-gray-500"
type="text" name="tag_name">
<button type="submit" hx-post="{{ url_for('tags.create_new_tag_for_workout', workout_id=workout_id) }}"
hx-include="[name='tag_name']" hx-target="#tag-wrapper-w-{{ workout_id }}"
class="p-2.5 ml-2 text-sm font-medium text-white bg-blue-700 rounded-lg border border-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-6 h-6" data-darkreader-inline-stroke=""
style="--darkreader-inline-stroke: currentColor;">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"></path>
</svg>
<span class="sr-only">Search</span>
</button>
</div>
</div>
</div>
</div>
<section class="workout-card workout-tags-card" aria-labelledby="workout-tags-heading-{{ workout_id }}">
<h2 id="workout-tags-heading-{{ workout_id }}">Tags</h2>
{{ render_partial('partials/workout_tag_editor.html', person_id=person_id, workout_id=workout_id, tags=tags) }}
</section>
+5 -20
View File
@@ -46,14 +46,9 @@
<h1>{{ person_name }}<span> / Workout</span></h1>
{{ render_partial('partials/start_date.html', person_id=person_id, workout_id=workout_id, start_date=start_date) }}
</div>
<details class="workout-menu">
<summary class="workout-button">Session options <span aria-hidden="true">&middot;&middot;&middot;</span></summary>
<div class="workout-menu-content">
<button type="button" class="workout-delete"
hx-get="{{ url_for('workout.delete_workout', person_id=person_id, workout_id=workout_id) }}"
hx-confirm="Are you sure you wish to delete this workout?" hx-push-url="true" hx-target="#container">Delete workout</button>
</div>
</details>
<button type="button" class="workout-button workout-delete"
hx-get="{{ url_for('workout.delete_workout', person_id=person_id, workout_id=workout_id) }}"
hx-confirm="Are you sure you wish to delete this workout?" hx-push-url="true" hx-target="#container">Delete workout</button>
</header>
<div class="workout-layout">
@@ -96,18 +91,8 @@
</details>
</div>
</details>
<details class="workout-card workout-disclosure" {% if note %}open{% endif %}>
<summary>Session notes <span class="workout-subtle">Cues, energy, how it felt</span></summary>
<div class="workout-disclosure-body">
{{ render_partial('partials/workout_note.html', person_id=person_id, workout_id=workout_id, note=note or '') }}
</div>
</details>
<details class="workout-card workout-disclosure">
<summary>Tags <span class="workout-subtle">Organize this session</span></summary>
<div class="workout-disclosure-body">
{{ render_partial('partials/workout_tags.html', person_id=person_id, workout_id=workout_id, tags=tags) }}
</div>
</details>
{{ render_partial('partials/session_note.html', person_id=person_id, workout_id=workout_id, note=note or '') }}
{{ render_partial('partials/workout_tags.html', person_id=person_id, workout_id=workout_id, tags=tags) }}
</aside>
</div>
</div>
+49
View File
@@ -120,6 +120,55 @@ class WorkoutViewTests(unittest.TestCase):
self.assertEqual(response.status_code, 302)
self.db.execute.assert_not_called()
def test_session_note_editor_round_trip_keeps_integrated_card(self):
import routes.notes as notes
text = 'Good energy\nKeep <cues> simple'
with patch.object(notes, 'db', self.db), patch.object(notes, '_fetch_workout_note', return_value=text), patch.object(notes, '_update_workout_note_for_person') as save:
edit = self.client.get(self.base + '/note/edit?view=session')
self.assertIn(b'workout-note-editor', edit.data)
# The HTML minifier can unescape RCDATA inside a textarea.
self.assertIn(b'Keep <cues> simple</textarea>', edit.data)
self.assertIn(b'Save note', edit.data)
saved = self.client.put(self.base + '/note?view=session', data={'note': text})
save.assert_called_once_with(1, 1, text)
self.assertIn(b'workout-note-trigger', saved.data)
self.assertIn(b'Good energy<br>Keep &lt;cues> simple', saved.data)
self.assertNotIn(b'<textarea', saved.data)
canceled = self.client.get(self.base + '/note?view=session')
self.assertIn(b'workout-note-preview', canceled.data)
legacy = self.client.get(self.base + '/note')
self.assertNotIn(b'workout-note-card', legacy.data)
def test_tags_return_updated_controls_after_select_clear_and_create(self):
import routes.tags as tags
tag = dict(tag_id=12, tag_name='Push', is_selected=True)
def execute(query, args=(), **kwargs):
if 'SELECT person_id' in query:
return {'person_id': 1}
if 'SELECT tag_id FROM Tag' in query:
return {'tag_id': 12}
if 'SELECT DISTINCT' in query:
return []
if 'FROM tag' in query:
return [tag]
return []
self.db.execute.side_effect = execute
with patch.object(tags, 'db', self.db):
for selected in ({'tag_id': '12'}, {}):
tag['is_selected'] = bool(selected)
response = self.client.post('/tag/workout/1/add?view=session', data=selected)
self.assertEqual(response.status_code, 200)
self.assertIn(b'workout-tag-editor-1', response.data)
self.assertIn(b'type=checkbox', response.data)
self.assertEqual(b' checked' in response.data, bool(selected))
tag['is_selected'] = True
response = self.client.post('/tag/workout/1/new?view=session', data={'tag_name': 'Push'})
self.assertEqual(response.status_code, 200)
self.assertIn(b'workout-tag-editor-1', response.data)
self.assertIn(b' checked', response.data)
if __name__ == '__main__':
unittest.main()