Make stats refresh, and add filter support to stats endpoint

This commit is contained in:
Peter Stockings
2025-01-26 23:01:39 +11:00
parent 23de6ef1f7
commit 527395d704
10 changed files with 73 additions and 65 deletions

19
app.py
View File

@@ -115,9 +115,9 @@ def get_person(person_id):
person['ExerciseProgressGraphs'] = list(filter(lambda e: e['ExerciseId'] in selected_exercise_ids, person['ExerciseProgressGraphs']))
if htmx:
return render_block(app.jinja_env, 'person.html', 'content', person=person, selected_exercise_ids=selected_exercise_ids, max_date=max_date, min_date=min_date, tags=tags), 200, {"HX-Trigger": "updatedPeople"}
return render_block(app.jinja_env, 'person.html', 'content', person=person, selected_exercise_ids=selected_exercise_ids, max_date=max_date, min_date=min_date, tags=tags), 200, {"HX-Trigger": "refreshStats"}
return render_template('person.html', person=person, selected_exercise_ids=selected_exercise_ids, max_date=max_date, min_date=min_date, tags=tags), 200, {"HX-Trigger": "updatedPeople"}
return render_template('person.html', person=person, selected_exercise_ids=selected_exercise_ids, max_date=max_date, min_date=min_date, tags=tags), 200, {"HX-Trigger": "refreshStats"}
@ app.route("/person/<int:person_id>/workout/overview", methods=['GET'])
def person_overview(person_id):
@@ -148,9 +148,9 @@ def person_overview(person_id):
}
if htmx:
return render_block(app.jinja_env, 'person_overview.html', 'content', **render_args)
return render_block(app.jinja_env, 'person_overview.html', 'content', **render_args), 200, {"HX-Trigger": "refreshStats"}
return render_template('person_overview.html', **render_args)
return render_template('person_overview.html', **render_args), 200, {"HX-Trigger": "refreshStats"}
@ app.route("/person/<int:person_id>/calendar")
@ validate_person
@@ -167,8 +167,8 @@ def get_calendar(person_id):
calendar_view = db.calendar.fetch_workouts_for_person(person_id, selected_date, selected_view)
if htmx:
return render_block(app.jinja_env, 'calendar.html', 'content', **calendar_view), 200, {"HX-Push-Url": url_for('get_calendar', person_id=person_id, view=selected_view, date=selected_date)}
return render_template('calendar.html', **calendar_view), 200, {"HX-Push-Url": url_for('get_calendar', person_id=person_id, view=selected_view, date=selected_date)}
return render_block(app.jinja_env, 'calendar.html', 'content', **calendar_view), 200, {"HX-Push-Url": url_for('get_calendar', person_id=person_id, view=selected_view, date=selected_date), "HX-Trigger": "refreshStats"}
return render_template('calendar.html', **calendar_view), 200, {"HX-Push-Url": url_for('get_calendar', person_id=person_id, view=selected_view, date=selected_date), "HX-Trigger": "refreshStats"}
@ app.route("/person/<int:person_id>/notes", methods=['GET'])
@ validate_person
@@ -450,8 +450,11 @@ def get_exercise_progress_for_user(person_id, exercise_id):
@app.route("/stats/person/<int:person_id>", methods=['GET'])
def get_stats_for_person(person_id):
stats = db.stats.fetch_stats_for_person(person_id)
return render_template('partials/stats.html', stats=stats)
min_date = request.args.get('min_date', type=convert_str_to_date)
max_date = request.args.get('max_date', type=convert_str_to_date)
selected_exercise_ids = request.args.getlist('exercise_id', type=int)
stats = db.stats.fetch_stats_for_person(person_id, min_date, max_date, selected_exercise_ids)
return render_template('partials/stats.html', stats=stats, refresh_url=request.full_path)
@ app.route("/person/<int:person_id>/workout/<int:workout_id>", methods=['GET'])
def show_workout(person_id, workout_id):

View File

@@ -138,10 +138,6 @@ class PersonOverview:
# Initialize the exercise sets dictionary
exercise_sets = {exercise["id"]: {"exercise_id": exercise["id"], "name": exercise["name"], "sets": []} for exercise in exercises}
workout_start_dates = []
set_count = 0
exercise_count = len(unique_exercise_ids)
for row in result:
workout_id = row["workout_id"]
@@ -154,8 +150,6 @@ class PersonOverview:
"exercises": {exercise["id"]: [] for exercise in exercises} # Keyed by exercise_id
}
workout_start_dates.append(row["start_date"])
# Add topset to the corresponding exercise
if row["exercise_id"] and row["topset_id"]:
# Add to workout exercises
@@ -172,7 +166,6 @@ class PersonOverview:
"workout_start_date": row["start_date"],
"exercise_name": row["exercise_name"]
})
set_count += 1
# Transform into a list of rows
for workout_id, workout in workout_map.items():
@@ -180,42 +173,11 @@ class PersonOverview:
exercise_progress_graphs = self.generate_exercise_progress_graphs(person_info["person_id"], exercise_sets)
workout_count = len(workout_start_dates)
stats = [{"Text": "Total Workouts", "Value": workout_count},
{"Text": "Total Sets", "Value": set_count},
{"Text": "Total Exercises", "Value": exercise_count}]
if workout_count > 0:
first_workout_date = min(workout_start_dates)
last_workout_date = max(workout_start_dates)
training_duration = last_workout_date - first_workout_date
stats.append({"Text": "Days Since First Workout", "Value": (
date.today() - first_workout_date).days})
if workout_count >= 2:
stats.append({"Text": "Days Since Last Workout",
"Value": (
date.today() - last_workout_date).days})
stats.append({"Text": "Total duration in days",
"Value": training_duration.days})
average_number_sets_per_workout = round(
set_count / workout_count, 1)
stats.append({"Text": "Average sets per workout",
"Value": average_number_sets_per_workout})
if training_duration > timedelta(days=0):
average_workouts_per_week = round(
workout_count / (training_duration.days / 7), 1)
stats.append({"Text": "Average Workouts Per Week",
"Value": average_workouts_per_week})
return {
**person_info,
"workouts": workouts,
"selected_exercises": exercises,
"exercise_progress_graphs": exercise_progress_graphs,
"stats": stats
"exercise_progress_graphs": exercise_progress_graphs
}
def generate_exercise_progress_graphs(self, person_id, exercise_sets):

View File

@@ -49,11 +49,13 @@ class Stats:
stats = [
{"Text": "Total Workouts", "Value": workout_count},
{"Text": "Total Sets", "Value": total_sets},
{"Text": "Total Exercises", "Value": exercise_count},
{"Text": "Average Sets Per Exercise", "Value": average_sets_per_exercise},
{"Text": "Average Exercises Per Workout", "Value": average_exercises_per_workout},
{"Text": "Average Sets Per Exercise", "Value": average_sets_per_exercise}
]
if exercise_count > 1:
stats.append({"Text": "Total Exercises", "Value": exercise_count})
stats.append({"Text": "Average Exercises Per Workout", "Value": average_exercises_per_workout})
if people_count > 1:
stats.append({"Text": "People Tracked", "Value": people_count})
@@ -82,7 +84,8 @@ class Stats:
return stats
def fetch_stats_for_person(self, person_id):
def fetch_stats_for_person(self, person_id, min_date=None, max_date=None, selected_exercise_ids=None):
# Base query
query = """
SELECT
t.workout_id AS "WorkoutId",
@@ -96,8 +99,26 @@ class Stats:
JOIN exercise e ON t.exercise_id = e.exercise_id
WHERE p.person_id = %s
"""
workouts_data = self.execute(query, [person_id])
# Parameters for the query
params = [person_id]
# Add optional filters
if min_date:
query += " AND w.start_date >= %s"
params.append(min_date)
if max_date:
query += " AND w.start_date <= %s"
params.append(max_date)
if selected_exercise_ids:
placeholders = ", ".join(["%s"] * len(selected_exercise_ids))
query += f" AND e.exercise_id IN ({placeholders})"
params.extend(selected_exercise_ids)
# Execute the query
workouts_data = self.execute(query, params)
# Generate stats from the retrieved data
person_stats = self.get_stats_from_topsets(workouts_data)
return person_stats

View File

@@ -163,8 +163,10 @@
{% block content %}
{% endblock %}
</div>
{% block stats %}
{% endblock %}
<div id="stats">
{% block stats %}
{% endblock %}
</div>
</main>
</div>

View File

@@ -157,15 +157,14 @@
</div>
{% endif %}
</div>
</div>
{% endblock %}
{% block stats %}
<div class="hidden" hx-get="{{ url_for('get_stats_for_person', person_id=person_id) }}" hx-trigger="load"
hx-target="this" hx-swap="outerHTML">
hx-target="#stats" hx-swap="innerHTML">
</div>
</div>
{% endblock %}
{% block add_workout_button %}

View File

@@ -81,10 +81,16 @@
</div>
</div>
<div class="hidden" hx-get="{{ url_for('get_stats_for_person', person_id=person_id) }}" hx-trigger="load"
hx-target="#stats" hx-swap="innerHTML">
</div>
{% endblock %}
{% block add_workout_button %}
<button
class="fixed z-90 bottom-10 right-8 bg-blue-600 w-20 h-20 rounded-full drop-shadow-lg flex justify-center items-center text-white text-4xl hover:bg-blue-700 hover:drop-shadow-2xl hover:animate-bounce duration-300"
hx-post="{{ url_for('create_workout', person_id=person_id) }}" hx-target='body' hx-swap='beforeend'>
hx-post="{{ url_for('create_workout', person_id=person_id) }}" hx-push-url="true" hx-target="#container">
<svg viewBox="0 0 20 20" enable-background="new 0 0 20 20" class="w-6 h-6 inline-block">
<path fill="#FFFFFF" d="M16,10c0,0.553-0.048,1-0.601,1H11v4.399C11,15.951,10.553,16,10,16c-0.553,0-1-0.049-1-0.601V11H4.601
C4.049,11,4,10.553,4,10c0-0.553,0.049-1,0.601-1H9V4.601C9,4.048,9.447,4,10,4c0.553,0,1,0.048,1,0.601V9h4.399

View File

@@ -1,4 +1,5 @@
<div class="mt-4 mb-4 w-full grid grid-cols-1 md:grid-cols-3 2xl:grid-cols-4 gap-4">
<div class="mt-4 mb-4 w-full grid grid-cols-1 md:grid-cols-3 2xl:grid-cols-4 gap-4" hx-get="{{ refresh_url }}"
hx-target="this" hx-swap="outerHTML" hx-trigger="refreshStats from:body">
{% for stat in stats %}
<div class="bg-white shadow rounded-lg p-4 sm:p-6 xl:p-8 ">
<div class="flex items-center">

View File

@@ -159,11 +159,13 @@
</div>
</div>
<div class="hidden" hx-get="{{ url_for('get_stats_for_person', person_id=person_id) }}"
hx-include="[name='exercise_id'],[name='min_date'],[name='max_date']" hx-trigger="load" hx-target="#stats"
hx-swap="innerHTML">
</div>
{% endblock %}
{% block stats %}
{{ render_partial('partials/stats.html', stats=stats) }}
{% endblock %}
{% block add_workout_button %}
<button

View File

@@ -142,4 +142,8 @@
</div>
<div class="hidden" hx-get="{{ url_for('get_stats_for_person', person_id=person_id) }}" hx-trigger="load"
hx-target="#stats" hx-swap="innerHTML">
</div>
{% endblock %}

View File

@@ -148,6 +148,10 @@ def get_stats_from_topsets(topsets):
len(topsets) / workout_count, 1)
stats.append({"Text": "Average sets per workout",
"Value": average_number_sets_per_workout})
# Average exercises per workout
average_exercises_per_workout = round(exercise_count / workout_count, 1)
stats.append({"Text": "Average Exercises Per Workout", "Value": average_exercises_per_workout})
training_duration = last_workout_date - first_workout_date
if training_duration > timedelta(days=0):
@@ -155,6 +159,10 @@ def get_stats_from_topsets(topsets):
workout_count / (training_duration.days / 7), 1)
stats.append({"Text": "Average Workouts Per Week",
"Value": average_workouts_per_week})
# Average sets per exercise
average_sets_per_exercise = round(len(topsets) / exercise_count, 1) if exercise_count > 0 else 0
stats.append({"Text": "Average Sets Per Exercise", "Value": average_sets_per_exercise})
return stats