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'])) person['ExerciseProgressGraphs'] = list(filter(lambda e: e['ExerciseId'] in selected_exercise_ids, person['ExerciseProgressGraphs']))
if htmx: 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']) @ app.route("/person/<int:person_id>/workout/overview", methods=['GET'])
def person_overview(person_id): def person_overview(person_id):
@@ -148,9 +148,9 @@ def person_overview(person_id):
} }
if htmx: 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") @ app.route("/person/<int:person_id>/calendar")
@ validate_person @ 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) calendar_view = db.calendar.fetch_workouts_for_person(person_id, selected_date, selected_view)
if htmx: 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_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)} 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']) @ app.route("/person/<int:person_id>/notes", methods=['GET'])
@ validate_person @ 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']) @app.route("/stats/person/<int:person_id>", methods=['GET'])
def get_stats_for_person(person_id): def get_stats_for_person(person_id):
stats = db.stats.fetch_stats_for_person(person_id) min_date = request.args.get('min_date', type=convert_str_to_date)
return render_template('partials/stats.html', stats=stats) 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']) @ app.route("/person/<int:person_id>/workout/<int:workout_id>", methods=['GET'])
def show_workout(person_id, workout_id): def show_workout(person_id, workout_id):

View File

@@ -138,10 +138,6 @@ class PersonOverview:
# Initialize the exercise sets dictionary # Initialize the exercise sets dictionary
exercise_sets = {exercise["id"]: {"exercise_id": exercise["id"], "name": exercise["name"], "sets": []} for exercise in exercises} 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: for row in result:
workout_id = row["workout_id"] workout_id = row["workout_id"]
@@ -154,8 +150,6 @@ class PersonOverview:
"exercises": {exercise["id"]: [] for exercise in exercises} # Keyed by exercise_id "exercises": {exercise["id"]: [] for exercise in exercises} # Keyed by exercise_id
} }
workout_start_dates.append(row["start_date"])
# Add topset to the corresponding exercise # Add topset to the corresponding exercise
if row["exercise_id"] and row["topset_id"]: if row["exercise_id"] and row["topset_id"]:
# Add to workout exercises # Add to workout exercises
@@ -172,7 +166,6 @@ class PersonOverview:
"workout_start_date": row["start_date"], "workout_start_date": row["start_date"],
"exercise_name": row["exercise_name"] "exercise_name": row["exercise_name"]
}) })
set_count += 1
# Transform into a list of rows # Transform into a list of rows
for workout_id, workout in workout_map.items(): 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) 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 { return {
**person_info, **person_info,
"workouts": workouts, "workouts": workouts,
"selected_exercises": exercises, "selected_exercises": exercises,
"exercise_progress_graphs": exercise_progress_graphs, "exercise_progress_graphs": exercise_progress_graphs
"stats": stats
} }
def generate_exercise_progress_graphs(self, person_id, exercise_sets): def generate_exercise_progress_graphs(self, person_id, exercise_sets):

View File

@@ -49,11 +49,13 @@ class Stats:
stats = [ stats = [
{"Text": "Total Workouts", "Value": workout_count}, {"Text": "Total Workouts", "Value": workout_count},
{"Text": "Total Sets", "Value": total_sets}, {"Text": "Total Sets", "Value": total_sets},
{"Text": "Total Exercises", "Value": exercise_count}, {"Text": "Average Sets Per Exercise", "Value": average_sets_per_exercise}
{"Text": "Average Sets Per Exercise", "Value": average_sets_per_exercise},
{"Text": "Average Exercises Per Workout", "Value": average_exercises_per_workout},
] ]
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: if people_count > 1:
stats.append({"Text": "People Tracked", "Value": people_count}) stats.append({"Text": "People Tracked", "Value": people_count})
@@ -82,7 +84,8 @@ class Stats:
return 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 = """ query = """
SELECT SELECT
t.workout_id AS "WorkoutId", t.workout_id AS "WorkoutId",
@@ -96,8 +99,26 @@ class Stats:
JOIN exercise e ON t.exercise_id = e.exercise_id JOIN exercise e ON t.exercise_id = e.exercise_id
WHERE p.person_id = %s 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) person_stats = self.get_stats_from_topsets(workouts_data)
return person_stats return person_stats

View File

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

View File

@@ -157,15 +157,14 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
</div>
{% endblock %}
{% block stats %}
<div class="hidden" hx-get="{{ url_for('get_stats_for_person', person_id=person_id) }}" hx-trigger="load" <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>
</div>
{% endblock %} {% endblock %}
{% block add_workout_button %} {% block add_workout_button %}

View File

@@ -81,10 +81,16 @@
</div> </div>
</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 <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" 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"> <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 <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 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 %} {% for stat in stats %}
<div class="bg-white shadow rounded-lg p-4 sm:p-6 xl:p-8 "> <div class="bg-white shadow rounded-lg p-4 sm:p-6 xl:p-8 ">
<div class="flex items-center"> <div class="flex items-center">

View File

@@ -159,11 +159,13 @@
</div> </div>
</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 %} {% endblock %}
{% block stats %}
{{ render_partial('partials/stats.html', stats=stats) }}
{% endblock %}
{% block add_workout_button %} {% block add_workout_button %}
<button <button

View File

@@ -142,4 +142,8 @@
</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 %} {% endblock %}

View File

@@ -149,6 +149,10 @@ def get_stats_from_topsets(topsets):
stats.append({"Text": "Average sets per workout", stats.append({"Text": "Average sets per workout",
"Value": average_number_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 training_duration = last_workout_date - first_workout_date
if training_duration > timedelta(days=0): if training_duration > timedelta(days=0):
average_workouts_per_week = round( average_workouts_per_week = round(
@@ -156,6 +160,10 @@ def get_stats_from_topsets(topsets):
stats.append({"Text": "Average Workouts Per Week", stats.append({"Text": "Average Workouts Per Week",
"Value": 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 return stats