Compare commits
8 Commits
afc5749c82
...
b4121eada7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4121eada7 | ||
|
|
a6a71f3139 | ||
|
|
9998616946 | ||
|
|
c20f2e2f85 | ||
|
|
ec8d7f6825 | ||
|
|
2e79ad1b8b | ||
|
|
d223bdeebc | ||
|
|
9a2ce6754a |
4
app.py
4
app.py
@@ -22,12 +22,16 @@ from flask_htmx import HTMX
|
|||||||
import minify_html
|
import minify_html
|
||||||
import os
|
import os
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
from flask_compress import Compress
|
||||||
|
|
||||||
# Load environment variables from .env file in non-production environments
|
# Load environment variables from .env file in non-production environments
|
||||||
if os.environ.get('FLASK_ENV') != 'production':
|
if os.environ.get('FLASK_ENV') != 'production':
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
app.config['COMPRESS_REGISTER'] = True
|
||||||
|
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 31536000 # 1 year
|
||||||
|
Compress(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)
|
||||||
|
|||||||
29
db.py
29
db.py
@@ -1,10 +1,11 @@
|
|||||||
import os
|
import os
|
||||||
import psycopg2
|
import psycopg2
|
||||||
|
from psycopg2 import pool
|
||||||
from psycopg2.extras import RealDictCursor
|
from psycopg2.extras import RealDictCursor
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
from flask import g
|
from flask import g, current_app
|
||||||
from features.exercises import Exercises
|
from features.exercises import Exercises
|
||||||
from features.people_graphs import PeopleGraphs
|
from features.people_graphs import PeopleGraphs
|
||||||
from features.person_overview import PersonOverview
|
from features.person_overview import PersonOverview
|
||||||
@@ -16,6 +17,8 @@ from utils import get_exercise_graph_model
|
|||||||
|
|
||||||
|
|
||||||
class DataBase():
|
class DataBase():
|
||||||
|
_pool = None
|
||||||
|
|
||||||
def __init__(self, app=None):
|
def __init__(self, app=None):
|
||||||
self.stats = Stats(self.execute)
|
self.stats = Stats(self.execute)
|
||||||
self.exercises = Exercises(self.execute)
|
self.exercises = Exercises(self.execute)
|
||||||
@@ -25,29 +28,29 @@ class DataBase():
|
|||||||
self.schema = Schema(self.execute)
|
self.schema = Schema(self.execute)
|
||||||
self.activityRequest = Activity(self.execute)
|
self.activityRequest = Activity(self.execute)
|
||||||
|
|
||||||
db_url = urlparse(os.environ['DATABASE_URL'])
|
if not os.environ.get('DATABASE_URL'):
|
||||||
# if db_url is null then throw error
|
|
||||||
if not db_url:
|
|
||||||
raise Exception("No DATABASE_URL environment variable set")
|
raise Exception("No DATABASE_URL environment variable set")
|
||||||
|
|
||||||
def getDB(self):
|
if DataBase._pool is None:
|
||||||
db = getattr(g, 'database', None)
|
|
||||||
if db is None:
|
|
||||||
db_url = urlparse(os.environ['DATABASE_URL'])
|
db_url = urlparse(os.environ['DATABASE_URL'])
|
||||||
g.database = psycopg2.connect(
|
DataBase._pool = pool.ThreadedConnectionPool(
|
||||||
|
1, 20, # minconn, maxconn
|
||||||
database=db_url.path[1:],
|
database=db_url.path[1:],
|
||||||
user=db_url.username,
|
user=db_url.username,
|
||||||
password=db_url.password,
|
password=db_url.password,
|
||||||
host=db_url.hostname,
|
host=db_url.hostname,
|
||||||
port=db_url.port
|
port=db_url.port
|
||||||
)
|
)
|
||||||
db = g.database
|
|
||||||
return db
|
|
||||||
|
|
||||||
def close_connection(exception):
|
def getDB(self):
|
||||||
db = getattr(g, 'database', None)
|
if 'database' not in g:
|
||||||
|
g.database = self._pool.getconn()
|
||||||
|
return g.database
|
||||||
|
|
||||||
|
def close_connection(self, exception=None):
|
||||||
|
db = g.pop('database', None)
|
||||||
if db is not None:
|
if db is not None:
|
||||||
db.close()
|
self._pool.putconn(db)
|
||||||
|
|
||||||
def execute(self, query, args=(), one=False, commit=False):
|
def execute(self, query, args=(), one=False, commit=False):
|
||||||
conn = self.getDB()
|
conn = self.getDB()
|
||||||
|
|||||||
@@ -17,4 +17,5 @@ Flask-Bcrypt==1.0.1
|
|||||||
email-validator==2.2.0
|
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
|
||||||
63
static/css/tailwind.min.css
vendored
63
static/css/tailwind.min.css
vendored
File diff suppressed because one or more lines are too long
12
static/css/tw-elements.min.css
vendored
12
static/css/tw-elements.min.css
vendored
File diff suppressed because one or more lines are too long
2029
static/js/mermaid.min.js
vendored
2029
static/js/mermaid.min.js
vendored
File diff suppressed because one or more lines are too long
8
static/js/plotly-2.35.2.min.js
vendored
8
static/js/plotly-2.35.2.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
18
static/js/tw-elements.min.js
vendored
18
static/js/tw-elements.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
<link href="/static/css/style.css" rel="stylesheet">
|
<link href="/static/css/style.css" rel="stylesheet">
|
||||||
<script src="/static/js/htmx.min.js" defer></script>
|
<script src="/static/js/htmx.min.js" defer></script>
|
||||||
<script src="/static/js/hyperscript.min.js"></script>
|
<script src="/static/js/hyperscript.min.js" defer></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -130,8 +130,12 @@
|
|||||||
<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', **exercise.graph) }}
|
hx-get="{{ url_for('get_exercise_progress_for_user', person_id=person.id, exercise_id=exercise.id, min_date=min_date, max_date=max_date) }}"
|
||||||
|
hx-trigger="intersect once" hx-swap="outerHTML">
|
||||||
|
<div class="flex items-center justify-center h-full bg-gray-50 rounded-lg">
|
||||||
|
<div class="text-sm text-gray-400 animate-pulse font-medium">Loading graph...</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class="min-w-full divide-y divide-gray-200">
|
<table class="min-w-full divide-y divide-gray-200">
|
||||||
|
|||||||
@@ -105,7 +105,13 @@
|
|||||||
|
|
||||||
<div class="mt-4 mb-4 w-full grid grid-cols-1 2xl:grid-cols-2 gap-4">
|
<div class="mt-4 mb-4 w-full grid grid-cols-1 2xl:grid-cols-2 gap-4">
|
||||||
{% for graph in exercise_progress_graphs %}
|
{% for graph in exercise_progress_graphs %}
|
||||||
{{ render_partial('partials/sparkline.html', **graph.progress_graph) }}
|
<div hx-get="{{ url_for('get_exercise_progress_for_user', person_id=person_id, exercise_id=graph.exercise_id, min_date=min_date, max_date=max_date) }}"
|
||||||
|
hx-trigger="intersect once" hx-swap="outerHTML">
|
||||||
|
<div class="flex items-center justify-center h-48 bg-gray-50 rounded-lg">
|
||||||
|
<div class="text-sm text-gray-400 animate-pulse font-medium">Loading {{ graph.exercise_name }}...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
18
utils.py
18
utils.py
@@ -48,18 +48,22 @@ def get_exercise_graph_model(title, estimated_1rm, repetitions, weight, start_da
|
|||||||
|
|
||||||
best_fit_points = []
|
best_fit_points = []
|
||||||
try:
|
try:
|
||||||
if len(relative_positions) > 1: # Ensure there are enough points for polyfit
|
# Filter out NaNs if any (though scaled values shouldn't have them if ranges are correct)
|
||||||
# Fit a polynomial of the given degree
|
mask = ~np.isnan(estimated_1rm_scaled)
|
||||||
coeffs = np.polyfit(relative_positions, estimated_1rm_scaled, degree)
|
x_fit = relative_positions[mask]
|
||||||
|
y_fit = estimated_1rm_scaled[mask]
|
||||||
|
|
||||||
|
# Ensure we have enough unique X positions for the given degree
|
||||||
|
if len(np.unique(x_fit)) > degree:
|
||||||
|
coeffs = np.polyfit(x_fit, y_fit, degree)
|
||||||
poly_fit = np.poly1d(coeffs)
|
poly_fit = np.poly1d(coeffs)
|
||||||
y_best_fit = poly_fit(relative_positions)
|
y_best_fit = poly_fit(relative_positions)
|
||||||
best_fit_points = list(zip(y_best_fit.tolist(), relative_positions.tolist()))
|
best_fit_points = list(zip(y_best_fit.tolist(), relative_positions.tolist()))
|
||||||
else:
|
else:
|
||||||
raise ValueError("Not enough data points for polyfit")
|
best_fit_points = []
|
||||||
except (np.linalg.LinAlgError, ValueError) as e:
|
except (np.linalg.LinAlgError, ValueError, TypeError) as e:
|
||||||
# Handle cases where polyfit fails
|
# Handle cases where polyfit fails or input is invalid
|
||||||
best_fit_points = []
|
best_fit_points = []
|
||||||
m, b = 0, 0
|
|
||||||
|
|
||||||
# Prepare data for plots
|
# Prepare data for plots
|
||||||
repetitions_data = {
|
repetitions_data = {
|
||||||
|
|||||||
Reference in New Issue
Block a user