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:
Peter Stockings
2022-11-20 14:14:04 +11:00
parent 93fc2fcfb8
commit 3dcc61005e
5 changed files with 161 additions and 74 deletions

91
app.py
View File

@@ -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
View File

@@ -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

View File

@@ -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">

View 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 %}

View File

@@ -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">