diff --git a/app.py b/app.py index 5c5db91..fae7c05 100644 --- a/app.py +++ b/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//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//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//workout//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//workout//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//workout//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//workout//tag/add", methods=['POST']) diff --git a/db.py b/db.py index 5b384ff..8e6e6ce 100644 --- a/db.py +++ b/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 diff --git a/routes/calendar.py b/routes/calendar.py index a56a09e..8503afe 100644 --- a/routes/calendar.py +++ b/routes/calendar.py @@ -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) diff --git a/routes/notes.py b/routes/notes.py new file mode 100644 index 0000000..61fbf2f --- /dev/null +++ b/routes/notes.py @@ -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//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//workout//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//workout//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//workout//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) \ No newline at end of file diff --git a/templates/changelog/changelog.html b/templates/changelog/changelog.html index 6a59959..dfa1bbb 100644 --- a/templates/changelog/changelog.html +++ b/templates/changelog/changelog.html @@ -10,6 +10,24 @@

Updates and changes to the site will be documented here, with the most recent changes listed first.

+ +
+

March 31, 2025

+
    +
  • Fixed `AttributeError` in calendar year view caused by passing a date string instead of a date + object to the template.
  • +
+ + +
+

March 31, 2025

+
    +
  • Refactored notes functionality (viewing/editing workout notes) into its own blueprint + (`routes/notes.py`).
  • +
  • Moved notes-specific database logic from `db.py` into the `routes/notes.py` blueprint for better + encapsulation.
  • +
+

March 30, 2025

diff --git a/templates/partials/workout_note.html b/templates/partials/workout_note.html index 33f4072..996632f 100644 --- a/templates/partials/workout_note.html +++ b/templates/partials/workout_note.html @@ -3,13 +3,13 @@ {% if note|length > 0 %} {{ note | replace('\n', '
') | safe }}
Edit {% else %} Add note @@ -24,7 +24,7 @@