Refactor calendar (month and year view), switching to vertical slice arch. Also in progress of refactoring of stats so they are retreived after inital page load for performance sake
This commit is contained in:
16
app.py
16
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/<int:person_id>/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/<int:person_id>/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/<int:person_id>", 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):
|
||||
|
||||
6
db.py
6
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:
|
||||
|
||||
141
features/calendar.py
Normal file
141
features/calendar.py
Normal file
@@ -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
|
||||
85
features/stats.py
Normal file
85
features/stats.py
Normal file
@@ -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
|
||||
@@ -108,7 +108,8 @@
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
{% block stats %}
|
||||
{% endblock %}
|
||||
</main>
|
||||
|
||||
</div>
|
||||
@@ -150,6 +151,9 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
{% block add_workout_button %}
|
||||
{% endblock %}
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -2,16 +2,17 @@
|
||||
|
||||
{% block content %}
|
||||
|
||||
<a hx-get="{{ url_for('get_calendar', person_id=person['PersonId']) }}" hx-target="#container"
|
||||
hx-vals='{"date": "{{ selected_date }}", "view": "{{ selected_view }}"}' hx-trigger="refreshView"
|
||||
id="refreshViewElement"></a>
|
||||
<a hx-get="{{ url_for('get_calendar', person_id=person_id) }}" hx-target="#container"
|
||||
hx-vals='{"date": "{{ date }}", "view": "{{ view }}"}' hx-trigger="refreshView" id="refreshViewElement"
|
||||
hx-swap="innerHTML swap:0.5s"></a>
|
||||
|
||||
<div class="flex flex-grow flex-col bg-white sm:rounded shadow overflow-hidden">
|
||||
<div class="flex items-center justify-between pt-2 pb-2">
|
||||
<div class="flex">
|
||||
<div class="flex ml-1 md:ml-6">
|
||||
<button hx-get="{{ url_for('get_calendar', person_id=person['PersonId']) }}" hx-target="#container"
|
||||
hx-vals='{"date": "{{ previous_date }}"}' hx-include="[name='view']" hx-push-url="true">
|
||||
<button hx-get="{{ url_for('get_calendar', person_id=person_id) }}" hx-target="#container"
|
||||
hx-vals='{"date": "{{ prev_date }}"}' hx-include="[name='view']" hx-push-url="true"
|
||||
hx-swap="innerHTML swap:0.5s">
|
||||
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor" data-darkreader-inline-stroke=""
|
||||
style="--darkreader-inline-stroke:currentColor;">
|
||||
@@ -19,8 +20,9 @@
|
||||
</path>
|
||||
</svg>
|
||||
</button>
|
||||
<button hx-get="{{ url_for('get_calendar', person_id=person['PersonId']) }}" hx-target="#container"
|
||||
hx-vals='{"date": "{{ next_date }}"}' hx-include="[name='view']" hx-push-url="true">
|
||||
<button hx-get="{{ url_for('get_calendar', person_id=person_id) }}" hx-target="#container"
|
||||
hx-vals='{"date": "{{ next_date }}"}' hx-include="[name='view']" hx-push-url="true"
|
||||
hx-swap="innerHTML swap:0.5s">
|
||||
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor" data-darkreader-inline-stroke=""
|
||||
style="--darkreader-inline-stroke:currentColor;">
|
||||
@@ -30,29 +32,29 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{% if selected_view == 'month' %}
|
||||
<h2 class="ml-2 text-xl font-bold leading-none">{{ selected_date | strftime('%B, %Y') }}</h2>
|
||||
{% if view == 'month' %}
|
||||
<h2 class="ml-2 text-xl font-bold leading-none">{{ date | strftime('%B, %Y') }}</h2>
|
||||
{% else %}
|
||||
<h2 class="ml-2 text-xl font-bold leading-none">{{ selected_date | strftime('%Y') }}</h2>
|
||||
<h2 class="ml-2 text-xl font-bold leading-none">{{ date | strftime('%Y') }}</h2>
|
||||
{% endif %}
|
||||
<span
|
||||
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['PersonName']}}</span>
|
||||
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 }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mr-4">
|
||||
<select name="view" hx-get="{{ url_for('get_calendar', person_id=person['PersonId']) }}"
|
||||
hx-target="#container" hx-vals='{"date": "{{ selected_date }}"}' hx-push-url="true"
|
||||
_="init js(me) tail.select(me, {}) end">
|
||||
<option value="month" {% if selected_view=='month' %}selected{% endif %}>Month</option>
|
||||
<option value="year" {% if selected_view=='year' %}selected{% endif %}>Year</option>
|
||||
<select name="view" hx-get="{{ url_for('get_calendar', person_id=person_id) }}" hx-target="#container"
|
||||
hx-vals='{"date": "{{ date }}"}' hx-push-url="true" _="init js(me) tail.select(me, {}) end">
|
||||
<option value="month" {% if view=='month' %}selected{% endif %}>Month</option>
|
||||
<option value="year" {% if view=='year' %}selected{% endif %}>Year</option>
|
||||
<option value="notes">Notes</option>
|
||||
<option value="all">All</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if selected_view == 'month' %}
|
||||
{% if view == 'month' %}
|
||||
<div class="flex flex-col px-2 py-2 -mb-px">
|
||||
<div class="grid grid-cols-7 pl-2 pr-2">
|
||||
|
||||
@@ -88,57 +90,44 @@
|
||||
|
||||
<div class="grid grid-cols-7 overflow-hidden flex-1 pl-2 pr-2 w-full">
|
||||
|
||||
{% 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) %}
|
||||
<div class="{% if date == datetime.today().date() %}rounded-md border-4 border-green-50{% endif %} border flex flex-col h-36 sm:h-40 md:h-30 lg:h-30 mx-auto mx-auto overflow-hidden w-full pt-2 pl-1 cursor-pointer {% if selected_date.month != date.month %}bg-gray-100{% endif %}"
|
||||
{% if workout %}
|
||||
hx-get="{{ url_for('get_workout_modal', person_id=person['PersonId'], workout_id=workout['WorkoutId']) }}"
|
||||
hx-target='body' hx-swap='beforeend' {% endif %}>
|
||||
{% for day in days %}
|
||||
<div
|
||||
class="{% if day.is_today %}rounded-md border-4 border-green-50{% endif %} border flex flex-col h-36 sm:h-40 md:h-30 lg:h-30 mx-auto mx-auto overflow-hidden w-full pt-2 pl-1 cursor-pointer {% if day.is_in_current_month %}bg-gray-100{% endif %}">
|
||||
<div class="top h-5 w-full">
|
||||
<span class="text-gray-500 font-semibold">{{ date.day }}</span>
|
||||
<span class="text-gray-500 font-semibold">{{ day.day }}</span>
|
||||
</div>
|
||||
<div class="bottom flex-grow py-1 w-full">
|
||||
{% if workout['TopSets']|length > 0 %}
|
||||
{% for topset in workout['TopSets'] %}
|
||||
{% for workout in day.workouts %}
|
||||
<div class="bottom flex-grow py-1 w-full"
|
||||
hx-get="{{ url_for('get_workout_modal', person_id=person_id, workout_id=workout.workout_id) }}"
|
||||
hx-target='body' hx-swap='beforeend'>
|
||||
{% for set in workout.sets %}
|
||||
<button
|
||||
class="flex flex-col xl:flex-row items-start lg:items-center flex-shrink-0 px-0 sm:px-0.5 md:px-0.5 lg:px-0.5 text-xs">
|
||||
<span class="ml-0 sm:ml-0.5 md:ml-2 lg:ml-2 font-medium leading-none truncate">{{
|
||||
topset['ExerciseName'] }}</span>
|
||||
<span class="ml-0 sm:ml-0.5 md:ml-2 lg:ml-2 font-light leading-none">{{ topset['Repetitions'] }}
|
||||
x
|
||||
{{
|
||||
topset['Weight']
|
||||
}}kg</span>
|
||||
set.exercise_name }}</span>
|
||||
<span class="ml-0 sm:ml-0.5 md:ml-2 lg:ml-2 font-light leading-none">{{ set.repetitions }} x {{
|
||||
set.weight }}kg</span>
|
||||
</button>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% elif selected_view == 'year'%}
|
||||
{% elif view == 'year'%}
|
||||
<div class="flex justify-center px-2 py-2 -mb-px">
|
||||
|
||||
<div
|
||||
class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-4 lg:gap-10 bg-white overflow-hidden">
|
||||
{% 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 %}
|
||||
<div>
|
||||
<div class="bg-grey-lighter font-semibold text-center cursor-pointer"
|
||||
hx-get="{{ url_for('get_calendar', person_id=person['PersonId']) }}" hx-target="#container"
|
||||
hx-vals='{"date": "{{ first_day_of_month }}", "view": "month"}' hx-push-url="true"
|
||||
_="on click go to the top of the body">{{
|
||||
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') }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="grid grid-cols-7 border-b font-semibold">
|
||||
@@ -152,18 +141,15 @@
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-7 overflow-hidden flex-1 pl-2 pr-2 w-full">
|
||||
{% 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%}
|
||||
<div class="{% if day_date == datetime.today().date() and is_in_month %}rounded-md border-4 border-green-50 border{% endif %} py-3 px-4 hover:bg-blue hover:text-white text-center cursor-pointer rounded-md {% if workout and is_in_month %}bg-green-100{% endif %}"
|
||||
{% if workout %}
|
||||
hx-get="{{ url_for('get_workout_modal', person_id=person['PersonId'], workout_id=workout['WorkoutId']) }}"
|
||||
{% for day in month.days %}
|
||||
<div class="{% if day.is_today and day.is_in_current_month %}rounded-md border-4 border-green-50 border{% endif %} py-3 px-4 hover:bg-blue hover:text-white text-center cursor-pointer rounded-md {% if day.has_workouts and day.is_in_current_month %}bg-green-100{% endif %}"
|
||||
{% if day.has_workouts %}
|
||||
hx-get="{{ url_for('get_workout_modal', person_id=person_id, workout_id=day.first_workout_id) }}"
|
||||
hx-target='body' hx-swap='beforeend' {% endif %}>
|
||||
{% if is_in_month %}
|
||||
{{ day_date.day }} {% endif %}</div>
|
||||
{% if day.is_in_current_month %}
|
||||
{{ day.day }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -175,11 +161,20 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{{ render_partial('partials/stats.html', stats=person['Stats']) }}
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block stats %}
|
||||
<div class="hidden" hx-get="{{ url_for('get_stats_for_person', person_id=person_id) }}" hx-trigger="load"
|
||||
hx-target="this" hx-swap="outerHTML">
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block add_workout_button %}
|
||||
<button
|
||||
class="fixed z-90 bottom-10 right-8 bg-blue-600 w-20 h-20 rounded-full drop-shadow-lg flex justify-center items-center text-white text-4xl hover:bg-blue-700 hover:drop-shadow-2xl hover:animate-bounce duration-300"
|
||||
hx-post="{{ url_for('create_workout', person_id=person['PersonId']) }}" hx-target='body' hx-swap='beforeend'>
|
||||
hx-post="{{ url_for('create_workout', person_id=person_id) }}" hx-target='body' hx-swap='beforeend'>
|
||||
<svg viewBox="0 0 20 20" enable-background="new 0 0 20 20" class="w-6 h-6 inline-block">
|
||||
<path fill="#FFFFFF" d="M16,10c0,0.553-0.048,1-0.601,1H11v4.399C11,15.951,10.553,16,10,16c-0.553,0-1-0.049-1-0.601V11H4.601
|
||||
C4.049,11,4,10.553,4,10c0-0.553,0.049-1,0.601-1H9V4.601C9,4.048,9.447,4,10,4c0.553,0,1,0.048,1,0.601V9h4.399
|
||||
|
||||
60
utils.py
60
utils.py
@@ -1,6 +1,5 @@
|
||||
import colorsys
|
||||
from datetime import datetime, date, timedelta
|
||||
import random
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
@@ -214,65 +213,6 @@ def flatten(lst):
|
||||
result.append(item)
|
||||
return result
|
||||
|
||||
|
||||
def get_date_info(input_date, selected_view):
|
||||
if selected_view not in ['month', 'year']:
|
||||
raise ValueError(
|
||||
'selected_view must be either "month" or "year"')
|
||||
|
||||
# First day of the month
|
||||
first_day_of_month = input_date.replace(day=1)
|
||||
|
||||
# Last day of the month
|
||||
if input_date.month == 12:
|
||||
last_day_of_month = input_date.replace(
|
||||
year=input_date.year+1, month=1, day=1) - timedelta(days=1)
|
||||
else:
|
||||
last_day_of_month = input_date.replace(
|
||||
month=input_date.month+1, day=1) - timedelta(days=1)
|
||||
|
||||
# First and last day of the year
|
||||
first_day_of_year = input_date.replace(month=1, day=1)
|
||||
last_day_of_year = input_date.replace(
|
||||
year=input_date.year+1, month=1, day=1) - timedelta(days=1)
|
||||
|
||||
# Next/previous month
|
||||
year, month = divmod(input_date.year * 12 + input_date.month, 12)
|
||||
next_month = date(year, month + 1, 1)
|
||||
prev_month_last_day = first_day_of_month - timedelta(days=1)
|
||||
prev_month = prev_month_last_day.replace(day=1)
|
||||
|
||||
# Next/previous year
|
||||
next_year = input_date.replace(year=input_date.year+1)
|
||||
prev_year = input_date.replace(year=input_date.year-1)
|
||||
|
||||
# Business logic, should move above to a separate function
|
||||
if selected_view == 'month':
|
||||
# Step 1: Find the first Sunday before or on the first day of the month
|
||||
days_to_subtract = (first_day_of_month.weekday() + 1) % 7
|
||||
start_date = first_day_of_month - timedelta(days=days_to_subtract)
|
||||
|
||||
# Step 2: Calculate the last day to display, based on the number of weeks
|
||||
end_date = start_date + timedelta(days=6 * 7 - 1)
|
||||
|
||||
return {
|
||||
'next_date': next_month,
|
||||
'previous_date': prev_month,
|
||||
'first_date_of_view': first_day_of_month,
|
||||
'last_date_of_view': last_day_of_month,
|
||||
'start_date': start_date,
|
||||
'end_date': end_date,
|
||||
}
|
||||
elif selected_view == 'year':
|
||||
return {
|
||||
'next_date': next_year,
|
||||
'previous_date': prev_year,
|
||||
'first_date_of_view': first_day_of_year,
|
||||
'last_date_of_view': last_day_of_year,
|
||||
'start_date': first_day_of_year,
|
||||
'end_date': last_day_of_year,
|
||||
}
|
||||
|
||||
def get_exercise_graph_model(title, estimated_1rm, repetitions, weight, start_dates, messages, epoch, person_id, exercise_id):
|
||||
min_date, max_date = min(start_dates), max(start_dates)
|
||||
min_e1rm, max_e1rm = min(estimated_1rm), max(estimated_1rm)
|
||||
|
||||
Reference in New Issue
Block a user