Add brotli complression, cache graph requests for 5mins and add pagination for person overview
This commit is contained in:
33
app.py
33
app.py
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -18,4 +18,6 @@ email-validator==2.2.0
|
||||
requests==2.26.0
|
||||
polars>=0.20.0
|
||||
pyarrow>=14.0.0
|
||||
Flask-Compress==1.13
|
||||
Flask-Compress==1.13
|
||||
Brotli==1.0.9
|
||||
Flask-Caching==2.0.2
|
||||
29
templates/partials/workout_rows.html
Normal file
29
templates/partials/workout_rows.html
Normal 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 %}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user