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
|
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 import Flask, abort, render_template, redirect, request, url_for
|
||||||
from flask_login import LoginManager, login_required, current_user
|
from flask_login import LoginManager, login_required, current_user
|
||||||
import jinja_partials
|
import jinja_partials
|
||||||
@@ -20,18 +26,17 @@ from extensions import db
|
|||||||
from utils import convert_str_to_date
|
from utils import convert_str_to_date
|
||||||
from flask_htmx import HTMX
|
from flask_htmx import HTMX
|
||||||
import minify_html
|
import minify_html
|
||||||
import os
|
|
||||||
from dotenv import load_dotenv
|
|
||||||
from flask_compress import Compress
|
from flask_compress import Compress
|
||||||
|
from flask_caching import Cache
|
||||||
# Load environment variables from .env file in non-production environments
|
|
||||||
if os.environ.get('FLASK_ENV') != 'production':
|
|
||||||
load_dotenv()
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config['COMPRESS_REGISTER'] = True
|
app.config['COMPRESS_REGISTER'] = True
|
||||||
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 31536000 # 1 year
|
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)
|
Compress(app)
|
||||||
|
cache = Cache(app)
|
||||||
app.config.from_pyfile('config.py')
|
app.config.from_pyfile('config.py')
|
||||||
app.secret_key = os.environ.get('SECRET_KEY', '2a661781919643cb8a5a8bc57642d99f')
|
app.secret_key = os.environ.get('SECRET_KEY', '2a661781919643cb8a5a8bc57642d99f')
|
||||||
jinja_partials.register_extensions(app)
|
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':
|
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)
|
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)
|
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)
|
tags = db.get_tags_for_person(person_id)
|
||||||
|
|
||||||
@@ -151,10 +159,15 @@ def person_overview(person_id):
|
|||||||
"tags": tags,
|
"tags": tags,
|
||||||
"selected_exercise_ids": selected_exercise_ids,
|
"selected_exercise_ids": selected_exercise_ids,
|
||||||
"max_date": max_date,
|
"max_date": max_date,
|
||||||
"min_date": min_date
|
"min_date": min_date,
|
||||||
|
"limit": limit,
|
||||||
|
"offset": offset,
|
||||||
|
"next_offset": offset + limit
|
||||||
}
|
}
|
||||||
|
|
||||||
if htmx:
|
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_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"}
|
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)
|
return render_template('partials/sparkline.html', **exercise_progress)
|
||||||
|
|
||||||
@app.route("/stats", methods=['GET'])
|
@app.route("/stats", methods=['GET'])
|
||||||
|
@cache.cached(timeout=300, query_string=True)
|
||||||
def get_stats():
|
def get_stats():
|
||||||
selected_people_ids = request.args.getlist('person_id', type=int)
|
selected_people_ids = request.args.getlist('person_id', type=int)
|
||||||
min_date = request.args.get('min_date', type=convert_str_to_date)
|
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)
|
return render_template('partials/stats.html', stats=stats, refresh_url=request.full_path)
|
||||||
|
|
||||||
@app.route("/graphs", methods=['GET'])
|
@app.route("/graphs", methods=['GET'])
|
||||||
|
@cache.cached(timeout=300, query_string=True)
|
||||||
def get_people_graphs():
|
def get_people_graphs():
|
||||||
selected_people_ids = request.args.getlist('person_id', type=int)
|
selected_people_ids = request.args.getlist('person_id', type=int)
|
||||||
min_date = request.args.get('min_date', type=convert_str_to_date)
|
min_date = request.args.get('min_date', type=convert_str_to_date)
|
||||||
|
|||||||
@@ -77,11 +77,33 @@ class PersonOverview:
|
|||||||
return exercises
|
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
|
# 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"""
|
sql_query = f"""
|
||||||
SELECT
|
SELECT
|
||||||
p.person_id,
|
p.person_id,
|
||||||
@@ -103,19 +125,18 @@ class PersonOverview:
|
|||||||
JOIN
|
JOIN
|
||||||
exercise e ON t.exercise_id = e.exercise_id
|
exercise e ON t.exercise_id = e.exercise_id
|
||||||
WHERE
|
WHERE
|
||||||
p.person_id = %s
|
w.workout_id IN ({workout_id_placeholders})
|
||||||
AND w.start_date BETWEEN %s AND %s
|
AND e.exercise_id IN ({exercise_placeholders})
|
||||||
AND e.exercise_id IN ({placeholders})
|
|
||||||
ORDER BY
|
ORDER BY
|
||||||
w.start_date DESC, e.exercise_id ASC, t.topset_id ASC;
|
w.start_date DESC, e.exercise_id ASC, t.topset_id ASC;
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Add parameters for the query
|
# Parameters for the detailed query
|
||||||
params = [person_id, start_date, end_date] + selected_exercise_ids
|
params = target_workout_ids + selected_exercise_ids
|
||||||
result = self.execute(sql_query, params)
|
result = self.execute(sql_query, params)
|
||||||
|
|
||||||
if not result:
|
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
|
# Extract person info from the first row
|
||||||
person_info = {"person_id": result[0]["person_id"], "person_name": result[0]["person_name"]}
|
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"])
|
exercises = sorted(exercises, key=lambda ex: ex["name"])
|
||||||
|
|
||||||
# Initialize the table structure
|
# Initialize the table structure
|
||||||
workouts = []
|
|
||||||
workout_map = {} # Map to track workouts
|
workout_map = {} # Map to track workouts
|
||||||
|
|
||||||
# Initialize the exercise sets dictionary
|
# Initialize the exercise sets dictionary
|
||||||
@@ -153,10 +173,11 @@ class PersonOverview:
|
|||||||
# 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
|
||||||
workout_map[workout_id]["exercises"][row["exercise_id"]].append({
|
if row["exercise_id"] in workout_map[workout_id]["exercises"]:
|
||||||
"repetitions": row["repetitions"],
|
workout_map[workout_id]["exercises"][row["exercise_id"]].append({
|
||||||
"weight": row["weight"]
|
"repetitions": row["repetitions"],
|
||||||
})
|
"weight": row["weight"]
|
||||||
|
})
|
||||||
|
|
||||||
# Add to the exercise sets dictionary with workout start date
|
# Add to the exercise sets dictionary with workout start date
|
||||||
exercise_sets[row["exercise_id"]]["sets"].append({
|
exercise_sets[row["exercise_id"]]["sets"].append({
|
||||||
@@ -167,9 +188,8 @@ class PersonOverview:
|
|||||||
"exercise_name": row["exercise_name"]
|
"exercise_name": row["exercise_name"]
|
||||||
})
|
})
|
||||||
|
|
||||||
# Transform into a list of rows
|
# Transform into a list of rows, maintaining DESC order
|
||||||
for workout_id, workout in workout_map.items():
|
workouts = [workout_map[wid] for wid in target_workout_ids if wid in workout_map]
|
||||||
workouts.append(workout)
|
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
@@ -177,7 +197,8 @@ class PersonOverview:
|
|||||||
**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,
|
||||||
|
"has_more": has_more
|
||||||
}
|
}
|
||||||
|
|
||||||
def generate_exercise_progress_graphs(self, person_id, exercise_sets):
|
def generate_exercise_progress_graphs(self, person_id, exercise_sets):
|
||||||
|
|||||||
@@ -18,4 +18,6 @@ email-validator==2.2.0
|
|||||||
requests==2.26.0
|
requests==2.26.0
|
||||||
polars>=0.20.0
|
polars>=0.20.0
|
||||||
pyarrow>=14.0.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>
|
</thead>
|
||||||
<tbody class="bg-white">
|
<tbody class="bg-white">
|
||||||
|
|
||||||
{% for workout in workouts %}
|
{% include 'partials/workout_rows.html' %}
|
||||||
<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 %}
|
|
||||||
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
Reference in New Issue
Block a user