From ce28f7f749d8934b1b603b2399f51d75bc6379d8 Mon Sep 17 00:00:00 2001 From: Peter Stockings Date: Sun, 8 Feb 2026 16:31:05 +1100 Subject: [PATCH] Add exercise category search in settings --- features/exercises.py | 46 ++++++++++++++++++++++++++++++++--------- routes/exercises.py | 14 +++++++++++++ templates/settings.html | 22 ++++++++++++++++---- 3 files changed, 68 insertions(+), 14 deletions(-) diff --git a/features/exercises.py b/features/exercises.py index 8c3af98..b2a25bb 100644 --- a/features/exercises.py +++ b/features/exercises.py @@ -3,16 +3,42 @@ class Exercises: self.execute = db_connection_method def get(self, query): - # Add wildcards to the query - search_query = f"%{query}%" - # We need to fetch exercises with their attributes. - # Since an exercise can have many attributes, we'll fetch basic info first or use a join. - # But wait, the settings page just lists names. We can fetch attributes separately for each row or do a group_concat-like join. - # However, for the settings list, we want to show the tags. - - # Let's use a simpler approach: fetch exercises and then for each one (or via a single join) get attributes. - exercises = self.execute("SELECT exercise_id, name FROM exercise WHERE LOWER(name) LIKE LOWER(%s) ORDER BY name ASC;", [search_query]) - + if not query: + exercises = self.execute("SELECT exercise_id, name FROM exercise ORDER BY name ASC;") + for ex in exercises: + ex['attributes'] = self.get_exercise_attributes(ex['exercise_id']) + return exercises + + # Check for category:value syntax + if ':' in 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: ex['attributes'] = self.get_exercise_attributes(ex['exercise_id']) diff --git a/routes/exercises.py b/routes/exercises.py index 18fd1b6..501bea2 100644 --- a/routes/exercises.py +++ b/routes/exercises.py @@ -93,6 +93,20 @@ def add_exercise(): 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) +@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//delete", methods=['DELETE']) @login_required @admin_required diff --git a/templates/settings.html b/templates/settings.html index cd5ed38..948662e 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -189,16 +189,30 @@ d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"> - + placeholder="Search e.g. 'muscle:chest'..." + hx-get="/exercises/search" + hx-trigger="input changed delay:250ms, search" + hx-target="#new-exercise" hx-indicator="#search-spinner"> +
+ + + + + +
+ hx-target="closest tr" hx-swap="innerHTML swap:0.5s"> {% for exercise in exercises %} {{ render_partial('partials/exercise.html', exercise_id=exercise.exercise_id, name=exercise.name, attributes=exercise.attributes)}}