Add exercise category search in settings

This commit is contained in:
Peter Stockings
2026-02-08 16:31:05 +11:00
parent 31f738cfb3
commit ce28f7f749
3 changed files with 68 additions and 14 deletions

View File

@@ -3,16 +3,42 @@ class Exercises:
self.execute = db_connection_method self.execute = db_connection_method
def get(self, query): def get(self, query):
# Add wildcards to the query if not query:
search_query = f"%{query}%" exercises = self.execute("SELECT exercise_id, name FROM exercise ORDER BY name ASC;")
# We need to fetch exercises with their attributes. for ex in exercises:
# Since an exercise can have many attributes, we'll fetch basic info first or use a join. ex['attributes'] = self.get_exercise_attributes(ex['exercise_id'])
# But wait, the settings page just lists names. We can fetch attributes separately for each row or do a group_concat-like join. return exercises
# However, for the settings list, we want to show the tags.
# Check for category:value syntax
# Let's use a simpler approach: fetch exercises and then for each one (or via a single join) get attributes. if ':' in query:
exercises = self.execute("SELECT exercise_id, name FROM exercise WHERE LOWER(name) LIKE LOWER(%s) ORDER BY name ASC;", [search_query]) category_part, value_part = query.split(':', 1)
category_part = f"%{category_part.strip().lower()}%"
value_part = f"%{value_part.strip().lower()}%"
query = """
SELECT DISTINCT e.exercise_id, e.name
FROM exercise e
JOIN exercise_to_attribute eta ON e.exercise_id = eta.exercise_id
JOIN exercise_attribute attr ON eta.attribute_id = attr.attribute_id
JOIN exercise_attribute_category cat ON attr.category_id = cat.category_id
WHERE LOWER(cat.name) LIKE LOWER(%s) AND LOWER(attr.name) LIKE LOWER(%s)
ORDER BY e.name ASC;
"""
exercises = self.execute(query, [category_part, value_part])
else:
# Fallback: search in name OR attribute name
search_term = query.strip().lower()
search_query = f"%{search_term}%"
query = """
SELECT DISTINCT e.exercise_id, e.name
FROM exercise e
LEFT JOIN exercise_to_attribute eta ON e.exercise_id = eta.exercise_id
LEFT JOIN exercise_attribute attr ON eta.attribute_id = attr.attribute_id
WHERE LOWER(e.name) LIKE LOWER(%s) OR LOWER(attr.name) LIKE LOWER(%s)
ORDER BY e.name ASC;
"""
exercises = self.execute(query, [search_query, search_query])
for ex in exercises: for ex in exercises:
ex['attributes'] = self.get_exercise_attributes(ex['exercise_id']) ex['attributes'] = self.get_exercise_attributes(ex['exercise_id'])

View File

@@ -93,6 +93,20 @@ def add_exercise():
person_id = request.args.get('person_id', type=int) person_id = request.args.get('person_id', type=int)
return render_template('partials/exercise/exercise_list_item.html', exercise=new_exercise, person_id=person_id) return render_template('partials/exercise/exercise_list_item.html', exercise=new_exercise, person_id=person_id)
@exercises_bp.route("/exercises/search")
@login_required
def search_exercises():
query = request.args.get('q', '')
exercises = db.exercises.get(query)
html = ""
for exercise in exercises:
html += render_template('partials/exercise.html',
exercise_id=exercise['exercise_id'],
name=exercise['name'],
attributes=exercise['attributes'])
return html
@exercises_bp.route("/exercise/<int:exercise_id>/delete", methods=['DELETE']) @exercises_bp.route("/exercise/<int:exercise_id>/delete", methods=['DELETE'])
@login_required @login_required
@admin_required @admin_required

View File

@@ -189,16 +189,30 @@
d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"></path> d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"></path>
</svg> </svg>
</div> </div>
<input type="search" id="exercise-search" <input type="search" id="exercise-search" name="q"
class="block w-full p-2 pl-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-cyan-500 focus:border-cyan-500 transition-all shadow-sm" class="block w-full p-2 pl-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-cyan-500 focus:border-cyan-500 transition-all shadow-sm"
placeholder="Search exercises..." placeholder="Search e.g. 'muscle:chest'..."
_="on input show <tbody>tr/> in closest <table/> when its textContent.toLowerCase() contains my value.toLowerCase()"> hx-get="/exercises/search"
hx-trigger="input changed delay:250ms, search"
hx-target="#new-exercise" hx-indicator="#search-spinner">
<div id="search-spinner"
class="htmx-indicator absolute inset-y-0 right-3 flex items-center">
<svg class="animate-spin h-4 w-4 text-cyan-600"
xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10"
stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
</path>
</svg>
</div>
</div> </div>
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody class="bg-white divide-y divide-gray-100" id="new-exercise" <tbody class="bg-white divide-y divide-gray-100" id="new-exercise"
hx-target="closest tr" hx-swap="outerHTML swap:0.5s"> hx-target="closest tr" hx-swap="innerHTML swap:0.5s">
{% for exercise in exercises %} {% for exercise in exercises %}
{{ render_partial('partials/exercise.html', exercise_id=exercise.exercise_id, {{ render_partial('partials/exercise.html', exercise_id=exercise.exercise_id,
name=exercise.name, attributes=exercise.attributes)}} name=exercise.name, attributes=exercise.attributes)}}