Add brotli complression, cache graph requests for 5mins and add pagination for person overview

This commit is contained in:
Peter Stockings
2026-02-04 09:28:18 +11:00
parent b4121eada7
commit 71a5ae590e
5 changed files with 96 additions and 44 deletions

33
app.py
View File

@@ -1,5 +1,11 @@
from datetime import date
import os
from dotenv import load_dotenv
# Load environment variables from .env file in non-production environments
if os.environ.get('FLASK_ENV') != 'production':
load_dotenv()
from datetime import date
from flask import Flask, abort, render_template, redirect, request, url_for
from flask_login import LoginManager, login_required, current_user
import jinja_partials
@@ -20,18 +26,17 @@ from extensions import db
from utils import convert_str_to_date
from flask_htmx import HTMX
import minify_html
import os
from dotenv import load_dotenv
from flask_compress import Compress
# Load environment variables from .env file in non-production environments
if os.environ.get('FLASK_ENV') != 'production':
load_dotenv()
from flask_caching import Cache
app = Flask(__name__)
app.config['COMPRESS_REGISTER'] = True
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 31536000 # 1 year
app.config['CACHE_TYPE'] = 'SimpleCache'
app.config['CACHE_DEFAULT_TIMEOUT'] = 300 # 5 minutes
Compress(app)
cache = Cache(app)
app.config.from_pyfile('config.py')
app.secret_key = os.environ.get('SECRET_KEY', '2a661781919643cb8a5a8bc57642d99f')
jinja_partials.register_extensions(app)
@@ -140,7 +145,10 @@ def person_overview(person_id):
if not selected_exercise_ids and htmx.trigger_name != 'exercise_id':
selected_exercise_ids = db.person_overview.list_of_performed_exercise_ids(person_id, min_date, max_date)
person = db.person_overview.get(person_id, min_date, max_date, selected_exercise_ids)
limit = request.args.get('limit', type=int, default=20)
offset = request.args.get('offset', type=int, default=0)
person = db.person_overview.get(person_id, min_date, max_date, selected_exercise_ids, limit=limit, offset=offset)
exercises = db.person_overview.get_exercises_with_selection(person_id, min_date, max_date, selected_exercise_ids)
tags = db.get_tags_for_person(person_id)
@@ -151,10 +159,15 @@ def person_overview(person_id):
"tags": tags,
"selected_exercise_ids": selected_exercise_ids,
"max_date": max_date,
"min_date": min_date
"min_date": min_date,
"limit": limit,
"offset": offset,
"next_offset": offset + limit
}
if htmx:
if htmx.target == 'load-more-row':
return render_template('partials/workout_rows.html', **render_args)
return render_block(app.jinja_env, 'person_overview.html', 'content', **render_args), 200, {"HX-Push-Url": url_for('person_overview', person_id=person_id, min_date=min_date, max_date=max_date, exercise_id=selected_exercise_ids), "HX-Trigger": "refreshStats"}
return render_template('person_overview.html', **render_args), 200, {"HX-Push-Url": url_for('person_overview', person_id=person_id, min_date=min_date, max_date=max_date, exercise_id=selected_exercise_ids), "HX-Trigger": "refreshStats"}
@@ -330,6 +343,7 @@ def get_exercise_progress_for_user(person_id, exercise_id):
return render_template('partials/sparkline.html', **exercise_progress)
@app.route("/stats", methods=['GET'])
@cache.cached(timeout=300, query_string=True)
def get_stats():
selected_people_ids = request.args.getlist('person_id', type=int)
min_date = request.args.get('min_date', type=convert_str_to_date)
@@ -339,6 +353,7 @@ def get_stats():
return render_template('partials/stats.html', stats=stats, refresh_url=request.full_path)
@app.route("/graphs", methods=['GET'])
@cache.cached(timeout=300, query_string=True)
def get_people_graphs():
selected_people_ids = request.args.getlist('person_id', type=int)
min_date = request.args.get('min_date', type=convert_str_to_date)

View File

@@ -77,11 +77,33 @@ class PersonOverview:
return exercises
def get(self, person_id, start_date, end_date, selected_exercise_ids):
def get(self, person_id, start_date, end_date, selected_exercise_ids, limit=20, offset=0):
# Build placeholders for exercise IDs
placeholders = ", ".join(["%s"] * len(selected_exercise_ids))
exercise_placeholders = ", ".join(["%s"] * len(selected_exercise_ids))
# Dynamically inject placeholders into the query
# 1. Fetch workout IDs first for pagination
# We need to filter by person, date, and selected exercises
workout_ids_query = f"""
SELECT DISTINCT w.workout_id, w.start_date
FROM workout w
JOIN topset t ON w.workout_id = t.workout_id
WHERE w.person_id = %s
AND w.start_date BETWEEN %s AND %s
AND t.exercise_id IN ({exercise_placeholders})
ORDER BY w.start_date DESC
LIMIT %s OFFSET %s
"""
params = [person_id, start_date, end_date] + selected_exercise_ids + [limit + 1, offset]
workout_id_results = self.execute(workout_ids_query, params)
if not workout_id_results:
return {"person_id": person_id, "person_name": None, "workouts": [], "selected_exercises": [], "exercise_progress_graphs": [], "has_more": False}
has_more = len(workout_id_results) > limit
target_workout_ids = [r["workout_id"] for r in workout_id_results[:limit]]
workout_id_placeholders = ", ".join(["%s"] * len(target_workout_ids))
# 2. Fetch all details for these specific workouts
sql_query = f"""
SELECT
p.person_id,
@@ -103,19 +125,18 @@ class PersonOverview:
JOIN
exercise e ON t.exercise_id = e.exercise_id
WHERE
p.person_id = %s
AND w.start_date BETWEEN %s AND %s
AND e.exercise_id IN ({placeholders})
w.workout_id IN ({workout_id_placeholders})
AND e.exercise_id IN ({exercise_placeholders})
ORDER BY
w.start_date DESC, e.exercise_id ASC, t.topset_id ASC;
"""
# Add parameters for the query
params = [person_id, start_date, end_date] + selected_exercise_ids
# Parameters for the detailed query
params = target_workout_ids + selected_exercise_ids
result = self.execute(sql_query, params)
if not result:
return {"person_id": person_id, "person_name": None, "workouts": [], "selected_exercises": [], "exercise_progress_graphs": []}
return {"person_id": person_id, "person_name": None, "workouts": [], "selected_exercises": [], "exercise_progress_graphs": [], "has_more": False}
# Extract person info from the first row
person_info = {"person_id": result[0]["person_id"], "person_name": result[0]["person_name"]}
@@ -132,7 +153,6 @@ class PersonOverview:
exercises = sorted(exercises, key=lambda ex: ex["name"])
# Initialize the table structure
workouts = []
workout_map = {} # Map to track workouts
# Initialize the exercise sets dictionary
@@ -153,10 +173,11 @@ class PersonOverview:
# Add topset to the corresponding exercise
if row["exercise_id"] and row["topset_id"]:
# Add to workout exercises
workout_map[workout_id]["exercises"][row["exercise_id"]].append({
"repetitions": row["repetitions"],
"weight": row["weight"]
})
if row["exercise_id"] in workout_map[workout_id]["exercises"]:
workout_map[workout_id]["exercises"][row["exercise_id"]].append({
"repetitions": row["repetitions"],
"weight": row["weight"]
})
# Add to the exercise sets dictionary with workout start date
exercise_sets[row["exercise_id"]]["sets"].append({
@@ -167,9 +188,8 @@ class PersonOverview:
"exercise_name": row["exercise_name"]
})
# Transform into a list of rows
for workout_id, workout in workout_map.items():
workouts.append(workout)
# Transform into a list of rows, maintaining DESC order
workouts = [workout_map[wid] for wid in target_workout_ids if wid in workout_map]
exercise_progress_graphs = self.generate_exercise_progress_graphs(person_info["person_id"], exercise_sets)
@@ -177,7 +197,8 @@ class PersonOverview:
**person_info,
"workouts": workouts,
"selected_exercises": exercises,
"exercise_progress_graphs": exercise_progress_graphs
"exercise_progress_graphs": exercise_progress_graphs,
"has_more": has_more
}
def generate_exercise_progress_graphs(self, person_id, exercise_sets):

View File

@@ -19,3 +19,5 @@ requests==2.26.0
polars>=0.20.0
pyarrow>=14.0.0
Flask-Compress==1.13
Brotli==1.0.9
Flask-Caching==2.0.2

View File

@@ -0,0 +1,29 @@
{% for workout in workouts %}
<tr hx-get="{{ url_for('workout.show_workout', person_id=person_id, workout_id=workout.id) }}" hx-push-url="true"
hx-target="#container" class="cursor-pointer">
<td class="p-4 whitespace-nowrap text-sm font-normal text-gray-500">
{{ workout.start_date | strftime("%b %d %Y") }}
</td>
{% for exercise in selected_exercises %}
<td class="p-4 whitespace-nowrap text-sm font-semibold text-gray-900">
{% for set in workout.exercises[exercise.id] %}
{{ set.repetitions }} x {{ set.weight }}kg
{% endfor %}
</td>
{% endfor %}
</tr>
{% if loop.last and has_more %}
<tr id="load-more-row">
<td colspan="{{ selected_exercises|length + 1 }}" class="p-4 text-center">
<button class="text-blue-600 font-medium hover:underline px-4 py-2"
hx-get="{{ url_for('person_overview', person_id=person_id, offset=next_offset, limit=limit) }}"
hx-include="[name='exercise_id'],[name='min_date'],[name='max_date']" hx-target="#load-more-row"
hx-swap="outerHTML">
Load More Workouts
</button>
</td>
</tr>
{% endif %}
{% endfor %}

View File

@@ -138,22 +138,7 @@
</thead>
<tbody class="bg-white">
{% for workout in workouts %}
<tr hx-get="{{ url_for('workout.show_workout', person_id=person_id, workout_id=workout.id) }}"
hx-push-url="true" hx-target="#container" class="cursor-pointer">
<td class="p-4 whitespace-nowrap text-sm font-normal text-gray-500">
{{ workout.start_date | strftime("%b %d %Y") }}
</td>
{% for exercise in selected_exercises %}
<td class="p-4 whitespace-nowrap text-sm font-semibold text-gray-900">
{% for set in workout.exercises[exercise.id] %}
{{ set.repetitions }} x {{ set.weight }}kg
{% endfor %}
</td>
{% endfor %}
</tr>
{% endfor %}
{% include 'partials/workout_rows.html' %}
</tbody>
</table>