feat: Refactor notes functionality into blueprint
- Moved notes-related routes (viewing/editing workout notes) from `app.py` into a new blueprint at `routes/notes.py`. - Integrated notes-specific database logic (fetching and updating notes) directly into `routes/notes.py` helper functions, removing the corresponding methods from `db.py` for better encapsulation. - Registered the new `notes_bp` blueprint in `app.py`. - Removed the original notes route definitions from `app.py`. - Updated `url_for` calls in `templates/partials/workout_note.html` to reference the new blueprint endpoints (e.g., `notes.get_person_notes`). - Updated `templates/changelog/changelog.html` to document this refactoring in its own entry.
This commit is contained in:
33
app.py
33
app.py
@@ -8,6 +8,7 @@ from decorators import validate_person, validate_topset, validate_workout
|
||||
from routes.auth import auth, get_person_by_id
|
||||
from routes.changelog import changelog_bp
|
||||
from routes.calendar import calendar_bp # Import the new calendar blueprint
|
||||
from routes.notes import notes_bp # Import the new notes blueprint
|
||||
from extensions import db
|
||||
from utils import convert_str_to_date, generate_plot
|
||||
from flask_htmx import HTMX
|
||||
@@ -36,6 +37,7 @@ def load_user(person_id):
|
||||
app.register_blueprint(auth, url_prefix='/auth')
|
||||
app.register_blueprint(changelog_bp, url_prefix='/changelog')
|
||||
app.register_blueprint(calendar_bp) # Register the calendar blueprint
|
||||
app.register_blueprint(notes_bp) # Register the notes blueprint
|
||||
|
||||
@app.after_request
|
||||
def response_minify(response):
|
||||
@@ -130,13 +132,8 @@ def person_overview(person_id):
|
||||
return render_template('person_overview.html', **render_args), 200, {"HX-Push-Url": url_for('person_overview', person_id=person_id, min_date=min_date, max_date=max_date, exercise_id=selected_exercise_ids), "HX-Trigger": "refreshStats"}
|
||||
|
||||
|
||||
@ app.route("/person/<int:person_id>/notes", methods=['GET'])
|
||||
@ validate_person
|
||||
def get_person_notes(person_id):
|
||||
(person_name, workout_notes) = db.get_workout_notes_for_person(person_id)
|
||||
if htmx:
|
||||
return render_block(app.jinja_env, 'notes.html', 'content', person_id=person_id, person_name=person_name, workout_notes=workout_notes)
|
||||
return render_template('notes.html', person_id=person_id, person_name=person_name, workout_notes=workout_notes)
|
||||
# Route moved to routes/notes.py
|
||||
|
||||
|
||||
@ app.route("/person/<int:person_id>/workout", methods=['POST'])
|
||||
def create_workout(person_id):
|
||||
@@ -331,27 +328,7 @@ def delete_tag(tag_id):
|
||||
db.delete_tag_for_dashboard(tag_id)
|
||||
return redirect(url_for('dashboard') + tag_filter)
|
||||
|
||||
|
||||
@ app.route("/person/<int:person_id>/workout/<int:workout_id>/note/edit", methods=['GET'])
|
||||
@ validate_workout
|
||||
def get_workout_note_edit_form(person_id, workout_id):
|
||||
workout = db.get_workout(person_id, workout_id)
|
||||
return render_template('partials/workout_note.html', person_id=person_id, workout_id=workout_id, note=workout['note'], is_edit=True)
|
||||
|
||||
|
||||
@ app.route("/person/<int:person_id>/workout/<int:workout_id>/note", methods=['PUT'])
|
||||
@ validate_workout
|
||||
def update_workout_note(person_id, workout_id):
|
||||
note = request.form.get('note')
|
||||
db.update_workout_note_for_person(person_id, workout_id, note)
|
||||
return render_template('partials/workout_note.html', person_id=person_id, workout_id=workout_id, note=note)
|
||||
|
||||
|
||||
@ app.route("/person/<int:person_id>/workout/<int:workout_id>/note", methods=['GET'])
|
||||
@ validate_workout
|
||||
def get_workout_note(person_id, workout_id):
|
||||
workout = db.get_workout(person_id, workout_id)
|
||||
return render_template('partials/workout_note.html', person_id=person_id, workout_id=workout_id, note=workout['note'])
|
||||
# Routes moved to routes/notes.py
|
||||
|
||||
|
||||
@ app.route("/person/<int:person_id>/workout/<int:workout_id>/tag/add", methods=['POST'])
|
||||
|
||||
55
db.py
55
db.py
@@ -345,9 +345,7 @@ class DataBase():
|
||||
def delete_tag_for_dashboard(self, tag_id):
|
||||
self.execute('DELETE FROM Tag WHERE tag_id=%s', [tag_id], commit=True)
|
||||
|
||||
def update_workout_note_for_person(self, person_id, workout_id, note):
|
||||
self.execute('UPDATE workout SET note=%s WHERE person_id=%s AND workout_id=%s', [
|
||||
note, person_id, workout_id], commit=True)
|
||||
# Note update logic moved to routes/notes.py
|
||||
|
||||
def add_tag_for_workout(self, workout_id, tags_id):
|
||||
# If tags_id is not empty, delete tags that are not in the new selection
|
||||
@@ -537,58 +535,9 @@ class DataBase():
|
||||
degree)
|
||||
|
||||
return exercise_progress
|
||||
|
||||
def get_workout_notes_for_person(self, person_id):
|
||||
sql_query = """
|
||||
SELECT
|
||||
p.name AS person_name,
|
||||
w.workout_id,
|
||||
to_char(w.start_date, 'Mon DD YYYY') AS formatted_start_date,
|
||||
w.note,
|
||||
t.filter AS tag_filter,
|
||||
t.name AS tag_name
|
||||
FROM person p
|
||||
LEFT JOIN workout w ON p.person_id = w.person_id AND w.note IS NOT NULL AND w.note <> ''
|
||||
LEFT JOIN workout_tag wt ON w.workout_id = wt.workout_id
|
||||
LEFT JOIN tag t ON wt.tag_id = t.tag_id
|
||||
WHERE p.person_id = %s
|
||||
ORDER BY w.start_date DESC, w.workout_id, t.name;
|
||||
"""
|
||||
|
||||
# Execute the SQL query
|
||||
raw_data = self.execute(sql_query, [person_id])
|
||||
# Note fetching logic moved to routes/notes.py
|
||||
|
||||
if not raw_data:
|
||||
return None, []
|
||||
|
||||
# Extract person name from the first row (all rows have the same person name)
|
||||
person_name = raw_data[0]['person_name']
|
||||
|
||||
# Process the workout notes
|
||||
workout_notes = {}
|
||||
for row in raw_data:
|
||||
workout_id = row['workout_id']
|
||||
if workout_id and row['note']:
|
||||
# Initialize the workout entry if it doesn't exist
|
||||
if workout_id not in workout_notes:
|
||||
workout_notes[workout_id] = {
|
||||
'workout_id': workout_id,
|
||||
'formatted_start_date': row['formatted_start_date'],
|
||||
'note': row['note'],
|
||||
'tags': []
|
||||
}
|
||||
# Add tags if present
|
||||
if row['tag_name']:
|
||||
workout_notes[workout_id]['tags'].append({
|
||||
'tag_filter': row['tag_filter'],
|
||||
'tag_name': row['tag_name'],
|
||||
'person_id': person_id
|
||||
})
|
||||
|
||||
# Convert to a list for the final output
|
||||
workout_notes_list = list(workout_notes.values())
|
||||
return person_name, workout_notes_list
|
||||
|
||||
def get_exercise_earliest_and_latest_dates(self, person_id, exercise_id):
|
||||
sql_query = """
|
||||
SELECT
|
||||
|
||||
@@ -154,7 +154,7 @@ def _process_workouts_for_year_view(grouped_workouts, year_date):
|
||||
|
||||
months_data.append({
|
||||
'name': first_day_of_month.strftime('%B'),
|
||||
'first_day_of_month': first_day_of_month.strftime('%Y-%m-%d'), # Pass date string for URL
|
||||
'first_day_of_month': first_day_of_month, # Pass the actual date object
|
||||
'days': month_days_data
|
||||
})
|
||||
return months_data
|
||||
@@ -175,7 +175,7 @@ def get_calendar(person_id):
|
||||
if selected_view == 'overview':
|
||||
return redirect(url_for('person_overview', person_id=person_id))
|
||||
if selected_view == 'notes':
|
||||
return redirect(url_for('get_person_notes', person_id=person_id))
|
||||
return redirect(url_for('notes.get_person_notes', person_id=person_id))
|
||||
|
||||
try:
|
||||
start_date, end_date, prev_date, next_date = _get_date_range_and_links(selected_date, selected_view)
|
||||
|
||||
101
routes/notes.py
Normal file
101
routes/notes.py
Normal file
@@ -0,0 +1,101 @@
|
||||
from flask import Blueprint, render_template, request, current_app
|
||||
from jinja2_fragments import render_block
|
||||
from flask_htmx import HTMX
|
||||
from extensions import db # Still need db for execute method
|
||||
from decorators import validate_person, validate_workout
|
||||
|
||||
notes_bp = Blueprint('notes', __name__)
|
||||
htmx = HTMX()
|
||||
|
||||
# --- Database Helper Functions (Moved from db.py) ---
|
||||
|
||||
def _fetch_workout_notes_for_person(person_id):
|
||||
"""Fetches and processes workout notes for a person directly."""
|
||||
sql_query = """
|
||||
SELECT
|
||||
p.name AS person_name,
|
||||
w.workout_id,
|
||||
to_char(w.start_date, 'Mon DD YYYY') AS formatted_start_date,
|
||||
w.note,
|
||||
t.filter AS tag_filter,
|
||||
t.name AS tag_name
|
||||
FROM person p
|
||||
LEFT JOIN workout w ON p.person_id = w.person_id AND w.note IS NOT NULL AND w.note <> ''
|
||||
LEFT JOIN workout_tag wt ON w.workout_id = wt.workout_id
|
||||
LEFT JOIN tag t ON wt.tag_id = t.tag_id
|
||||
WHERE p.person_id = %s
|
||||
ORDER BY w.start_date DESC, w.workout_id, t.name;
|
||||
"""
|
||||
raw_data = db.execute(sql_query, [person_id])
|
||||
|
||||
if not raw_data:
|
||||
# Attempt to get person name even if no notes exist
|
||||
person_name_result = db.execute("SELECT name FROM person WHERE person_id = %s", [person_id], one=True)
|
||||
person_name = person_name_result['name'] if person_name_result else "Unknown"
|
||||
return person_name, []
|
||||
|
||||
person_name = raw_data[0]['person_name']
|
||||
workout_notes = {}
|
||||
for row in raw_data:
|
||||
workout_id = row['workout_id']
|
||||
if workout_id and row['note']:
|
||||
if workout_id not in workout_notes:
|
||||
workout_notes[workout_id] = {
|
||||
'workout_id': workout_id,
|
||||
'formatted_start_date': row['formatted_start_date'],
|
||||
'note': row['note'],
|
||||
'tags': []
|
||||
}
|
||||
if row['tag_name']:
|
||||
workout_notes[workout_id]['tags'].append({
|
||||
'tag_filter': row['tag_filter'],
|
||||
'tag_name': row['tag_name'],
|
||||
'person_id': person_id
|
||||
})
|
||||
|
||||
return person_name, list(workout_notes.values())
|
||||
|
||||
def _fetch_workout_note(person_id, workout_id):
|
||||
"""Fetches a single workout note directly."""
|
||||
# Simplified query just to get the note for a specific workout
|
||||
query = "SELECT note FROM workout WHERE person_id = %s AND workout_id = %s"
|
||||
result = db.execute(query, [person_id, workout_id], one=True)
|
||||
return result['note'] if result else ''
|
||||
|
||||
def _update_workout_note_for_person(person_id, workout_id, note):
|
||||
"""Updates a workout note directly."""
|
||||
db.execute('UPDATE workout SET note=%s WHERE person_id=%s AND workout_id=%s',
|
||||
[note, person_id, workout_id], commit=True)
|
||||
|
||||
# --- Routes ---
|
||||
|
||||
@notes_bp.route("/person/<int:person_id>/notes", methods=['GET'])
|
||||
@validate_person
|
||||
def get_person_notes(person_id):
|
||||
"""Displays all workout notes for a given person."""
|
||||
person_name, workout_notes = _fetch_workout_notes_for_person(person_id) # Use local helper
|
||||
if htmx:
|
||||
return render_block(current_app.jinja_env, 'notes.html', 'content', person_id=person_id, person_name=person_name, workout_notes=workout_notes)
|
||||
return render_template('notes.html', person_id=person_id, person_name=person_name, workout_notes=workout_notes)
|
||||
|
||||
@notes_bp.route("/person/<int:person_id>/workout/<int:workout_id>/note/edit", methods=['GET'])
|
||||
@validate_workout
|
||||
def get_workout_note_edit_form(person_id, workout_id):
|
||||
"""Returns the form to edit a specific workout note."""
|
||||
note = _fetch_workout_note(person_id, workout_id) # Use local helper
|
||||
return render_template('partials/workout_note.html', person_id=person_id, workout_id=workout_id, note=note, is_edit=True)
|
||||
|
||||
@notes_bp.route("/person/<int:person_id>/workout/<int:workout_id>/note", methods=['PUT'])
|
||||
@validate_workout
|
||||
def update_workout_note(person_id, workout_id):
|
||||
"""Updates a specific workout note."""
|
||||
note = request.form.get('note')
|
||||
_update_workout_note_for_person(person_id, workout_id, note) # Use local helper
|
||||
return render_template('partials/workout_note.html', person_id=person_id, workout_id=workout_id, note=note)
|
||||
|
||||
@notes_bp.route("/person/<int:person_id>/workout/<int:workout_id>/note", methods=['GET'])
|
||||
@validate_workout
|
||||
def get_workout_note(person_id, workout_id):
|
||||
"""Returns the display partial for a specific workout note."""
|
||||
note = _fetch_workout_note(person_id, workout_id) # Use local helper
|
||||
return render_template('partials/workout_note.html', person_id=person_id, workout_id=workout_id, note=note)
|
||||
@@ -10,6 +10,24 @@
|
||||
<div class="prose max-w-none">
|
||||
<p>Updates and changes to the site will be documented here, with the most recent changes listed first.</p>
|
||||
|
||||
<!-- New Entry for Calendar Year View Fix -->
|
||||
<hr class="my-6">
|
||||
<h2 class="text-xl font-semibold mb-2">March 31, 2025</h2>
|
||||
<ul class="list-disc pl-5 space-y-1">
|
||||
<li>Fixed `AttributeError` in calendar year view caused by passing a date string instead of a date
|
||||
object to the template.</li>
|
||||
</ul>
|
||||
|
||||
<!-- New Entry for Notes Refactoring -->
|
||||
<hr class="my-6">
|
||||
<h2 class="text-xl font-semibold mb-2">March 31, 2025</h2>
|
||||
<ul class="list-disc pl-5 space-y-1">
|
||||
<li>Refactored notes functionality (viewing/editing workout notes) into its own blueprint
|
||||
(`routes/notes.py`).</li>
|
||||
<li>Moved notes-specific database logic from `db.py` into the `routes/notes.py` blueprint for better
|
||||
encapsulation.</li>
|
||||
</ul>
|
||||
|
||||
<!-- New Entry for Refactoring -->
|
||||
<hr class="my-6">
|
||||
<h2 class="text-xl font-semibold mb-2">March 30, 2025</h2>
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
{% if note|length > 0 %}
|
||||
<span class="text-base font-normal text-gray-500 whitespace-normal">{{ note | replace('\n', '<br>') | safe }}</span>
|
||||
<a class="text-sm font-medium text-cyan-600 hover:bg-gray-100 rounded-lg inline-flex items-center p-2 cursor-pointer"
|
||||
hx-get="{{ url_for('get_workout_note_edit_form', person_id=person_id, workout_id=workout_id) }}"
|
||||
hx-get="{{ url_for('notes.get_workout_note_edit_form', person_id=person_id, workout_id=workout_id) }}"
|
||||
hx-target="#workout-note-{{workout_id}}">
|
||||
Edit
|
||||
</a>
|
||||
{% else %}
|
||||
<a class="text-sm font-medium text-cyan-600 hover:bg-gray-100 rounded-lg inline-flex items-center p-2 cursor-pointer float-none sm:float-right"
|
||||
hx-get="{{ url_for('get_workout_note_edit_form', person_id=person_id, workout_id=workout_id) }}"
|
||||
hx-get="{{ url_for('notes.get_workout_note_edit_form', person_id=person_id, workout_id=workout_id) }}"
|
||||
hx-target="#workout-note-{{workout_id}}">
|
||||
Add note
|
||||
</a>
|
||||
@@ -24,7 +24,7 @@
|
||||
<div class="flex flex-col">
|
||||
<button
|
||||
class="inline-flex justify-center p-2 text-blue-600 rounded-full cursor-pointer hover:bg-blue-100 dark:text-blue-500 dark:hover:bg-gray-600"
|
||||
hx-get="{{ url_for('get_workout_note', person_id=person_id, workout_id=workout_id) }}"
|
||||
hx-get="{{ url_for('notes.get_workout_note', person_id=person_id, workout_id=workout_id) }}"
|
||||
hx-target="#workout-note-{{workout_id}}">
|
||||
<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">
|
||||
@@ -34,7 +34,7 @@
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex justify-center p-2 text-blue-600 rounded-full cursor-pointer hover:bg-blue-100 dark:text-blue-500 dark:hover:bg-gray-600"
|
||||
hx-put="{{ url_for('update_workout_note', person_id=person_id, workout_id=workout_id) }}"
|
||||
hx-put="{{ url_for('notes.update_workout_note', person_id=person_id, workout_id=workout_id) }}"
|
||||
hx-include="[name='note']">
|
||||
<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">
|
||||
|
||||
Reference in New Issue
Block a user