Add options to choose type of bike, and also persist distance, power, calories, speed to db
This commit is contained in:
53
app.py
53
app.py
@@ -28,7 +28,17 @@ class User(db.Model):
|
|||||||
__tablename__ = 'users'
|
__tablename__ = 'users'
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
name = db.Column(db.String(255), nullable=False)
|
name = db.Column(db.String(255), nullable=False)
|
||||||
|
bike_id = db.Column(db.Integer, db.ForeignKey(
|
||||||
|
'bikes.id', ondelete='CASCADE'), nullable=False)
|
||||||
workouts = db.relationship('Workout', backref='user', lazy=True)
|
workouts = db.relationship('Workout', backref='user', lazy=True)
|
||||||
|
bike = db.relationship('Bike', backref='user', lazy=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Bike(db.Model):
|
||||||
|
__tablename__ = 'bikes'
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
display_name = db.Column(db.String(255), nullable=False)
|
||||||
|
code_name = db.Column(db.String(255), nullable=False)
|
||||||
|
|
||||||
|
|
||||||
class Workout(db.Model):
|
class Workout(db.Model):
|
||||||
@@ -48,10 +58,13 @@ class CadenceReading(db.Model):
|
|||||||
'workouts.id', ondelete='CASCADE'), nullable=False)
|
'workouts.id', ondelete='CASCADE'), nullable=False)
|
||||||
created_at = db.Column(db.DateTime, nullable=False)
|
created_at = db.Column(db.DateTime, nullable=False)
|
||||||
rpm = db.Column(db.Integer, nullable=False)
|
rpm = db.Column(db.Integer, nullable=False)
|
||||||
|
distance = db.Column(db.Numeric, nullable=False)
|
||||||
|
speed = db.Column(db.Numeric, nullable=False)
|
||||||
|
calories = db.Column(db.Numeric, nullable=False)
|
||||||
|
power = db.Column(db.Integer, nullable=False)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/', methods=['GET'])
|
def render_users_and_workouts():
|
||||||
def get_workouts():
|
|
||||||
users = User.query.all()
|
users = User.query.all()
|
||||||
users_data = []
|
users_data = []
|
||||||
for user in users:
|
for user in users:
|
||||||
@@ -59,11 +72,17 @@ def get_workouts():
|
|||||||
user_data = {
|
user_data = {
|
||||||
'id': user.id,
|
'id': user.id,
|
||||||
'name': user.name,
|
'name': user.name,
|
||||||
|
'bike_id': user.bike_id,
|
||||||
'workouts_count': len(workouts),
|
'workouts_count': len(workouts),
|
||||||
'workouts': workouts
|
'workouts': workouts
|
||||||
}
|
}
|
||||||
users_data.append(user_data)
|
users_data.append(user_data)
|
||||||
return render_template('users_and_workouts.html', users=users_data)
|
return render_template('users_and_workouts.html', users=users_data, bikes=Bike.query.all())
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/', methods=['GET'])
|
||||||
|
def get_workouts():
|
||||||
|
return render_users_and_workouts()
|
||||||
|
|
||||||
|
|
||||||
@app.route("/user/<int:user_id>/new_workout")
|
@app.route("/user/<int:user_id>/new_workout")
|
||||||
@@ -85,14 +104,14 @@ def users():
|
|||||||
# create a new user
|
# create a new user
|
||||||
data = request.form
|
data = request.form
|
||||||
name = data['name']
|
name = data['name']
|
||||||
|
bike_id = data['bike_id']
|
||||||
|
|
||||||
# create a new user and add it to the database
|
# create a new user and add it to the database
|
||||||
new_user = User(name=name)
|
new_user = User(name=name, bike_id=bike_id)
|
||||||
db.session.add(new_user)
|
db.session.add(new_user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
users = User.query.all()
|
return render_users_and_workouts()
|
||||||
return render_template('users.html', users=users)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/user/<int:user_id>', methods=['DELETE'])
|
@app.route('/user/<int:user_id>', methods=['DELETE'])
|
||||||
@@ -101,8 +120,7 @@ def delete_user(user_id):
|
|||||||
if user:
|
if user:
|
||||||
db.session.delete(user)
|
db.session.delete(user)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
users = User.query.all()
|
return render_users_and_workouts()
|
||||||
return render_template('users.html', users=users)
|
|
||||||
else:
|
else:
|
||||||
return jsonify({'error': 'User not found.'}), 404
|
return jsonify({'error': 'User not found.'}), 404
|
||||||
|
|
||||||
@@ -115,7 +133,7 @@ def workouts(user_id):
|
|||||||
|
|
||||||
elif request.method == 'POST':
|
elif request.method == 'POST':
|
||||||
data = request.json
|
data = request.json
|
||||||
rpm_readings = data['workout']
|
data = data['workout']
|
||||||
|
|
||||||
# create a new workout
|
# create a new workout
|
||||||
workout = Workout(user_id=user_id)
|
workout = Workout(user_id=user_id)
|
||||||
@@ -123,9 +141,9 @@ def workouts(user_id):
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
# add cadence readings to the workout
|
# add cadence readings to the workout
|
||||||
for reading in rpm_readings:
|
for d in data:
|
||||||
cadence_reading = CadenceReading(
|
cadence_reading = CadenceReading(
|
||||||
workout_id=workout.id, created_at=reading['timestamp'], rpm=reading['rpm'])
|
workout_id=workout.id, created_at=d['timestamp'], rpm=d['rpm'], distance=d['distance'], speed=d['speed'], calories=d['calories'], power=d['power'])
|
||||||
db.session.add(cadence_reading)
|
db.session.add(cadence_reading)
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
@@ -186,6 +204,19 @@ def workouts_for_user(user_id):
|
|||||||
return render_template('workouts_list.html', workouts=workouts_data)
|
return render_template('workouts_list.html', workouts=workouts_data)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/user/<int:user_id>/bike', methods=['GET'])
|
||||||
|
def update_users_bike(user_id):
|
||||||
|
bike_id = request.args.get('bike_id')
|
||||||
|
user = User.query.get(user_id)
|
||||||
|
bike = Bike.query.get(bike_id)
|
||||||
|
if user and bike:
|
||||||
|
user.bike_id = bike_id
|
||||||
|
db.session.commit()
|
||||||
|
return render_users_and_workouts()
|
||||||
|
else:
|
||||||
|
return jsonify({'error': 'User or bike not found.'}), 404
|
||||||
|
|
||||||
|
|
||||||
def get_workouts_for_user(user_id):
|
def get_workouts_for_user(user_id):
|
||||||
workouts_data = []
|
workouts_data = []
|
||||||
workouts = Workout.query.filter_by(user_id=user_id).order_by(
|
workouts = Workout.query.filter_by(user_id=user_id).order_by(
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
CREATE TABLE
|
CREATE TABLE
|
||||||
users (id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL);
|
users (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
bike_id INTEGER NOT NULL REFERENCES bikes (id) ON DELETE CASCADE DEFAULT '1',
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE
|
CREATE TABLE
|
||||||
workouts (
|
workouts (
|
||||||
@@ -14,5 +18,26 @@ CREATE TABLE
|
|||||||
workout_id INTEGER NOT NULL REFERENCES workouts (id) ON DELETE CASCADE,
|
workout_id INTEGER NOT NULL REFERENCES workouts (id) ON DELETE CASCADE,
|
||||||
created_at TIMESTAMP NOT NULL,
|
created_at TIMESTAMP NOT NULL,
|
||||||
rpm INTEGER NOT NULL,
|
rpm INTEGER NOT NULL,
|
||||||
|
distance NUMERIC NOT NULL DEFAULT '0',
|
||||||
|
power NUMERIC NOT NULL DEFAULT '0',
|
||||||
|
speed NUMERIC NOT NULL DEFAULT '0',
|
||||||
|
calories NUMERIC NOT NULL DEFAULT '0',
|
||||||
CONSTRAINT unique_cadence_reading_per_workout_time UNIQUE (workout_id, created_at)
|
CONSTRAINT unique_cadence_reading_per_workout_time UNIQUE (workout_id, created_at)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE
|
||||||
|
bikes (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
display_name VARCHAR(255) NOT NULL,
|
||||||
|
code_name VARCHAR(255) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO
|
||||||
|
bikes (code_name, display_name)
|
||||||
|
VALUES
|
||||||
|
('ad6', 'AirDyne6'),
|
||||||
|
('aab', 'Assault Air Bike'),
|
||||||
|
('echo', 'Echo Bike'),
|
||||||
|
('bikeergDamper10', 'BikeErg D10'),
|
||||||
|
('bikeergDamper5', 'BikeErg D5'),
|
||||||
|
('bikeergDamper1', 'BikeErg D1');
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{{ user.name }}
|
{{ user.name }} - {{ user.bike.display_name }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
@@ -324,8 +324,8 @@
|
|||||||
// Add the new data point to the data array
|
// Add the new data point to the data array
|
||||||
data.push(newData);
|
data.push(newData);
|
||||||
|
|
||||||
let watts = generators.aab.rpm.power(rpm)
|
let watts = generators['{{ user.bike.code_name }}'].rpm.power(rpm)
|
||||||
let speed = generators.aab.rpm.speed(rpm)
|
let speed = generators['{{ user.bike.code_name }}'].rpm.speed(rpm)
|
||||||
if (previousReadingTime) {
|
if (previousReadingTime) {
|
||||||
distance += calculateDistance(previousReadingTime, new Date(), speed)
|
distance += calculateDistance(previousReadingTime, new Date(), speed)
|
||||||
calories += calculateCalories(previousReadingTime, new Date(), watts)
|
calories += calculateCalories(previousReadingTime, new Date(), watts)
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr class="bg-gray-200 text-gray-600 uppercase text-sm leading-normal">
|
<tr class="bg-gray-200 text-gray-600 uppercase text-sm leading-normal">
|
||||||
<th class="py-3 px-6 text-left">Name</th>
|
<th class="py-3 px-6 text-left">Name</th>
|
||||||
<th class="py-3 px-6 text-left">Workouts</th>
|
<th class="py-3 px-6 text-left">Bike</th>
|
||||||
|
<th class="py-3 px-6 text-left"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -19,19 +20,39 @@
|
|||||||
</path>
|
</path>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<div>
|
<div class="flex">
|
||||||
{{ u.name }}
|
<p>{{ u.name }}</p>
|
||||||
|
<p class="ml-1 prose-sm">({{ u.workouts_count }})</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-4 px-6 border-b border-gray-200 flex inline">
|
<td class="py-2 px-6 border-b border-gray-200">
|
||||||
|
<div class="relative">
|
||||||
|
<select
|
||||||
|
class="block appearance-none w-full bg-gray-200 border border-gray-200 text-gray-700 py-3 px-4 pr-8 rounded leading-tight focus:outline-none focus:bg-white focus:border-gray-500"
|
||||||
|
id="grid-state" name="bike_id" required
|
||||||
|
hx-get="{{ url_for('update_users_bike', user_id=u.id) }}" hx-target="#users-container">
|
||||||
|
{% for b in bikes %}
|
||||||
|
<option value="{{ b.id }}" {% if u.bike_id==b.id %} selected {% endif %}>{{ b.display_name }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700">
|
||||||
|
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||||
|
<path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="py-5 px-6 border-b border-gray-200 flex inline">
|
||||||
<div class="flex justify-between w-full">
|
<div class="flex justify-between w-full">
|
||||||
<div>{{ u.workouts_count }}</div>
|
<div></div>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
|
|
||||||
<div hx-delete="{{ url_for('delete_user', user_id=u.id) }}"
|
<div hx-delete="{{ url_for('delete_user', user_id=u.id) }}"
|
||||||
hx-confirm="Are you sure you wish to delete your account {{ u.name }}?"
|
hx-confirm="Are you sure you wish to delete your account {{ u.name }}?"
|
||||||
hx-target="#users-container" class="pr-2">
|
hx-target="#users-container" class="cursor-pointer pr-2">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||||
stroke="currentColor" class="w-6 h-6">
|
stroke="currentColor" class="w-6 h-6">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round"
|
<path stroke-linecap="round" stroke-linejoin="round"
|
||||||
@@ -39,7 +60,8 @@
|
|||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div hx-get="{{ url_for('workouts', user_id=u.id) }}" hx-target="#container">
|
<div hx-get="{{ url_for('workouts', user_id=u.id) }}" hx-target="#container"
|
||||||
|
class="cursor-pointer">
|
||||||
<svg class="w-6 h-6 dark:text-white" xmlns="http://www.w3.org/2000/svg" fill="none"
|
<svg class="w-6 h-6 dark:text-white" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||||
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round"
|
<path stroke-linecap="round" stroke-linejoin="round"
|
||||||
@@ -55,16 +77,41 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<form class="w-full" hx-post="{{ url_for('users') }}" hx-target="#users-container">
|
<form class="w-full mt-4" hx-post="{{ url_for('users') }}" hx-target="#users-container">
|
||||||
<div class="flex items-center border-b border-teal-500 py-2">
|
|
||||||
<input
|
|
||||||
class="appearance-none bg-transparent border-none w-full text-gray-700 mr-3 py-1 px-2 leading-tight focus:outline-none"
|
|
||||||
type="text" placeholder="Jane Doe" aria-label="Full name" name="name" required>
|
|
||||||
<button
|
|
||||||
class="flex-shrink-0 bg-teal-500 hover:bg-teal-700 border-teal-500 hover:border-teal-700 text-sm border-4 text-white py-1 px-2 rounded mr-5"
|
|
||||||
type="submit">
|
|
||||||
Add
|
|
||||||
</button>
|
|
||||||
|
|
||||||
|
<div class="flex flex-wrap mb-2">
|
||||||
|
<div class="w-full md:w-1/2 px-3 mb-6 md:mb-0">
|
||||||
|
<label class="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2" for="grid-city">
|
||||||
|
Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
class="appearance-none block w-full bg-gray-200 text-gray-700 border border-gray-200 rounded py-3 px-4 leading-tight focus:outline-none focus:bg-white focus:border-gray-500"
|
||||||
|
id="grid-city" type="text" placeholder="Full name" name="name" required>
|
||||||
|
</div>
|
||||||
|
<div class="w-full md:w-1/2 px-3 mb-4">
|
||||||
|
<label class="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2" for="grid-state">
|
||||||
|
Bike
|
||||||
|
</label>
|
||||||
|
<div class="relative">
|
||||||
|
<select
|
||||||
|
class="block appearance-none w-full bg-gray-200 border border-gray-200 text-gray-700 py-3 px-4 pr-8 rounded leading-tight focus:outline-none focus:bg-white focus:border-gray-500"
|
||||||
|
id="grid-state" name="bike_id" required>
|
||||||
|
{% for b in bikes %}
|
||||||
|
<option value="{{ b.id }}">{{ b.display_name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700">
|
||||||
|
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||||
|
<path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="shadow bg-purple-500 hover:bg-purple-400 focus:shadow-outline focus:outline-none text-white font-bold py-2 px-4 rounded ml-3 w-full md:w-auto"
|
||||||
|
type="submit">
|
||||||
|
Add User
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -33,19 +33,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="container mx-auto mt-8">
|
<div class="max-w-4xl mx-auto mt-8">
|
||||||
<div class="flex flex-col sm:flex-row justify-between">
|
<div class="flex flex-col justify-between">
|
||||||
<div class="w-full sm:w-1/2 px-4 mb-8">
|
<div class="w-full px-4 mb-8">
|
||||||
|
|
||||||
<div class="bg-white shadow-md rounded-lg overflow-hidden" id="users-container">
|
<div class="bg-white shadow-md rounded-lg overflow-hidden" id="users-container">
|
||||||
|
|
||||||
{% with users=users %}
|
{% with users=users, bikes=bikes %}
|
||||||
{% include 'users.html' %}
|
{% include 'users.html' %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full sm:w-1/2 px-4 mb-8" id="container">
|
<div class="w-full px-4 mb-8" id="container">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user