diff --git a/app.py b/app.py index 2a94a05..383e894 100644 --- a/app.py +++ b/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) diff --git a/features/person_overview.py b/features/person_overview.py index 8446adb..a6a0f1c 100644 --- a/features/person_overview.py +++ b/features/person_overview.py @@ -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): diff --git a/requirements.txt b/requirements.txt index 213e456..4cf6da4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 \ No newline at end of file +Flask-Compress==1.13 +Brotli==1.0.9 +Flask-Caching==2.0.2 \ No newline at end of file diff --git a/templates/partials/workout_rows.html b/templates/partials/workout_rows.html new file mode 100644 index 0000000..0a10baa --- /dev/null +++ b/templates/partials/workout_rows.html @@ -0,0 +1,29 @@ +{% for workout in workouts %} + + + {{ workout.start_date | strftime("%b %d %Y") }} + + + {% for exercise in selected_exercises %} + + {% for set in workout.exercises[exercise.id] %} + {{ set.repetitions }} x {{ set.weight }}kg + {% endfor %} + + {% endfor %} + + +{% if loop.last and has_more %} + + + + + +{% endif %} +{% endfor %} \ No newline at end of file diff --git a/templates/person_overview.html b/templates/person_overview.html index d1bc236..095e480 100644 --- a/templates/person_overview.html +++ b/templates/person_overview.html @@ -138,22 +138,7 @@ - {% for workout in workouts %} - - - {{ workout.start_date | strftime("%b %d %Y") }} - - - {% for exercise in selected_exercises %} - - {% for set in workout.exercises[exercise.id] %} - {{ set.repetitions }} x {{ set.weight }}kg - {% endfor %} - - {% endfor %} - - {% endfor %} + {% include 'partials/workout_rows.html' %}