Convert edit/add/remove people form in settings to htmx driven and trigger event to refresh list of people and workout count on insert/update/remove person
This commit is contained in:
91
app.py
91
app.py
@@ -19,6 +19,12 @@ def dashboard():
|
|||||||
return render_template('index.html', model=people_and_exercise_rep_maxes)
|
return render_template('index.html', model=people_and_exercise_rep_maxes)
|
||||||
|
|
||||||
|
|
||||||
|
@ app.route("/person/list", methods=['GET'])
|
||||||
|
def get_person_list():
|
||||||
|
people = db.get_people_and_workout_count(-1)
|
||||||
|
return render_template('partials/people_link.html', people=people)
|
||||||
|
|
||||||
|
|
||||||
@ app.route("/person/<int:person_id>")
|
@ app.route("/person/<int:person_id>")
|
||||||
@ validate_person
|
@ validate_person
|
||||||
def get_person(person_id):
|
def get_person(person_id):
|
||||||
@@ -216,14 +222,91 @@ def delete_topset(person_id, workout_id, topset_id):
|
|||||||
@ app.route("/person", methods=['POST'])
|
@ app.route("/person", methods=['POST'])
|
||||||
def create_person():
|
def create_person():
|
||||||
name = request.form.get("name")
|
name = request.form.get("name")
|
||||||
db.create_person(name)
|
new_person_id = db.create_person(name)
|
||||||
return redirect(url_for('settings'))
|
return f"""
|
||||||
|
<tr>
|
||||||
|
<td class="p-4 whitespace-nowrap text-sm font-semibold text-gray-900">
|
||||||
|
{ name }
|
||||||
|
</td>
|
||||||
|
<td class="p-4 whitespace-nowrap text-sm font-semibold text-gray-900">
|
||||||
|
<a hx-get="{ url_for('get_person_edit_form', person_id=new_person_id) }"
|
||||||
|
class="text-sm font-medium text-cyan-600 hover:bg-gray-100 rounded-lg inline-flex items-center p-2 cursor-pointer">
|
||||||
|
Edit
|
||||||
|
</a>
|
||||||
|
<a hx-delete="{ url_for('delete_person', person_id=new_person_id) }"
|
||||||
|
class="text-sm font-medium text-cyan-600 hover:bg-gray-100 rounded-lg inline-flex items-center p-2 cursor-pointer">
|
||||||
|
Remove
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
""", 200, {"HX-Trigger": "updatedPeople"}
|
||||||
|
|
||||||
|
|
||||||
@ app.route("/person/<int:person_id>/delete", methods=['GET', 'POST'])
|
@ app.route("/person/<int:person_id>/delete", methods=['DELETE'])
|
||||||
def delete_person(person_id):
|
def delete_person(person_id):
|
||||||
db.delete_person(person_id)
|
db.delete_person(person_id)
|
||||||
return redirect(url_for('settings'))
|
return "", 200, {"HX-Trigger": "updatedPeople"}
|
||||||
|
|
||||||
|
|
||||||
|
@ app.route("/person/<int:person_id>/edit_form", methods=['GET'])
|
||||||
|
def get_person_edit_form(person_id):
|
||||||
|
person = db.get_person(person_id)
|
||||||
|
return f"""
|
||||||
|
<tr>
|
||||||
|
<td class="p-4 whitespace-nowrap text-sm font-semibold text-gray-900">
|
||||||
|
<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" type="text" name="name" value="{person['PersonName']}">
|
||||||
|
</td>
|
||||||
|
<td class="p-4 whitespace-nowrap text-sm font-semibold text-gray-900">
|
||||||
|
<a hx-put="{ url_for('update_person_name', person_id=person_id) }" hx-include="closest tr" class="text-sm font-medium text-cyan-600 hover:bg-gray-100 rounded-lg inline-flex items-center p-2 cursor-pointer">
|
||||||
|
Update
|
||||||
|
</a>
|
||||||
|
<a hx-get="{url_for('get_person_name', person_id=person_id)}" class="text-sm font-medium text-cyan-600 hover:bg-gray-100 rounded-lg inline-flex items-center p-2 cursor-pointer">
|
||||||
|
Cancel
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ app.route("/person/<int:person_id>/name", methods=['PUT'])
|
||||||
|
def update_person_name(person_id):
|
||||||
|
new_name = request.form.get("name")
|
||||||
|
db.update_person_name(person_id, new_name)
|
||||||
|
return f"""
|
||||||
|
<tr>
|
||||||
|
<td class="p-4 whitespace-nowrap text-sm font-semibold text-gray-900">
|
||||||
|
{new_name}
|
||||||
|
</td>
|
||||||
|
<td class="p-4 whitespace-nowrap text-sm font-semibold text-gray-900">
|
||||||
|
<a hx-get="{ url_for('get_person_edit_form', person_id=person_id) }" hx-include="closest tr" class="text-sm font-medium text-cyan-600 hover:bg-gray-100 rounded-lg inline-flex items-center p-2 cursor-pointer">
|
||||||
|
Edit
|
||||||
|
</a>
|
||||||
|
<a href="{ url_for('delete_person', person_id=person_id) }" class="text-sm font-medium text-cyan-600 hover:bg-gray-100 rounded-lg inline-flex items-center p-2 cursor-pointer">
|
||||||
|
Remove
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
""", 200, {"HX-Trigger": "updatedPeople"}
|
||||||
|
|
||||||
|
|
||||||
|
@ app.route("/person/<int:person_id>/name", methods=['GET'])
|
||||||
|
def get_person_name(person_id):
|
||||||
|
person = db.get_person(person_id)
|
||||||
|
return f"""
|
||||||
|
<tr>
|
||||||
|
<td class="p-4 whitespace-nowrap text-sm font-semibold text-gray-900">
|
||||||
|
{person['PersonName']}
|
||||||
|
</td>
|
||||||
|
<td class="p-4 whitespace-nowrap text-sm font-semibold text-gray-900">
|
||||||
|
<a hx-get="{ url_for('get_person_edit_form', person_id=person_id) }" hx-include="closest tr" class="text-sm font-medium text-cyan-600 hover:bg-gray-100 rounded-lg inline-flex items-center p-2 cursor-pointer">
|
||||||
|
Edit
|
||||||
|
</a>
|
||||||
|
<a href="{ url_for('delete_person', person_id=person_id) }" class="text-sm font-medium text-cyan-600 hover:bg-gray-100 rounded-lg inline-flex items-center p-2 cursor-pointer">
|
||||||
|
Remove
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
@ app.route("/exercise", methods=['POST'])
|
@ app.route("/exercise", methods=['POST'])
|
||||||
|
|||||||
87
db.py
87
db.py
@@ -65,8 +65,9 @@ class DataBase():
|
|||||||
return person
|
return person
|
||||||
|
|
||||||
def create_person(self, name):
|
def create_person(self, name):
|
||||||
self.execute('INSERT INTO Person (Name) VALUES (%s)',
|
new_person = self.execute('INSERT INTO Person (Name) VALUES (%s) RETURNING PersonId AS "PersonId"', [
|
||||||
[name], commit=True)
|
name], commit=True, one=True)
|
||||||
|
return new_person['PersonId']
|
||||||
|
|
||||||
def delete_person(self, person_id):
|
def delete_person(self, person_id):
|
||||||
self.execute('DELETE FROM TopSet WHERE WorkoutId IN (SELECT WorkoutId FROM Workout WHERE PersonId=%s)', [
|
self.execute('DELETE FROM TopSet WHERE WorkoutId IN (SELECT WorkoutId FROM Workout WHERE PersonId=%s)', [
|
||||||
@@ -76,9 +77,13 @@ class DataBase():
|
|||||||
self.execute('DELETE FROM Person WHERE PersonId=%s',
|
self.execute('DELETE FROM Person WHERE PersonId=%s',
|
||||||
[person_id], commit=True)
|
[person_id], commit=True)
|
||||||
|
|
||||||
|
def update_person_name(self, person_id, name):
|
||||||
|
self.execute('UPDATE Person SET Name=%s WHERE PersonId=%s', [
|
||||||
|
name, person_id], commit=True)
|
||||||
|
|
||||||
def is_valid_workout(self, person_id, workout_id):
|
def is_valid_workout(self, person_id, workout_id):
|
||||||
workout = self.execute('SELECT W.WorkoutId AS "WorkoutId" FROM Person P, Workout W WHERE P.PersonId=W.PersonId AND P.PersonId=%s AND W.WorkoutId=%s LIMIT 1', [
|
workout = self.execute('SELECT W.WorkoutId AS "WorkoutId" FROM Person P, Workout W WHERE P.PersonId=W.PersonId AND P.PersonId=%s AND W.WorkoutId=%s LIMIT 1', [
|
||||||
person_id, workout_id], one=True)
|
person_id, workout_id], one=True)
|
||||||
return workout
|
return workout
|
||||||
|
|
||||||
def is_valid_topset(self, person_id, workout_id, topset_id):
|
def is_valid_topset(self, person_id, workout_id, topset_id):
|
||||||
@@ -121,12 +126,12 @@ class DataBase():
|
|||||||
return self.execute("""
|
return self.execute("""
|
||||||
SELECT
|
SELECT
|
||||||
P.PersonId AS "PersonId",
|
P.PersonId AS "PersonId",
|
||||||
P.Name AS "Name",
|
P.Name AS "Name",
|
||||||
COUNT(W.WorkoutId) AS "NumberOfWorkouts",
|
COUNT(W.WorkoutId) AS "NumberOfWorkouts",
|
||||||
CASE P.PersonId
|
CASE P.PersonId
|
||||||
WHEN %s
|
WHEN %s
|
||||||
THEN 1
|
THEN 1
|
||||||
ELSE 0
|
ELSE 0
|
||||||
END "IsActive"
|
END "IsActive"
|
||||||
FROM
|
FROM
|
||||||
Person P LEFT JOIN Workout W ON P.PersonId = W.PersonId
|
Person P LEFT JOIN Workout W ON P.PersonId = W.PersonId
|
||||||
@@ -141,15 +146,15 @@ class DataBase():
|
|||||||
|
|
||||||
def get_person(self, person_id):
|
def get_person(self, person_id):
|
||||||
topsets = self.execute("""
|
topsets = self.execute("""
|
||||||
SELECT
|
SELECT
|
||||||
P.PersonId AS "PersonId",
|
P.PersonId AS "PersonId",
|
||||||
P.Name AS "PersonName",
|
P.Name AS "PersonName",
|
||||||
W.WorkoutId AS "WorkoutId",
|
W.WorkoutId AS "WorkoutId",
|
||||||
W.StartDate AS "StartDate",
|
W.StartDate AS "StartDate",
|
||||||
T.TopSetId AS "TopSetId",
|
T.TopSetId AS "TopSetId",
|
||||||
E.ExerciseId AS "ExerciseId",
|
E.ExerciseId AS "ExerciseId",
|
||||||
E.Name AS "ExerciseName",
|
E.Name AS "ExerciseName",
|
||||||
T.Repetitions AS "Repetitions",
|
T.Repetitions AS "Repetitions",
|
||||||
T.Weight AS "Weight"
|
T.Weight AS "Weight"
|
||||||
FROM Person P
|
FROM Person P
|
||||||
LEFT JOIN Workout W ON P.PersonId=W.PersonId
|
LEFT JOIN Workout W ON P.PersonId=W.PersonId
|
||||||
@@ -167,15 +172,15 @@ class DataBase():
|
|||||||
|
|
||||||
def get_workout(self, person_id, workout_id):
|
def get_workout(self, person_id, workout_id):
|
||||||
topsets = self.execute("""
|
topsets = self.execute("""
|
||||||
SELECT
|
SELECT
|
||||||
P.PersonId AS "PersonId",
|
P.PersonId AS "PersonId",
|
||||||
P.Name AS "PersonName",
|
P.Name AS "PersonName",
|
||||||
W.WorkoutId AS "WorkoutId",
|
W.WorkoutId AS "WorkoutId",
|
||||||
W.StartDate AS "StartDate",
|
W.StartDate AS "StartDate",
|
||||||
T.TopSetId AS "TopSetId",
|
T.TopSetId AS "TopSetId",
|
||||||
E.ExerciseId AS "ExerciseId",
|
E.ExerciseId AS "ExerciseId",
|
||||||
E.Name AS "ExerciseName",
|
E.Name AS "ExerciseName",
|
||||||
T.Repetitions AS "Repetitions",
|
T.Repetitions AS "Repetitions",
|
||||||
T.Weight AS "Weight"
|
T.Weight AS "Weight"
|
||||||
FROM Person P
|
FROM Person P
|
||||||
LEFT JOIN Workout W ON P.PersonId=W.PersonId
|
LEFT JOIN Workout W ON P.PersonId=W.PersonId
|
||||||
@@ -195,15 +200,15 @@ class DataBase():
|
|||||||
|
|
||||||
def get_topset(self, person_id, workout_id, topset_id):
|
def get_topset(self, person_id, workout_id, topset_id):
|
||||||
topset = self.execute("""
|
topset = self.execute("""
|
||||||
SELECT
|
SELECT
|
||||||
P.PersonId AS "PersonId",
|
P.PersonId AS "PersonId",
|
||||||
P.Name AS "PersonName",
|
P.Name AS "PersonName",
|
||||||
W.WorkoutId AS "WorkoutId",
|
W.WorkoutId AS "WorkoutId",
|
||||||
W.StartDate AS "StartDate",
|
W.StartDate AS "StartDate",
|
||||||
T.TopSetId AS "TopSetId",
|
T.TopSetId AS "TopSetId",
|
||||||
E.ExerciseId AS "ExerciseId",
|
E.ExerciseId AS "ExerciseId",
|
||||||
E.Name AS "ExerciseName",
|
E.Name AS "ExerciseName",
|
||||||
T.Repetitions AS "Repetitions",
|
T.Repetitions AS "Repetitions",
|
||||||
T.Weight AS "Weight"
|
T.Weight AS "Weight"
|
||||||
FROM Person P
|
FROM Person P
|
||||||
INNER JOIN Workout W ON P.PersonId=W.PersonId
|
INNER JOIN Workout W ON P.PersonId=W.PersonId
|
||||||
@@ -228,15 +233,15 @@ class DataBase():
|
|||||||
|
|
||||||
def get_all_topsets(self):
|
def get_all_topsets(self):
|
||||||
all_topsets = self.execute("""
|
all_topsets = self.execute("""
|
||||||
SELECT
|
SELECT
|
||||||
P.PersonId AS "PersonId",
|
P.PersonId AS "PersonId",
|
||||||
P.Name AS "PersonName",
|
P.Name AS "PersonName",
|
||||||
W.WorkoutId AS "WorkoutId",
|
W.WorkoutId AS "WorkoutId",
|
||||||
W.StartDate AS "StartDate",
|
W.StartDate AS "StartDate",
|
||||||
T.TopSetId AS "TopSetId",
|
T.TopSetId AS "TopSetId",
|
||||||
E.ExerciseId AS "ExerciseId",
|
E.ExerciseId AS "ExerciseId",
|
||||||
E.Name AS "ExerciseName",
|
E.Name AS "ExerciseName",
|
||||||
T.Repetitions AS "Repetitions",
|
T.Repetitions AS "Repetitions",
|
||||||
T.Weight AS "Weight",
|
T.Weight AS "Weight",
|
||||||
round((100 * T.Weight)/(101.3-2.67123 * T.Repetitions),0)::numeric::integer AS "Estimated1RM"
|
round((100 * T.Weight)/(101.3-2.67123 * T.Repetitions),0)::numeric::integer AS "Estimated1RM"
|
||||||
FROM Person P
|
FROM Person P
|
||||||
|
|||||||
@@ -85,25 +85,10 @@
|
|||||||
<span class="ml-3">Dashboard</span>
|
<span class="ml-3">Dashboard</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<ul class="space-y-2 pb-2">
|
<ul class="space-y-2 pb-2" hx-get="{{ url_for('get_person_list') }}"
|
||||||
|
hx-trigger="updatedPeople from:body">
|
||||||
{% for p in get_list_of_people_and_workout_count() %}
|
{{ render_partial('partials/people_link.html',
|
||||||
<li>
|
people=get_list_of_people_and_workout_count()) }}
|
||||||
<a href="{{ url_for('get_person' ,person_id=p['PersonId']) }}"
|
|
||||||
class="text-base text-gray-900 font-normal rounded-lg hover:bg-gray-100 flex items-center p-2 group {% if p['IsActive']==1 %} bg-gray-200 {% endif %}">
|
|
||||||
<svg class="w-6 h-6 text-gray-500 flex-shrink-0 group-hover:text-gray-900 transition duration-75"
|
|
||||||
fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path fill-rule="evenodd"
|
|
||||||
d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd">
|
|
||||||
</path>
|
|
||||||
</svg>
|
|
||||||
<span class="ml-3 flex-1 whitespace-nowrap">{{ p['Name']}}</span>
|
|
||||||
<span
|
|
||||||
class="bg-gray-200 text-gray-800 ml-3 text-sm font-medium inline-flex items-center justify-center px-2 rounded-full">{{
|
|
||||||
p['NumberOfWorkouts'] }}</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
<div class="space-y-2 pt-2">
|
<div class="space-y-2 pt-2">
|
||||||
|
|||||||
16
templates/partials/people_link.html
Normal file
16
templates/partials/people_link.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{% for p in people %}
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('get_person' ,person_id=p['PersonId']) }}"
|
||||||
|
class="text-base text-gray-900 font-normal rounded-lg hover:bg-gray-100 flex items-center p-2 group {% if p['IsActive']==1 %} bg-gray-200 {% endif %}">
|
||||||
|
<svg class="w-6 h-6 text-gray-500 flex-shrink-0 group-hover:text-gray-900 transition duration-75"
|
||||||
|
fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
<span class="ml-3 flex-1 whitespace-nowrap">{{ p['Name']}}</span>
|
||||||
|
<span
|
||||||
|
class="bg-gray-200 text-gray-800 ml-3 text-sm font-medium inline-flex items-center justify-center px-2 rounded-full">{{
|
||||||
|
p['NumberOfWorkouts'] }}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
@@ -17,10 +17,6 @@
|
|||||||
<table class="table-fixed min-w-full divide-y divide-gray-200">
|
<table class="table-fixed min-w-full divide-y divide-gray-200">
|
||||||
<thead class="bg-gray-50">
|
<thead class="bg-gray-50">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col"
|
|
||||||
class="p-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-0.5">
|
|
||||||
#
|
|
||||||
</th>
|
|
||||||
<th scope="col"
|
<th scope="col"
|
||||||
class="p-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-3/5">
|
class="p-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-3/5">
|
||||||
Name
|
Name
|
||||||
@@ -30,17 +26,19 @@
|
|||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="bg-white">
|
<tbody class="bg-white" id="new-person" hx-target="closest tr"
|
||||||
|
hx-swap="outerHTML swap:0.5s">
|
||||||
{% for p in people %}
|
{% for p in people %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="p-4 whitespace-nowrap text-sm font-normal text-gray-500">
|
|
||||||
{{ loop.index }}
|
|
||||||
</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">
|
||||||
{{ p['Name'] }}
|
{{ p['Name'] }}
|
||||||
</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">
|
||||||
<a href="{{ url_for('delete_person', person_id=p['PersonId']) }}"
|
<a hx-get="{{ url_for('get_person_edit_form', person_id=p['PersonId']) }}"
|
||||||
|
class="text-sm font-medium text-cyan-600 hover:bg-gray-100 rounded-lg inline-flex items-center p-2 cursor-pointer">
|
||||||
|
Edit
|
||||||
|
</a>
|
||||||
|
<a hx-delete="{{ url_for('delete_person', person_id=p['PersonId']) }}"
|
||||||
class="text-sm font-medium text-cyan-600 hover:bg-gray-100 rounded-lg inline-flex items-center p-2 cursor-pointer">
|
class="text-sm font-medium text-cyan-600 hover:bg-gray-100 rounded-lg inline-flex items-center p-2 cursor-pointer">
|
||||||
Remove
|
Remove
|
||||||
</a>
|
</a>
|
||||||
@@ -54,7 +52,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form class="w-full mt-3" action="{{ url_for('create_person') }}" method="post">
|
<form class="w-full mt-3" hx-post="{{ url_for('create_person') }}" hx-swap="beforeend" hx-target="#new-person">
|
||||||
<div class="flex flex-wrap -mx-3 mb-2">
|
<div class="flex flex-wrap -mx-3 mb-2">
|
||||||
<div class="grow px-3">
|
<div class="grow px-3">
|
||||||
<label class="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2" for="grid-city">
|
<label class="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2" for="grid-city">
|
||||||
|
|||||||
Reference in New Issue
Block a user