diff --git a/app.py b/app.py index c619701..97ece8a 100644 --- a/app.py +++ b/app.py @@ -7,7 +7,7 @@ import jinja_partials from jinja2_fragments import render_block from decorators import validate_person, validate_topset, validate_workout from db import DataBase -from utils import count_prs_over_time, get_date_info, get_people_and_exercise_rep_maxes, convert_str_to_date, get_earliest_and_latest_workout_date, filter_workout_topsets, first_and_last_visible_days_in_month, get_weekly_pr_graph_model, get_workout_counts +from utils import count_prs_over_time, get_people_and_exercise_rep_maxes, convert_str_to_date, get_earliest_and_latest_workout_date, filter_workout_topsets, first_and_last_visible_days_in_month, get_weekly_pr_graph_model, get_workout_counts from flask_htmx import HTMX import minify_html from urllib.parse import quote @@ -123,8 +123,6 @@ def get_person(person_id): @ app.route("/person//calendar") @ validate_person def get_calendar(person_id): - person = db.get_person(person_id) - selected_date = convert_str_to_date(request.args.get( 'date'), '%Y-%m-%d') or date.today() selected_view = request.args.get('view') or 'month' @@ -134,12 +132,11 @@ def get_calendar(person_id): elif selected_view == 'notes': return redirect(url_for('get_person_notes', person_id=person_id)) - # selected_view = month | year | all - date_info = get_date_info(selected_date, selected_view) + calendar_view = db.calendar.fetch_workouts_for_person(person_id, selected_date, selected_view) if htmx: - return render_block(app.jinja_env, 'calendar.html', 'content', person=person, selected_date=selected_date, selected_view=selected_view, **date_info, datetime=datetime, timedelta=timedelta, relativedelta=relativedelta, first_and_last_visible_days_in_month=first_and_last_visible_days_in_month), 200, {"HX-Push-Url": url_for('get_calendar', person_id=person_id, view=selected_view, date=selected_date)} - return render_template('calendar.html', person=person, selected_date=selected_date, selected_view=selected_view, **date_info, datetime=datetime, timedelta=timedelta, relativedelta=relativedelta, first_and_last_visible_days_in_month=first_and_last_visible_days_in_month), 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)} + 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)} @ app.route("/person//notes", methods=['GET']) @ validate_person @@ -428,6 +425,11 @@ def get_exercise_progress_for_user(person_id, exercise_id): return render_template('partials/sparkline.html', **exercise_progress) +@app.route("/stats/person/", methods=['GET']) +def get_stats_for_person(person_id): + stats = db.stats.fetch_stats_for_person(person_id) + return render_template('partials/stats.html', stats=stats) + @app.teardown_appcontext def closeConnection(exception): diff --git a/db.py b/db.py index cde3ad3..a19f664 100644 --- a/db.py +++ b/db.py @@ -6,13 +6,15 @@ from datetime import datetime from dateutil.relativedelta import relativedelta from urllib.parse import urlparse from flask import g - - +from features.calendar import Calendar +from features.stats import Stats from utils import count_prs_over_time, get_all_exercises_from_topsets, get_exercise_graph_model, get_stats_from_topsets, get_topsets_for_person, get_weekly_pr_graph_model, get_workout_counts, get_workouts class DataBase(): def __init__(self, app): + self.calendar = Calendar(self.execute) + self.stats = Stats(self.execute) db_url = urlparse(os.environ['DATABASE_URL']) # if db_url is null then throw error if not db_url: diff --git a/features/calendar.py b/features/calendar.py new file mode 100644 index 0000000..9729caa --- /dev/null +++ b/features/calendar.py @@ -0,0 +1,141 @@ +from datetime import datetime, timedelta +from dateutil.relativedelta import relativedelta +import pandas as pd + +class Calendar: + def __init__(self, db_connection_method): + self.execute = db_connection_method + + def fetch_workouts_for_person(self, person_id, date, view): + prev_date, next_date = None, None + if view == 'month': + first_day_of_month = date.replace(day=1) + days_to_subtract = (first_day_of_month.weekday() + 1) % 7 + start_date = first_day_of_month - timedelta(days=days_to_subtract) + end_date = start_date + timedelta(days=6 * 7 - 1) + prev_date = first_day_of_month - relativedelta(months=1) + next_date = first_day_of_month + relativedelta(months=1) + elif view == 'year': + start_date = date.replace(month=1, day=1) + end_date = date.replace(year=date.year + 1, month=1, day=1) - timedelta(days=1) + prev_date = date - relativedelta(years=1) + next_date = date + relativedelta(years=1) + else: + raise ValueError('Invalid view') + + query = """ + SELECT + w.workout_id, + w.start_date, + t.topset_id, + t.repetitions, + t.weight, + e.name AS exercise_name, + p.name AS person_name + FROM + person p + LEFT JOIN workout w ON p.person_id = w.person_id AND w.start_date BETWEEN %s AND %s + LEFT JOIN topset t ON w.workout_id = t.workout_id + LEFT JOIN exercise e ON t.exercise_id = e.exercise_id + WHERE + p.person_id = %s + ORDER BY + w.start_date, + t.topset_id; + """ + workouts_data = self.execute(query, [start_date, end_date, person_id]) + + # Assuming person_name is the same for all rows as we filter by person_id + person_name = workouts_data[0]['person_name'] if workouts_data else 'Unknown' + + calendar_view = {'prev_date': prev_date, 'next_date': next_date, 'person_id': person_id, 'person_name': person_name, 'view': view, 'date': date} + + if view == 'month': + calendar_view['days'] = [] + workouts_by_date = {} + + for row in workouts_data: + if row['workout_id'] is None: + continue # Skip rows that don't have workout data + + workout_date_str = row['start_date'].strftime("%Y-%m-%d") + workout_id = row['workout_id'] + + if workout_date_str not in workouts_by_date: + workouts_by_date[workout_date_str] = {} + + if workout_id not in workouts_by_date[workout_date_str]: + workouts_by_date[workout_date_str][workout_id] = { + 'workout_id': workout_id, + 'start_date': row['start_date'], + 'sets': [] + } + + workouts_by_date[workout_date_str][workout_id]['sets'].append({ + 'repetitions': row['repetitions'], + 'weight': row['weight'], + 'exercise_name': row['exercise_name'] + }) + + for current_date in pd.date_range(start_date, end_date, freq='D'): + date_str = current_date.strftime("%Y-%m-%d") + day_workouts = workouts_by_date.get(date_str, {}) + today = datetime.today().date() + + calendar_view['days'].append({ + 'day': current_date.day, + 'is_today': current_date == today, + 'is_in_current_month': current_date.month == date.month, # Ensure it compares with the selected month + 'has_workouts': len(day_workouts) > 0, + 'workouts': list(day_workouts.values()) + }) + + + elif view == 'year': + calendar_view['months'] = [] + workouts_by_date = {} + for row in workouts_data: + if row['start_date'] is None: + continue # Skip rows that don't have workout data + workout_date_str = row['start_date'].strftime("%Y-%m-%d") + if workout_date_str not in workouts_by_date: + workouts_by_date[workout_date_str] = [] + + workouts_by_date[workout_date_str].append({ + 'workout_id': row['workout_id'], + 'start_date': row['start_date'], + 'topset_id': row['topset_id'], + 'repetitions': row['repetitions'], + 'weight': row['weight'], + 'exercise_name': row['exercise_name'] + }) + + + for month in range(1, 13): + first_day_of_month = date.replace(month=month, day=1) + days_to_subtract = (first_day_of_month.weekday() + 1) % 7 + start_date = first_day_of_month - timedelta(days=days_to_subtract) + end_date = start_date + timedelta(days=6 * 7 - 1) + + month_data = {'name': first_day_of_month.strftime('%B'), 'first_day_of_month': first_day_of_month, 'days': []} + + current_day = start_date + while current_day <= end_date: + day_workouts = workouts_by_date.get(current_day.strftime('%Y-%m-%d'), []) + has_workouts = len(day_workouts) > 0 + first_workout_id = day_workouts[0]['workout_id'] if has_workouts else None + + day_data = { + 'day': current_day.day, + 'is_today': current_day == datetime.today().date(), + 'is_in_current_month': current_day.month == month, + 'workouts': day_workouts, + 'has_workouts': has_workouts, + 'first_workout_id': first_workout_id + } + month_data['days'].append(day_data) + current_day += timedelta(days=1) + + calendar_view['months'].append(month_data) + + return calendar_view diff --git a/features/stats.py b/features/stats.py new file mode 100644 index 0000000..0eb85fd --- /dev/null +++ b/features/stats.py @@ -0,0 +1,85 @@ +from collections import Counter +from datetime import date + +class Stats: + def __init__(self, db_connection_method): + self.execute = db_connection_method + + def get_stats_from_topsets(self, topsets): + if not topsets: + return [] + + # Extracting necessary fields + workout_ids = [t['WorkoutId'] for t in topsets if t['WorkoutId']] + person_ids = [t['PersonId'] for t in topsets if t['PersonId']] + start_dates = [t['StartDate'] for t in topsets if t['StartDate']] + + workout_count = len(set(workout_ids)) + people_count = len(set(person_ids)) + total_sets = len(topsets) + stats = [ + {"Text": "Total Workouts", "Value": workout_count}, + {"Text": "Total Sets", "Value": total_sets} + ] + + if people_count > 1: + stats.append({"Text": "People tracked", "Value": people_count}) + + if workout_count > 0: + first_workout_date = min(start_dates) + last_workout_date = max(start_dates) + current_date = date.today() + + stats.append({"Text": "Days Since First Workout", + "Value": (current_date - first_workout_date).days}) + + if workout_count >= 2: + stats.append({"Text": "Days Since Last Workout", + "Value": (current_date - last_workout_date).days}) + + average_sets_per_workout = round(total_sets / workout_count, 1) + stats.append({"Text": "Average sets per workout", + "Value": average_sets_per_workout}) + + training_duration = last_workout_date - first_workout_date + if training_duration.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 stats + + + def fetch_stats_for_person(self, person_id): + query = """ + SELECT + t.workout_id AS "WorkoutId", + w.person_id AS "PersonId", + w.start_date AS "StartDate" + FROM + topset t + JOIN workout w ON t.workout_id = w.workout_id + JOIN person p ON w.person_id = p.person_id + WHERE p.person_id = %s + """ + workouts_data = self.execute(query, [person_id]) + + person_stats = self.get_stats_from_topsets(workouts_data) + return person_stats + + def fetch_all_stats(self): + query = """ + SELECT + t.workout_id AS "WorkoutId", + w.person_id AS "PersonId", + w.start_date AS "StartDate" + FROM + topset t + JOIN workout w ON t.workout_id = w.workout_id + JOIN person p ON w.person_id = p.person_id; + """ + workouts_data = self.execute(query, []) + + person_stats = self.get_stats_from_topsets(workouts_data) + return person_stats diff --git a/templates/base.html b/templates/base.html index 9fec697..bb22023 100644 --- a/templates/base.html +++ b/templates/base.html @@ -108,7 +108,8 @@ {% block content %} {% endblock %} - + {% block stats %} + {% endblock %} @@ -150,6 +151,9 @@ + {% block add_workout_button %} + {% endblock %} + \ No newline at end of file diff --git a/templates/calendar.html b/templates/calendar.html index 6730eec..921610b 100644 --- a/templates/calendar.html +++ b/templates/calendar.html @@ -2,16 +2,17 @@ {% block content %} - +
- -
- {% if selected_view == 'month' %} -

{{ selected_date | strftime('%B, %Y') }}

+ {% if view == 'month' %} +

{{ date | strftime('%B, %Y') }}

{% else %} -

{{ selected_date | strftime('%Y') }}

+

{{ date | strftime('%Y') }}

{% endif %} {{ - person['PersonName']}} + class="bg-blue-100 text-blue-800 text-sm font-medium mr-2 px-2.5 py-0.5 rounded dark:bg-blue-200 dark:text-blue-800 ml-1 sm:ml-5 flex justify-center items-center "> + {{ person_name }} +
- + +
- {% if selected_view == 'month' %} + {% if view == 'month' %}
@@ -88,57 +90,44 @@
- {% for i in range((end_date-start_date).days + 1) %} - {% set date = start_date + timedelta(days=i) %} - {% set workout = person['Workouts'] | - get_first_element_from_list_with_matching_attribute( - 'StartDate', - date) %} -
+ {% for day in days %} +
- {{ date.day }} + {{ day.day }}
-
- {% if workout['TopSets']|length > 0 %} - {% for topset in workout['TopSets'] %} + {% for workout in day.workouts %} +
+ {% for set in workout.sets %} {% endfor %} - {% endif %}
+ {% endfor %}
{% endfor %}
- {% elif selected_view == 'year'%} + {% elif view == 'year'%}
- {% for i in range(12) %} - {% set date = start_date + relativedelta(months=i) %} - {% set first_day_of_month = date.replace(day=1) %} - {% set last_day_of_month = date + relativedelta(day=31) %} - {% set (first_day, last_day) = first_and_last_visible_days_in_month(first_day_of_month, last_day_of_month) - %} + {% for month in months %}
{{ - first_day_of_month | strftime('%B %Y') }} + hx-get="{{ url_for('get_calendar', person_id=person_id) }}" hx-target="#container" + hx-vals='{"date": "{{ month.first_day_of_month }}", "view": "month"}' hx-push-url="true" + _="on click go to the top of the body"> + {{ month.first_day_of_month | strftime('%B') }}
@@ -152,18 +141,15 @@
- {% for i in range((last_day-first_day).days + 1) %} - {% set day_date = first_day + timedelta(days=i) %} - {% set workout = person['Workouts'] | get_first_element_from_list_with_matching_attribute( - 'StartDate', - day_date) %} - {% set is_in_month = day_date.month == first_day_of_month.month%} -
- {% if is_in_month %} - {{ day_date.day }} {% endif %}
+ {% if day.is_in_current_month %} + {{ day.day }} + {% endif %} +
{% endfor %}
@@ -175,11 +161,20 @@ {% endif %}
-{{ render_partial('partials/stats.html', stats=person['Stats']) }} + +{% endblock %} + +{% block stats %} + +{% endblock %} + +{% block add_workout_button %}