Change schema and switch to flask-sqlalchemy, comment out existing endpoints and add new endpoints to add/delete users & workouts
This commit is contained in:
267
app.py
267
app.py
@@ -1,86 +1,241 @@
|
|||||||
from datetime import datetime, date, timedelta
|
from datetime import timedelta
|
||||||
import decimal
|
import io
|
||||||
import json
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from urllib import response
|
from flask import Flask, make_response, render_template, request, jsonify
|
||||||
from dateutil.relativedelta import relativedelta
|
|
||||||
import os
|
|
||||||
from flask import Flask, render_template, request
|
|
||||||
import jinja_partials
|
import jinja_partials
|
||||||
from flask_htmx import HTMX
|
from flask_htmx import HTMX
|
||||||
import minify_html
|
import matplotlib.pyplot as plt
|
||||||
from urllib.parse import urlparse
|
import os
|
||||||
|
|
||||||
from db import DataBase
|
|
||||||
from graph import generate_graph, generate_sparkline_graph
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
# TODO CHANGE SECRET KEY TO ENVIRONMENT VARIABLE
|
# TODO CHANGE SECRET KEY TO ENVIRONMENT VARIABLE
|
||||||
app.config['SECRET_KEY'] = 'secret!'
|
app.config['SECRET_KEY'] = 'secret!'
|
||||||
|
uri = os.getenv("DATABASE_URL")
|
||||||
|
if uri and uri.startswith("postgres://"):
|
||||||
|
uri = uri.replace("postgres://", "postgresql://", 1)
|
||||||
|
|
||||||
|
app.config['SQLALCHEMY_DATABASE_URI'] = uri
|
||||||
|
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||||
jinja_partials.register_extensions(app)
|
jinja_partials.register_extensions(app)
|
||||||
htmx = HTMX(app)
|
htmx = HTMX(app)
|
||||||
db = DataBase(app)
|
db = SQLAlchemy(app)
|
||||||
|
|
||||||
|
|
||||||
@app.after_request
|
class User(db.Model):
|
||||||
def response_minify(response):
|
__tablename__ = 'users'
|
||||||
"""
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
minify html response to decrease site traffic
|
name = db.Column(db.String(255), nullable=False)
|
||||||
"""
|
|
||||||
if response.content_type == u'text/html; charset=utf-8':
|
|
||||||
response.set_data(
|
|
||||||
minify_html.minify(response.get_data(
|
|
||||||
as_text=True), minify_js=True, remove_processing_instructions=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
return response
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
@ app.route("/")
|
class Workout(db.Model):
|
||||||
|
__tablename__ = 'workouts'
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey(
|
||||||
|
'users.id', ondelete='CASCADE'), nullable=False)
|
||||||
|
created_at = db.Column(db.DateTime, nullable=False, default=db.func.now())
|
||||||
|
cadence_readings = db.relationship(
|
||||||
|
'CadenceReading', backref='workout', lazy=True)
|
||||||
|
|
||||||
|
|
||||||
|
class CadenceReading(db.Model):
|
||||||
|
__tablename__ = 'cadence_readings'
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
workout_id = db.Column(db.Integer, db.ForeignKey(
|
||||||
|
'workouts.id', ondelete='CASCADE'), nullable=False)
|
||||||
|
created_at = db.Column(db.DateTime, nullable=False)
|
||||||
|
rpm = db.Column(db.Integer, nullable=False)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
def home():
|
def home():
|
||||||
return render_template('attemptv2.html')
|
return render_template('attemptv2.html')
|
||||||
|
|
||||||
|
|
||||||
@ app.route("/devices")
|
@app.route('/users', methods=['GET', 'POST'])
|
||||||
def devices():
|
def users():
|
||||||
devices = db.get_devices()
|
if request.method == 'GET':
|
||||||
return render_template('devices.html', devices=devices)
|
# get a list of all users in the database
|
||||||
|
users = User.query.all()
|
||||||
|
users_list = [{'id': user.id, 'name': user.name} for user in users]
|
||||||
|
return jsonify(users_list), 200
|
||||||
|
|
||||||
|
elif request.method == 'POST':
|
||||||
|
# create a new user
|
||||||
|
data = request.json
|
||||||
|
name = data['name']
|
||||||
|
|
||||||
|
# create a new user and add it to the database
|
||||||
|
new_user = User(name=name)
|
||||||
|
db.session.add(new_user)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify({'message': 'User created successfully.'}), 201
|
||||||
|
|
||||||
|
|
||||||
@ app.route("/device/<device_id>")
|
@app.route('/user/<int:user_id>', methods=['DELETE'])
|
||||||
def device(device_id):
|
def delete_user(user_id):
|
||||||
device = db.get_device(device_id)
|
user = User.query.get(user_id)
|
||||||
return render_template('device.html', device=device)
|
if user:
|
||||||
|
db.session.delete(user)
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify({'message': 'User deleted successfully.'}), 200
|
||||||
|
else:
|
||||||
|
return jsonify({'error': 'User not found.'}), 404
|
||||||
|
|
||||||
|
|
||||||
@app.route("/overview/<device_id>")
|
@app.route('/user/<int:user_id>/workouts', methods=['GET', 'POST'])
|
||||||
def overview(device_id):
|
def create_workout(user_id):
|
||||||
cadences = db.get_all_cadences(device_id)
|
if request.method == 'GET':
|
||||||
last_cadence = cadences[-1]['rpm'] if cadences else 0
|
# get a list of all workouts for a user
|
||||||
if cadences:
|
workouts = Workout.query.filter_by(user_id=user_id).all()
|
||||||
first = cadences[0]['logged_at']
|
workouts_data = []
|
||||||
last = cadences[-1]['logged_at']
|
for workout in workouts:
|
||||||
duration = str(timedelta(seconds=(last-first).seconds))
|
cadence_readings = CadenceReading.query.filter_by(
|
||||||
|
workout_id=workout.id).all()
|
||||||
|
if cadence_readings:
|
||||||
|
# get the earliest and latest cadence readings timestamps
|
||||||
|
start_time = min(
|
||||||
|
reading.created_at for reading in cadence_readings)
|
||||||
|
end_time = max(
|
||||||
|
reading.created_at for reading in cadence_readings)
|
||||||
|
duration = end_time - start_time
|
||||||
|
# format the duration as hh:mm:ss or mm:ss or ss
|
||||||
|
if duration >= timedelta(hours=1):
|
||||||
|
duration_str = str(duration)
|
||||||
|
else:
|
||||||
|
duration_str = str(duration).split('.')[0]
|
||||||
|
workouts_data.append({
|
||||||
|
'id': workout.id,
|
||||||
|
'started_at': start_time.strftime('%a %b %d %Y %H:%M:%S'),
|
||||||
|
'finished_at': end_time.strftime('%a %b %d %Y %H:%M:%S'),
|
||||||
|
'duration': duration_str
|
||||||
|
})
|
||||||
|
return jsonify({'workouts': workouts_data}), 200
|
||||||
|
|
||||||
last_cadence = cadences[-1]['rpm']
|
elif request.method == 'POST':
|
||||||
|
data = request.json
|
||||||
|
rpm_readings = data['workout']
|
||||||
|
|
||||||
power = round(decimal.Decimal(0.0011)*last_cadence ** 3 + decimal.Decimal(
|
# create a new workout
|
||||||
0.0026) * last_cadence ** 2 + decimal.Decimal(0.5642)*last_cadence)
|
workout = Workout(user_id=user_id)
|
||||||
|
db.session.add(workout)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
graph_data = generate_sparkline_graph(
|
# add cadence readings to the workout
|
||||||
[c['rpm'] for c in cadences[-100:]])
|
for reading in rpm_readings:
|
||||||
|
cadence_reading = CadenceReading(
|
||||||
|
workout_id=workout.id, created_at=reading['timestamp'], rpm=reading['rpm'])
|
||||||
|
db.session.add(cadence_reading)
|
||||||
|
|
||||||
return render_template('overview.html', last_cadence=last_cadence, power=power, duration=duration, cadences=cadences[-15:], graph_data=graph_data)
|
db.session.commit()
|
||||||
return render_template('overview.html', last_cadence=0, power=0, duration=duration, cadences=[], graph_data='')
|
|
||||||
|
return jsonify({'message': 'Workout created successfully.'}), 201
|
||||||
|
|
||||||
|
|
||||||
@ app.route("/cadence", methods=['POST'])
|
@app.route('/user/<int:user_id>/workout/<int:workout_id>', methods=['GET', 'DELETE'])
|
||||||
def cadence():
|
def workout(user_id, workout_id):
|
||||||
data = request.get_json()
|
workout = Workout.query.filter_by(user_id=user_id, id=workout_id).first()
|
||||||
print('' + datetime.now().replace(microsecond=0).isoformat() +
|
if workout:
|
||||||
' ' + json.dumps(data))
|
if request.method == 'GET':
|
||||||
db.insert_cadence(data['rpm'], data['id'])
|
# Get the cadence readings for the workout
|
||||||
return 'ok'
|
cadence_readings = CadenceReading.query.filter_by(
|
||||||
|
workout_id=workout_id).all()
|
||||||
|
if cadence_readings:
|
||||||
|
# Create a graph of cadence readings
|
||||||
|
x_values = [reading.created_at for reading in cadence_readings]
|
||||||
|
y_values = [reading.rpm for reading in cadence_readings]
|
||||||
|
fig, ax = plt.subplots()
|
||||||
|
ax.plot(x_values, y_values)
|
||||||
|
ax.set_xlabel('Time')
|
||||||
|
ax.set_ylabel('Cadence (RPM)')
|
||||||
|
ax.set_title(
|
||||||
|
'Cadence Readings for Workout {}'.format(workout_id))
|
||||||
|
# Save the graph to a bytes buffer
|
||||||
|
buffer = io.BytesIO()
|
||||||
|
plt.savefig(buffer, format='png')
|
||||||
|
buffer.seek(0)
|
||||||
|
# Create a response object with the graph image
|
||||||
|
response = make_response(buffer.getvalue())
|
||||||
|
response.headers['Content-Type'] = 'image/png'
|
||||||
|
response.headers['Content-Disposition'] = 'attachment; filename=cadence.png'
|
||||||
|
return response, 200
|
||||||
|
else:
|
||||||
|
return jsonify({'message': 'No cadence readings for workout {}.'.format(workout_id)}), 404
|
||||||
|
elif request.method == 'DELETE':
|
||||||
|
# Delete the workout and its associated cadence readings
|
||||||
|
db.session.delete(workout)
|
||||||
|
CadenceReading.query.filter_by(workout_id=workout_id).delete()
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify({'message': 'Workout {} deleted successfully.'.format(workout_id)}), 200
|
||||||
|
else:
|
||||||
|
return jsonify({'message': 'Workout {} not found for user {}.'.format(workout_id, user_id)}), 404
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(debug=True)
|
||||||
|
|
||||||
|
|
||||||
|
# @app.after_request
|
||||||
|
# def response_minify(response):
|
||||||
|
# """
|
||||||
|
# minify html response to decrease site traffic
|
||||||
|
# """
|
||||||
|
# if response.content_type == u'text/html; charset=utf-8':
|
||||||
|
# response.set_data(
|
||||||
|
# minify_html.minify(response.get_data(
|
||||||
|
# as_text=True), minify_js=True, remove_processing_instructions=True)
|
||||||
|
# )
|
||||||
|
|
||||||
|
# return response
|
||||||
|
# return response
|
||||||
|
|
||||||
|
|
||||||
|
# @ app.route("/")
|
||||||
|
# def home():
|
||||||
|
# return render_template('attemptv2.html')
|
||||||
|
|
||||||
|
|
||||||
|
# @ app.route("/devices")
|
||||||
|
# def devices():
|
||||||
|
# devices = db.get_devices()
|
||||||
|
# return render_template('devices.html', devices=devices)
|
||||||
|
|
||||||
|
|
||||||
|
# @ app.route("/device/<device_id>")
|
||||||
|
# def device(device_id):
|
||||||
|
# device = db.get_device(device_id)
|
||||||
|
# return render_template('device.html', device=device)
|
||||||
|
|
||||||
|
|
||||||
|
# @app.route("/overview/<device_id>")
|
||||||
|
# def overview(device_id):
|
||||||
|
# cadences = db.get_all_cadences(device_id)
|
||||||
|
# last_cadence = cadences[-1]['rpm'] if cadences else 0
|
||||||
|
# if cadences:
|
||||||
|
# first = cadences[0]['logged_at']
|
||||||
|
# last = cadences[-1]['logged_at']
|
||||||
|
# duration = str(timedelta(seconds=(last-first).seconds))
|
||||||
|
|
||||||
|
# last_cadence = cadences[-1]['rpm']
|
||||||
|
|
||||||
|
# power = round(decimal.Decimal(0.0011)*last_cadence ** 3 + decimal.Decimal(
|
||||||
|
# 0.0026) * last_cadence ** 2 + decimal.Decimal(0.5642)*last_cadence)
|
||||||
|
|
||||||
|
# graph_data = generate_sparkline_graph(
|
||||||
|
# [c['rpm'] for c in cadences[-100:]])
|
||||||
|
|
||||||
|
# return render_template('overview.html', last_cadence=last_cadence, power=power, duration=duration, cadences=cadences[-15:], graph_data=graph_data)
|
||||||
|
# return render_template('overview.html', last_cadence=0, power=0, duration=duration, cadences=[], graph_data='')
|
||||||
|
|
||||||
|
|
||||||
|
# @ app.route("/cadence", methods=['POST'])
|
||||||
|
# def cadence():
|
||||||
|
# data = request.get_json()
|
||||||
|
# print('' + datetime.now().replace(microsecond=0).isoformat() +
|
||||||
|
# ' ' + json.dumps(data))
|
||||||
|
# db.insert_cadence(data['rpm'], data['id'])
|
||||||
|
# return 'ok'
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@@ -7,4 +7,6 @@ flask-htmx==0.2.0
|
|||||||
python-dateutil==2.8.2
|
python-dateutil==2.8.2
|
||||||
minify-html==0.10.3
|
minify-html==0.10.3
|
||||||
bidict==0.22.1
|
bidict==0.22.1
|
||||||
pygal==3.0.0
|
pygal==3.0.0
|
||||||
|
Flask-SQLAlchemy==3.0.3
|
||||||
|
matplotlib==3.5.2
|
||||||
@@ -63,6 +63,7 @@
|
|||||||
let isRunning = false;
|
let isRunning = false;
|
||||||
let startTime = 0;
|
let startTime = 0;
|
||||||
let intervalId = null;
|
let intervalId = null;
|
||||||
|
let workout = [];
|
||||||
|
|
||||||
const integerNumber = (num) => parseInt(num);
|
const integerNumber = (num) => parseInt(num);
|
||||||
const decimalNumber = (num) => parseFloat(num.toFixed(1));
|
const decimalNumber = (num) => parseFloat(num.toFixed(1));
|
||||||
@@ -105,6 +106,20 @@
|
|||||||
clearInterval(intervalId);
|
clearInterval(intervalId);
|
||||||
toggleButton.textContent = 'Start';
|
toggleButton.textContent = 'Start';
|
||||||
disconnect();
|
disconnect();
|
||||||
|
|
||||||
|
fetch("/user/1/workouts", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ workout: workout }),
|
||||||
|
}).then(res => res.json())
|
||||||
|
.then(res => swal("Submitted", json.stringify(res), "success"))
|
||||||
|
.catch(err => swal("Failed to submit workout", err.message, "error"));
|
||||||
|
|
||||||
|
workout = [];
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle workout
|
// Toggle workout
|
||||||
@@ -200,8 +215,10 @@
|
|||||||
characteristic.addEventListener("characteristicvaluechanged", props.onChange);
|
characteristic.addEventListener("characteristicvaluechanged", props.onChange);
|
||||||
console.log("> Characteristic value changed event listener added");
|
console.log("> Characteristic value changed event listener added");
|
||||||
|
|
||||||
|
/*
|
||||||
btn.classList.remove("bg-blue-600");
|
btn.classList.remove("bg-blue-600");
|
||||||
btn.classList.add("bg-green-600");
|
btn.classList.add("bg-green-600");
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
async function disconnect() {
|
async function disconnect() {
|
||||||
@@ -213,8 +230,10 @@
|
|||||||
"characteristicvaluechanged",
|
"characteristicvaluechanged",
|
||||||
handleNotifications
|
handleNotifications
|
||||||
);
|
);
|
||||||
|
/*
|
||||||
btn.classList.remove("bg-green-600");
|
btn.classList.remove("bg-green-600");
|
||||||
btn.classList.add("bg-blue-600");
|
btn.classList.add("bg-blue-600");
|
||||||
|
*/
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Argh! " + error);
|
console.log("Argh! " + error);
|
||||||
swal("Oops", error, "error");
|
swal("Oops", error, "error");
|
||||||
@@ -261,6 +280,8 @@
|
|||||||
|
|
||||||
updateRpmPower(rpm, 0);
|
updateRpmPower(rpm, 0);
|
||||||
updateGraph();
|
updateGraph();
|
||||||
|
|
||||||
|
workout.push({ rpm, timestamp: new Date() })
|
||||||
/*
|
/*
|
||||||
fetch("/cadence", {
|
fetch("/cadence", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
Reference in New Issue
Block a user