Refactor dashboard
This commit is contained in:
60
app.py
60
app.py
@@ -6,7 +6,7 @@ import jinja_partials
|
|||||||
from jinja2_fragments import render_block
|
from jinja2_fragments import render_block
|
||||||
from decorators import validate_person, validate_topset, validate_workout
|
from decorators import validate_person, validate_topset, validate_workout
|
||||||
from db import DataBase
|
from db import DataBase
|
||||||
from utils import get_people_and_exercise_rep_maxes, convert_str_to_date, first_and_last_visible_days_in_month, generate_plot
|
from utils import convert_str_to_date, generate_plot
|
||||||
from flask_htmx import HTMX
|
from flask_htmx import HTMX
|
||||||
import minify_html
|
import minify_html
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
@@ -38,36 +38,44 @@ def response_minify(response):
|
|||||||
return response
|
return response
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@ app.route("/")
|
@ app.route("/")
|
||||||
def dashboard():
|
def dashboard():
|
||||||
all_topsets = db.get_all_topsets()
|
selected_people_ids = request.args.getlist('person_id', type=int)
|
||||||
|
min_date = request.args.get('min_date', type=convert_str_to_date)
|
||||||
|
max_date = request.args.get('max_date', type=convert_str_to_date)
|
||||||
|
selected_exercise_ids = request.args.getlist('exercise_id', type=int)
|
||||||
|
|
||||||
exercises = db.get_all_exercises()
|
if not selected_people_ids and htmx.trigger_name != 'person_id':
|
||||||
people = db.get_people()
|
selected_people_ids = db.dashboard.get_people_ids()
|
||||||
tags = db.get_tags_for_dashboard()
|
|
||||||
|
|
||||||
selected_person_ids = [int(i)
|
if not min_date or not max_date:
|
||||||
for i in request.args.getlist('person_id')]
|
db_min_date, db_max_date = db.dashboard.get_earliest_and_latest_workout_dates(selected_people_ids)
|
||||||
if not selected_person_ids and htmx.trigger_name != 'person_id':
|
min_date = min_date or db_min_date
|
||||||
selected_person_ids = [p['PersonId'] for p in people]
|
max_date = max_date or db_max_date
|
||||||
|
|
||||||
selected_exercise_ids = [int(i)
|
|
||||||
for i in request.args.getlist('exercise_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 = [e['exercise_id'] for e in exercises]
|
selected_exercise_ids = db.dashboard.list_of_performed_exercise_ids(selected_people_ids, min_date, max_date)
|
||||||
|
|
||||||
min_date = convert_str_to_date(request.args.get(
|
people = db.dashboard.get_people_with_selection(selected_people_ids)
|
||||||
'min_date'), '%Y-%m-%d') or min([t['StartDate'] for t in all_topsets])
|
exercises = db.dashboard.get_exercises_with_selection(selected_people_ids, min_date, max_date, selected_exercise_ids)
|
||||||
max_date = convert_str_to_date(request.args.get(
|
tags = db.get_tags_for_dashboard()
|
||||||
'max_date'), '%Y-%m-%d') or max([t['StartDate'] for t in all_topsets])
|
dashboard = db.dashboard.get(selected_people_ids, min_date, max_date, selected_exercise_ids)
|
||||||
|
|
||||||
people_and_exercise_rep_maxes = get_people_and_exercise_rep_maxes(
|
# Render the appropriate response for HTMX or full page
|
||||||
all_topsets, selected_person_ids, selected_exercise_ids, min_date, max_date)
|
render_args = {
|
||||||
|
**dashboard,
|
||||||
|
"people": people,
|
||||||
|
"exercises": exercises,
|
||||||
|
"tags": tags,
|
||||||
|
"selected_people_ids": selected_people_ids,
|
||||||
|
"max_date": max_date,
|
||||||
|
"min_date": min_date,
|
||||||
|
"selected_exercise_ids": selected_exercise_ids
|
||||||
|
}
|
||||||
|
|
||||||
if htmx:
|
if htmx:
|
||||||
return render_block(app.jinja_env, 'dashboard.html', 'content', model=people_and_exercise_rep_maxes, people=people, exercises=exercises, min_date=min_date, max_date=max_date, selected_person_ids=selected_person_ids, selected_exercise_ids=selected_exercise_ids, tags=tags)
|
return render_block(app.jinja_env, 'dashboard.html', 'content', **render_args)
|
||||||
return render_template('dashboard.html', model=people_and_exercise_rep_maxes, people=people, exercises=exercises, min_date=min_date, max_date=max_date, selected_person_ids=selected_person_ids, selected_exercise_ids=selected_exercise_ids, tags=tags)
|
return render_template('dashboard.html', **render_args)
|
||||||
|
|
||||||
|
|
||||||
@ app.route("/person/list", methods=['GET'])
|
@ app.route("/person/list", methods=['GET'])
|
||||||
@@ -238,8 +246,8 @@ def delete_person(person_id):
|
|||||||
|
|
||||||
@ app.route("/person/<int:person_id>/edit_form", methods=['GET'])
|
@ app.route("/person/<int:person_id>/edit_form", methods=['GET'])
|
||||||
def get_person_edit_form(person_id):
|
def get_person_edit_form(person_id):
|
||||||
person = db.get_person(person_id)
|
name = db.get_person_name(person_id)
|
||||||
return render_template('partials/person.html', person_id=person_id, name=person['PersonName'], is_edit=True)
|
return render_template('partials/person.html', person_id=person_id, name=name, is_edit=True)
|
||||||
|
|
||||||
|
|
||||||
@ app.route("/person/<int:person_id>/name", methods=['PUT'])
|
@ app.route("/person/<int:person_id>/name", methods=['PUT'])
|
||||||
@@ -251,8 +259,8 @@ def update_person_name(person_id):
|
|||||||
|
|
||||||
@ app.route("/person/<int:person_id>/name", methods=['GET'])
|
@ app.route("/person/<int:person_id>/name", methods=['GET'])
|
||||||
def get_person_name(person_id):
|
def get_person_name(person_id):
|
||||||
person = db.get_person(person_id)
|
name = db.get_person_name(person_id)
|
||||||
return render_template('partials/person.html', person_id=person_id, name=person['PersonName'])
|
return render_template('partials/person.html', person_id=person_id, name=name)
|
||||||
|
|
||||||
|
|
||||||
@ app.route("/exercise", methods=['POST'])
|
@ app.route("/exercise", methods=['POST'])
|
||||||
@@ -634,7 +642,7 @@ def my_utility_processor():
|
|||||||
def list_to_string(list):
|
def list_to_string(list):
|
||||||
return [str(i) for i in list]
|
return [str(i) for i in list]
|
||||||
|
|
||||||
return dict(is_selected_page=is_selected_page, get_first_element_from_list_with_matching_attribute=get_first_element_from_list_with_matching_attribute, in_list=in_list, strftime=strftime, datetime=datetime, timedelta=timedelta, relativedelta=relativedelta, first_and_last_visible_days_in_month=first_and_last_visible_days_in_month, list_to_string=list_to_string, quote=quote)
|
return dict(is_selected_page=is_selected_page, get_first_element_from_list_with_matching_attribute=get_first_element_from_list_with_matching_attribute, in_list=in_list, strftime=strftime, datetime=datetime, timedelta=timedelta, relativedelta=relativedelta, list_to_string=list_to_string, quote=quote)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
33
db.py
33
db.py
@@ -13,7 +13,8 @@ from features.person_overview import PersonOverview
|
|||||||
from features.stats import Stats
|
from features.stats import Stats
|
||||||
from features.workout import Workout
|
from features.workout import Workout
|
||||||
from features.sql_explorer import SQLExplorer
|
from features.sql_explorer import SQLExplorer
|
||||||
from utils import get_all_exercises_from_topsets, get_exercise_graph_model, get_topsets_for_person, get_workouts
|
from features.dashboard import Dashboard
|
||||||
|
from utils import get_exercise_graph_model
|
||||||
|
|
||||||
|
|
||||||
class DataBase():
|
class DataBase():
|
||||||
@@ -25,6 +26,8 @@ class DataBase():
|
|||||||
self.sql_explorer = SQLExplorer(self.execute)
|
self.sql_explorer = SQLExplorer(self.execute)
|
||||||
self.person_overview = PersonOverview(self.execute)
|
self.person_overview = PersonOverview(self.execute)
|
||||||
self.people_graphs = PeopleGraphs(self.execute)
|
self.people_graphs = PeopleGraphs(self.execute)
|
||||||
|
self.dashboard = Dashboard(self.execute)
|
||||||
|
|
||||||
db_url = urlparse(os.environ['DATABASE_URL'])
|
db_url = urlparse(os.environ['DATABASE_URL'])
|
||||||
# if db_url is null then throw error
|
# if db_url is null then throw error
|
||||||
if not db_url:
|
if not db_url:
|
||||||
@@ -189,32 +192,10 @@ class DataBase():
|
|||||||
self.execute('UPDATE workout SET start_date=%s WHERE workout_id=%s', [
|
self.execute('UPDATE workout SET start_date=%s WHERE workout_id=%s', [
|
||||||
start_date, workout_id], commit=True)
|
start_date, workout_id], commit=True)
|
||||||
|
|
||||||
def get_person(self, person_id):
|
def get_person_name(self, person_id):
|
||||||
topsets = self.execute("""
|
result = self.execute("""SELECT name from Person WHERE person_id=%s""", [person_id], one=True)
|
||||||
SELECT
|
return result["name"]
|
||||||
P.person_id AS "PersonId",
|
|
||||||
P.name AS "PersonName",
|
|
||||||
W.workout_id AS "WorkoutId",
|
|
||||||
W.start_date AS "StartDate",
|
|
||||||
T.topset_id AS "TopSetId",
|
|
||||||
E.exercise_id AS "ExerciseId",
|
|
||||||
E.name AS "ExerciseName",
|
|
||||||
T.repetitions AS "Repetitions",
|
|
||||||
T.weight AS "Weight",
|
|
||||||
round((100 * T.Weight::numeric::integer)/(101.3-2.67123 * T.Repetitions),0)::numeric::integer AS "Estimated1RM"
|
|
||||||
FROM Person P
|
|
||||||
LEFT JOIN Workout W ON P.person_id=W.person_id
|
|
||||||
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""", [person_id])
|
|
||||||
|
|
||||||
return {
|
|
||||||
'PersonId': next((t['PersonId'] for t in topsets), -1),
|
|
||||||
'PersonName': next((t['PersonName'] for t in topsets), 'Unknown'),
|
|
||||||
'Exercises': get_all_exercises_from_topsets(topsets),
|
|
||||||
'Workouts': get_workouts(topsets),
|
|
||||||
'ExerciseProgressGraphs': get_topsets_for_person(topsets)
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_workout(self, person_id, workout_id):
|
def get_workout(self, person_id, workout_id):
|
||||||
topsets = self.execute("""
|
topsets = self.execute("""
|
||||||
|
|||||||
270
features/dashboard.py
Normal file
270
features/dashboard.py
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
from utils import calculate_estimated_1rm, get_exercise_graph_model
|
||||||
|
|
||||||
|
|
||||||
|
class Dashboard:
|
||||||
|
def __init__(self, db_connection_method):
|
||||||
|
self.execute = db_connection_method
|
||||||
|
|
||||||
|
def get_people_ids(self):
|
||||||
|
query = """
|
||||||
|
SELECT person_id
|
||||||
|
FROM Person
|
||||||
|
ORDER BY person_id
|
||||||
|
"""
|
||||||
|
result = self.execute(query)
|
||||||
|
# Extract and return the list of IDs
|
||||||
|
return [row["person_id"] for row in result]
|
||||||
|
|
||||||
|
def get_earliest_and_latest_workout_dates(self, selected_people_ids):
|
||||||
|
# Create placeholders for the person IDs
|
||||||
|
placeholders = ", ".join(["%s"] * len(selected_people_ids))
|
||||||
|
|
||||||
|
sql_query = f"""
|
||||||
|
SELECT
|
||||||
|
MIN(w.start_date) AS earliest_date,
|
||||||
|
MAX(w.start_date) AS latest_date
|
||||||
|
FROM workout w
|
||||||
|
INNER JOIN topset t ON w.workout_id = t.workout_id
|
||||||
|
WHERE w.person_id IN ({placeholders});
|
||||||
|
"""
|
||||||
|
result = self.execute(sql_query, selected_people_ids)
|
||||||
|
|
||||||
|
if not result or not result[0]:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
return result[0]['earliest_date'], result[0]['latest_date']
|
||||||
|
|
||||||
|
def list_of_performed_exercise_ids(self, selected_people_ids, min_date, max_date):
|
||||||
|
# Create placeholders for the person IDs
|
||||||
|
placeholders = ", ".join(["%s"] * len(selected_people_ids))
|
||||||
|
|
||||||
|
sql_query = f"""
|
||||||
|
SELECT
|
||||||
|
ARRAY_AGG(DISTINCT e.exercise_id) AS exercise_ids
|
||||||
|
FROM workout w
|
||||||
|
LEFT JOIN topset t ON w.workout_id = t.workout_id
|
||||||
|
LEFT JOIN exercise e ON t.exercise_id = e.exercise_id
|
||||||
|
WHERE w.start_date BETWEEN %s AND %s
|
||||||
|
AND w.person_id IN ({placeholders})
|
||||||
|
"""
|
||||||
|
# Add min_date, max_date, and selected_people_ids to the parameters
|
||||||
|
params = [min_date, max_date] + selected_people_ids
|
||||||
|
|
||||||
|
result = self.execute(sql_query, params)
|
||||||
|
|
||||||
|
if not result or not result[0]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
return result[0]['exercise_ids']
|
||||||
|
|
||||||
|
def get_exercises_with_selection(self, selected_people_ids, start_date, end_date, selected_exercise_ids):
|
||||||
|
# Create placeholders for the person IDs
|
||||||
|
placeholders = ", ".join(["%s"] * len(selected_people_ids))
|
||||||
|
|
||||||
|
# SQL query to fetch all exercises performed by the selected people in the given time range
|
||||||
|
sql_query = f"""
|
||||||
|
SELECT DISTINCT
|
||||||
|
e.exercise_id,
|
||||||
|
e.name AS exercise_name
|
||||||
|
FROM
|
||||||
|
workout w
|
||||||
|
JOIN
|
||||||
|
topset t ON w.workout_id = t.workout_id
|
||||||
|
JOIN
|
||||||
|
exercise e ON t.exercise_id = e.exercise_id
|
||||||
|
WHERE
|
||||||
|
w.person_id IN ({placeholders})
|
||||||
|
AND w.start_date BETWEEN %s AND %s
|
||||||
|
ORDER BY
|
||||||
|
e.name ASC;
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Add parameters for the query
|
||||||
|
params = selected_people_ids + [start_date, end_date]
|
||||||
|
|
||||||
|
# Execute the query with parameters
|
||||||
|
result = self.execute(sql_query, params)
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
return [] # No exercises found in the given time range
|
||||||
|
|
||||||
|
# Add the "selected" property to each exercise
|
||||||
|
exercises = []
|
||||||
|
for row in result:
|
||||||
|
exercises.append({
|
||||||
|
"id": row["exercise_id"],
|
||||||
|
"name": row["exercise_name"],
|
||||||
|
"selected": row["exercise_id"] in selected_exercise_ids
|
||||||
|
})
|
||||||
|
|
||||||
|
return exercises
|
||||||
|
|
||||||
|
def get_people_with_selection(self, selected_people_ids):
|
||||||
|
# SQL query to fetch all people
|
||||||
|
sql_query = """
|
||||||
|
SELECT DISTINCT
|
||||||
|
p.person_id AS id,
|
||||||
|
p.name AS name
|
||||||
|
FROM
|
||||||
|
person p
|
||||||
|
ORDER BY
|
||||||
|
p.name ASC;
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Execute the query (no parameters required since we're fetching all people)
|
||||||
|
result = self.execute(sql_query)
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
return [] # No people found
|
||||||
|
|
||||||
|
# Add the "selected" property to each person
|
||||||
|
people = []
|
||||||
|
for row in result:
|
||||||
|
people.append({
|
||||||
|
"id": row["id"],
|
||||||
|
"name": row["name"],
|
||||||
|
"selected": row["id"] in selected_people_ids
|
||||||
|
})
|
||||||
|
|
||||||
|
return people
|
||||||
|
|
||||||
|
def generate_exercise_progress_graphs(self, person_id, exercise_id, exercise_name, exercise_sets):
|
||||||
|
# Extract the required data
|
||||||
|
estimated_1rm = [t["estimated_1rm"] for t in exercise_sets]
|
||||||
|
repetitions = [t["reps"] for t in exercise_sets]
|
||||||
|
weight = [t["weight"] for t in exercise_sets]
|
||||||
|
start_dates = [t["workout_start_date"] for t in exercise_sets]
|
||||||
|
messages = [
|
||||||
|
f'{t["reps"]} x {t["weight"]}kg ({t["estimated_1rm"]}kg E1RM) on {t["workout_start_date"].strftime("%d %b %y")}'
|
||||||
|
for t in exercise_sets
|
||||||
|
]
|
||||||
|
epoch = "All"
|
||||||
|
|
||||||
|
# Check for valid data before generating the graph
|
||||||
|
if exercise_name and estimated_1rm and repetitions and weight and start_dates and messages:
|
||||||
|
exercise_progress = get_exercise_graph_model(
|
||||||
|
title=exercise_name,
|
||||||
|
estimated_1rm=estimated_1rm,
|
||||||
|
repetitions=repetitions,
|
||||||
|
weight=weight,
|
||||||
|
start_dates=start_dates,
|
||||||
|
messages=messages,
|
||||||
|
epoch=epoch,
|
||||||
|
person_id=person_id,
|
||||||
|
exercise_id=exercise_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
return exercise_progress
|
||||||
|
|
||||||
|
|
||||||
|
def get(self, selected_people_ids, start_date, end_date, selected_exercise_ids):
|
||||||
|
# Create placeholders for selected_people_ids and selected_exercise_ids
|
||||||
|
people_placeholders = ", ".join(["%s"] * len(selected_people_ids))
|
||||||
|
exercise_placeholders = ", ".join(["%s"] * len(selected_exercise_ids))
|
||||||
|
|
||||||
|
# SQL query to fetch data
|
||||||
|
sql_query = f"""
|
||||||
|
SELECT
|
||||||
|
p.person_id,
|
||||||
|
p.name AS person_name,
|
||||||
|
e.exercise_id,
|
||||||
|
e.name AS exercise_name,
|
||||||
|
t.topset_id,
|
||||||
|
t.repetitions,
|
||||||
|
t.weight,
|
||||||
|
w.start_date AS workout_date,
|
||||||
|
w.workout_id
|
||||||
|
FROM
|
||||||
|
person p
|
||||||
|
JOIN
|
||||||
|
workout w ON p.person_id = w.person_id
|
||||||
|
JOIN
|
||||||
|
topset t ON w.workout_id = t.workout_id
|
||||||
|
JOIN
|
||||||
|
exercise e ON t.exercise_id = e.exercise_id
|
||||||
|
WHERE
|
||||||
|
p.person_id IN ({people_placeholders})
|
||||||
|
AND w.start_date BETWEEN %s AND %s
|
||||||
|
AND e.exercise_id IN ({exercise_placeholders})
|
||||||
|
ORDER BY
|
||||||
|
p.person_id ASC, e.exercise_id ASC, t.topset_id DESC;
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Add parameters for the query
|
||||||
|
params = selected_people_ids + [start_date, end_date] + selected_exercise_ids
|
||||||
|
|
||||||
|
# Execute the query
|
||||||
|
result = self.execute(sql_query, params)
|
||||||
|
|
||||||
|
# Handle empty result
|
||||||
|
if not result:
|
||||||
|
return {"people": []}
|
||||||
|
|
||||||
|
# Organize data into the desired structure
|
||||||
|
people_map = {}
|
||||||
|
|
||||||
|
for row in result:
|
||||||
|
# Person level
|
||||||
|
person_id = row["person_id"]
|
||||||
|
if person_id not in people_map:
|
||||||
|
people_map[person_id] = {
|
||||||
|
"id": person_id,
|
||||||
|
"name": row["person_name"],
|
||||||
|
"exercises": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Exercise level
|
||||||
|
exercise_id = row["exercise_id"]
|
||||||
|
person_exercises = people_map[person_id]["exercises"]
|
||||||
|
if exercise_id not in person_exercises:
|
||||||
|
person_exercises[exercise_id] = {
|
||||||
|
"id": exercise_id,
|
||||||
|
"name": row["exercise_name"],
|
||||||
|
"sets": []
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set level
|
||||||
|
person_exercises[exercise_id]["sets"].append({
|
||||||
|
"id": row["topset_id"],
|
||||||
|
"reps": row["repetitions"],
|
||||||
|
"weight": row["weight"],
|
||||||
|
"exercise_id": row["exercise_id"],
|
||||||
|
"exercise_name": row["exercise_name"],
|
||||||
|
"workout_id": row["workout_id"],
|
||||||
|
"workout_start_date": row["workout_date"],
|
||||||
|
"estimated_1rm": calculate_estimated_1rm(row["weight"], row["repetitions"])
|
||||||
|
})
|
||||||
|
|
||||||
|
# Convert the map into a list of people, generate graphs, and organize exercises
|
||||||
|
people = []
|
||||||
|
for person_id, person_data in people_map.items():
|
||||||
|
exercises = []
|
||||||
|
for exercise_id, exercise_data in person_data["exercises"].items():
|
||||||
|
# Sort sets by timestamp (descending)
|
||||||
|
exercise_data["sets"] = sorted(
|
||||||
|
exercise_data["sets"], key=lambda x: x["id"], reverse=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Generate the graph for the exercise
|
||||||
|
graph = self.generate_exercise_progress_graphs(
|
||||||
|
person_id=person_id,
|
||||||
|
exercise_id=exercise_id,
|
||||||
|
exercise_name=exercise_data["name"],
|
||||||
|
exercise_sets=exercise_data["sets"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add the graph to the exercise data
|
||||||
|
exercises.append({
|
||||||
|
"id": exercise_data["id"],
|
||||||
|
"name": exercise_data["name"],
|
||||||
|
"graph": graph,
|
||||||
|
"sets": exercise_data["sets"]
|
||||||
|
})
|
||||||
|
person_data["exercises"] = exercises
|
||||||
|
people.append(person_data)
|
||||||
|
|
||||||
|
return {"dashboard": people}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -20,10 +20,8 @@
|
|||||||
placeholder: 'Filter people',
|
placeholder: 'Filter people',
|
||||||
})
|
})
|
||||||
end">
|
end">
|
||||||
{% for p in people %}
|
{% for person in people %}
|
||||||
<option value="{{ p['PersonId'] }}" {% if p['PersonId'] in selected_person_ids %}selected{%
|
<option value="{{ person.id }}" {% if person.selected %}selected{% endif %}>{{ person.name
|
||||||
endif %}>{{
|
|
||||||
p['Name']
|
|
||||||
}}</option>
|
}}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
@@ -47,9 +45,9 @@
|
|||||||
placeholder: 'Filter exercises',
|
placeholder: 'Filter exercises',
|
||||||
})
|
})
|
||||||
end">
|
end">
|
||||||
{% for e in exercises %}
|
{% for exercise in exercises %}
|
||||||
<option value="{{ e.exercise_id }}" {% if e.exercise_id in selected_exercise_ids
|
<option value="{{ exercise.id }}" {% if exercise.selected %}selected{% endif %}>
|
||||||
%}selected{% endif %}>{{ e.name }}</option>
|
{{ exercise.name }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -102,7 +100,7 @@
|
|||||||
hx-target="this" hx-swap="outerHTML">
|
hx-target="this" hx-swap="outerHTML">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if model['People']|length == 0 %}
|
{% if dashboard|length == 0 %}
|
||||||
<div class="bg-purple-100 rounded-lg py-5 px-6 mb-4 text-base text-purple-700 mb-3" role="alert" id="no-workouts">
|
<div class="bg-purple-100 rounded-lg py-5 px-6 mb-4 text-base text-purple-700 mb-3" role="alert" id="no-workouts">
|
||||||
No workouts selected.
|
No workouts selected.
|
||||||
</div>
|
</div>
|
||||||
@@ -110,36 +108,35 @@
|
|||||||
|
|
||||||
<div class="w-full grid grid-cols-1 xl:grid-cols-3 2xl:grid-cols-3 gap-4">
|
<div class="w-full grid grid-cols-1 xl:grid-cols-3 2xl:grid-cols-3 gap-4">
|
||||||
|
|
||||||
{% for p in model['People'] %}
|
{% for person in dashboard %}
|
||||||
<div class="bg-white shadow rounded-lg p-4 sm:p-6 xl:p-8 ">
|
<div class="bg-white shadow rounded-lg p-4 sm:p-6 xl:p-8 ">
|
||||||
|
|
||||||
<div class="mb-4 flex items-center justify-between">
|
<div class="mb-4 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-xl font-bold text-gray-900 mb-2">{{ p['PersonName'] }}</h3>
|
<h3 class="text-xl font-bold text-gray-900 mb-2">{{ person.name }}</h3>
|
||||||
<span class="text-base font-normal text-gray-500">Current rep maxes</span>
|
<span class="text-base font-normal text-gray-500">Current rep maxes</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
<a hx-get="{{ url_for('get_calendar' ,person_id=p['PersonId']) }}" hx-push-url="true"
|
<a hx-get="{{ url_for('get_calendar', person_id=person.id) }}" hx-push-url="true" hx-target="#container"
|
||||||
hx-target="#container"
|
|
||||||
class="text-sm font-medium text-cyan-600 hover:bg-gray-100 rounded-lg p-2 cursor-pointer">View
|
class="text-sm font-medium text-cyan-600 hover:bg-gray-100 rounded-lg p-2 cursor-pointer">View
|
||||||
workouts</a>
|
workouts</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if p['NumberOfWorkouts'] == 0 %}
|
{% if person.exercises|length == 0 %}
|
||||||
<div class="bg-purple-100 rounded-lg py-5 px-6 mb-4 text-base text-purple-700 mb-3" role="alert">
|
<div class="bg-purple-100 rounded-lg py-5 px-6 mb-4 text-base text-purple-700 mb-3" role="alert">
|
||||||
No workouts completed.
|
No workouts completed.
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% for e in p['Exercises'] %}
|
{% for exercise in person.exercises %}
|
||||||
{% if e['Topsets']|length > 1 %}
|
{% if exercise.sets|length > 1 %}
|
||||||
<div class="flex flex-col mt-8">
|
<div class="flex flex-col mt-8">
|
||||||
<div class="overflow-x-auto rounded-lg">
|
<div class="overflow-x-auto rounded-lg">
|
||||||
<div class="align-middle inline-block min-w-full">
|
<div class="align-middle inline-block min-w-full">
|
||||||
<div class="shadow overflow-hidden sm:rounded-lg">
|
<div class="shadow overflow-hidden sm:rounded-lg">
|
||||||
<div class="w-full mt-2 pb-2 aspect-video">
|
<div class="w-full mt-2 pb-2 aspect-video">
|
||||||
{{ render_partial('partials/sparkline.html', **e['ExerciseProgressGraph']) }}
|
{{ render_partial('partials/sparkline.html', **exercise.graph) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class="min-w-full divide-y divide-gray-200">
|
<table class="min-w-full divide-y divide-gray-200">
|
||||||
@@ -161,16 +158,19 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody class="bg-white">
|
<tbody class="bg-white">
|
||||||
|
|
||||||
{% for rm in e['Topsets'] %}
|
{% for set in exercise.sets %}
|
||||||
<tr>
|
<tr hx-get="{{ url_for('goto_tag') }}"
|
||||||
|
hx-vals='{"filter": "?exercise_id={{ set.exercise_id }}", "person_id" : "{{ person.id }}" }'
|
||||||
|
hx-target="#container" hx-swap="innerHTML" hx-push-url="true"
|
||||||
|
class="cursor-pointer">
|
||||||
<td class="p-4 whitespace-nowrap text-sm font-normal text-gray-500">
|
<td class="p-4 whitespace-nowrap text-sm font-normal text-gray-500">
|
||||||
{{ rm['StartDate'] }}
|
{{ set.workout_start_date | strftime("%b %d %Y") }}
|
||||||
</td>
|
</td>
|
||||||
<td class="p-4 whitespace-nowrap text-sm font-semibold text-gray-900">
|
<td class="p-4 whitespace-nowrap text-sm font-semibold text-gray-900">
|
||||||
{{ rm['Repetitions'] }} x {{ rm['Weight'] }}kg
|
{{ set.reps }} x {{ set.weight }}kg
|
||||||
</td>
|
</td>
|
||||||
<td class="p-4 whitespace-nowrap text-sm font-semibold text-gray-900">
|
<td class="p-4 whitespace-nowrap text-sm font-semibold text-gray-900">
|
||||||
{{ rm['Estimated1RM'] }}kg
|
{{ set.estimated_1rm }}kg
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
142
utils.py
142
utils.py
@@ -5,119 +5,6 @@ import pandas as pd
|
|||||||
import plotly.express as px
|
import plotly.express as px
|
||||||
import plotly.io as pio
|
import plotly.io as pio
|
||||||
|
|
||||||
def get_workouts(topsets):
|
|
||||||
# Ensure all entries have 'WorkoutId' and 'TopSetId', then sort by 'WorkoutId' and 'TopSetId'
|
|
||||||
filtered_topsets = sorted(
|
|
||||||
[t for t in topsets if t['WorkoutId'] is not None and t['TopSetId'] is not None],
|
|
||||||
key=lambda x: (x['WorkoutId'], x['TopSetId'])
|
|
||||||
)
|
|
||||||
|
|
||||||
workouts = {}
|
|
||||||
for t in filtered_topsets:
|
|
||||||
workout_id = t['WorkoutId']
|
|
||||||
if workout_id not in workouts:
|
|
||||||
workouts[workout_id] = {
|
|
||||||
'WorkoutId': workout_id,
|
|
||||||
'StartDate': t['StartDate'],
|
|
||||||
'TopSets': []
|
|
||||||
}
|
|
||||||
workouts[workout_id]['TopSets'].append({
|
|
||||||
'TopSetId': t['TopSetId'],
|
|
||||||
'ExerciseId': t['ExerciseId'],
|
|
||||||
'ExerciseName': t['ExerciseName'],
|
|
||||||
'Weight': t['Weight'],
|
|
||||||
'Repetitions': t['Repetitions'],
|
|
||||||
'Estimated1RM': t['Estimated1RM']
|
|
||||||
})
|
|
||||||
|
|
||||||
# Convert the workouts dictionary back to a list and sort by 'StartDate'
|
|
||||||
sorted_workouts = sorted(workouts.values(), key=lambda x: x['StartDate'], reverse=True)
|
|
||||||
|
|
||||||
return sorted_workouts
|
|
||||||
|
|
||||||
|
|
||||||
def get_all_exercises_from_topsets(topsets):
|
|
||||||
exercises_dict = {}
|
|
||||||
for t in topsets:
|
|
||||||
exercise_id = t.get('ExerciseId')
|
|
||||||
if exercise_id and exercise_id not in exercises_dict:
|
|
||||||
exercises_dict[exercise_id] = {
|
|
||||||
'ExerciseId': exercise_id,
|
|
||||||
'ExerciseName': t.get('ExerciseName', 'Unknown')
|
|
||||||
}
|
|
||||||
return list(exercises_dict.values())
|
|
||||||
|
|
||||||
def get_topsets_for_person(person_topsets):
|
|
||||||
# Group topsets by ExerciseId
|
|
||||||
grouped_topsets = {}
|
|
||||||
for topset in person_topsets:
|
|
||||||
exercise_id = topset['ExerciseId']
|
|
||||||
if exercise_id in grouped_topsets:
|
|
||||||
grouped_topsets[exercise_id].append(topset)
|
|
||||||
else:
|
|
||||||
grouped_topsets[exercise_id] = [topset]
|
|
||||||
|
|
||||||
# Process each group of topsets
|
|
||||||
exercises_topsets = []
|
|
||||||
for exercise_id, topsets in grouped_topsets.items():
|
|
||||||
# Sort topsets by StartDate in descending order
|
|
||||||
sorted_topsets = sorted(topsets, key=lambda x: x['StartDate'], reverse=True)
|
|
||||||
|
|
||||||
# Extracting values and calculating value ranges for SVG dimensions
|
|
||||||
estimated_1rm = [t['Estimated1RM'] for t in sorted_topsets]
|
|
||||||
repetitions = [t['Repetitions'] for t in sorted_topsets]
|
|
||||||
weight = [t['Weight'] for t in sorted_topsets]
|
|
||||||
start_dates = [t['StartDate'] for t in sorted_topsets]
|
|
||||||
messages = [f'{t["Repetitions"]} x {t["Weight"]}kg ({t["Estimated1RM"]}kg E1RM) on {t["StartDate"].strftime("%d %b %y")}' for t in sorted_topsets]
|
|
||||||
epoch = 'All'
|
|
||||||
person_id = sorted_topsets[0]['PersonId']
|
|
||||||
exercise_name = sorted_topsets[0]['ExerciseName']
|
|
||||||
|
|
||||||
if exercise_name and estimated_1rm and repetitions and weight and start_dates and messages:
|
|
||||||
exercise_progress = get_exercise_graph_model(exercise_name, estimated_1rm, repetitions, weight, start_dates, messages, epoch, person_id, exercise_id)
|
|
||||||
|
|
||||||
exercises_topsets.append({
|
|
||||||
'ExerciseId': exercise_id,
|
|
||||||
'ExerciseName': exercise_name,
|
|
||||||
'Topsets': sorted_topsets,
|
|
||||||
'ExerciseProgressGraph': exercise_progress
|
|
||||||
})
|
|
||||||
|
|
||||||
return exercises_topsets
|
|
||||||
|
|
||||||
def get_people_and_exercise_rep_maxes(topsets, selected_person_ids, selected_exercise_ids, min_date, max_date):
|
|
||||||
# Filter topsets once based on the criteria
|
|
||||||
filtered_topsets = [
|
|
||||||
t for t in topsets if t['PersonId'] in selected_person_ids
|
|
||||||
and t['ExerciseId'] in selected_exercise_ids
|
|
||||||
and min_date <= t['StartDate'] <= max_date
|
|
||||||
]
|
|
||||||
|
|
||||||
# Group the filtered topsets by PersonId
|
|
||||||
grouped_by_person = {}
|
|
||||||
for t in filtered_topsets:
|
|
||||||
person_id = t['PersonId']
|
|
||||||
if person_id in grouped_by_person:
|
|
||||||
grouped_by_person[person_id].append(t)
|
|
||||||
else:
|
|
||||||
grouped_by_person[person_id] = [t]
|
|
||||||
|
|
||||||
people = []
|
|
||||||
for person_id, person_topsets in grouped_by_person.items():
|
|
||||||
person_name = person_topsets[0]['PersonName']
|
|
||||||
workout_ids = {t['WorkoutId'] for t in person_topsets if t['WorkoutId']}
|
|
||||||
number_of_workouts = len(workout_ids)
|
|
||||||
|
|
||||||
people.append({
|
|
||||||
'PersonId': person_id,
|
|
||||||
'PersonName': person_name,
|
|
||||||
'NumberOfWorkouts': number_of_workouts,
|
|
||||||
'Exercises': get_topsets_for_person(person_topsets)
|
|
||||||
})
|
|
||||||
|
|
||||||
return {"People": people}
|
|
||||||
|
|
||||||
|
|
||||||
def convert_str_to_date(date_str, format='%Y-%m-%d'):
|
def convert_str_to_date(date_str, format='%Y-%m-%d'):
|
||||||
try:
|
try:
|
||||||
return datetime.strptime(date_str, format).date()
|
return datetime.strptime(date_str, format).date()
|
||||||
@@ -126,35 +13,6 @@ def convert_str_to_date(date_str, format='%Y-%m-%d'):
|
|||||||
except TypeError:
|
except TypeError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def flatten_list(list_of_lists):
|
|
||||||
return [item for sublist in list_of_lists for item in sublist]
|
|
||||||
|
|
||||||
|
|
||||||
def first_and_last_visible_days_in_month(first_day_of_month, last_day_of_month):
|
|
||||||
start = dict([(6, 0), (0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)])
|
|
||||||
start_date = first_day_of_month - \
|
|
||||||
timedelta(days=start[first_day_of_month.weekday()])
|
|
||||||
|
|
||||||
end = dict([(6, 6), (0, 5), (1, 4), (2, 3), (3, 2), (4, 1), (5, 0)])
|
|
||||||
end_date = last_day_of_month + \
|
|
||||||
timedelta(days=end[last_day_of_month.weekday()])
|
|
||||||
return (start_date, end_date)
|
|
||||||
|
|
||||||
|
|
||||||
def flatten(lst):
|
|
||||||
"""
|
|
||||||
Flatten a list of lists.
|
|
||||||
"""
|
|
||||||
result = []
|
|
||||||
for item in lst:
|
|
||||||
if isinstance(item, list):
|
|
||||||
result.extend(flatten(item))
|
|
||||||
else:
|
|
||||||
result.append(item)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def get_exercise_graph_model(title, estimated_1rm, repetitions, weight, start_dates, messages, epoch, person_id, exercise_id, min_date=None, max_date=None):
|
def get_exercise_graph_model(title, estimated_1rm, repetitions, weight, start_dates, messages, epoch, person_id, exercise_id, min_date=None, max_date=None):
|
||||||
# Precompute ranges
|
# Precompute ranges
|
||||||
min_date, max_date = min(start_dates), max(start_dates)
|
min_date, max_date = min(start_dates), max(start_dates)
|
||||||
|
|||||||
Reference in New Issue
Block a user