Files
workout/app.py
Peter Stockings eaeb4ab2c8 feat: Refactor workout functionality into blueprint
- Moved workout-related routes (create/delete/edit workouts, topsets, tags, start dates, show workout) from `app.py` into a new blueprint at `routes/workout.py`.
- Integrated workout view model logic from `features/workout.py` directly into `routes/workout.py` helper function `_get_workout_view_model`.
- Removed `features/workout.py` and the corresponding class instantiation in `db.py`.
- Registered the new `workout_bp` blueprint in `app.py`.
- Removed the original workout route definitions from `app.py`.
- Updated `url_for` calls in relevant templates (`workout.html`, `person_overview.html`, `partials/workout_tags.html`, `partials/topset.html`, `partials/start_date.html`, `partials/new_set_form.html`, `notes.html`, `calendar.html`) to reference the new blueprint endpoints (e.g., `workout.create_workout`).
- Updated `templates/changelog/changelog.html` to document this refactoring.
2025-03-31 22:38:48 +11:00

457 lines
18 KiB
Python

from datetime import date
import os
from flask import Flask, abort, render_template, redirect, request, url_for
from flask_login import LoginManager
import jinja_partials
from jinja2_fragments import render_block
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 routes.workout import workout_bp # Import the new workout blueprint
from extensions import db
from utils import convert_str_to_date, generate_plot
from flask_htmx import HTMX
import minify_html
import os
from dotenv import load_dotenv
# Load environment variables from .env file in non-production environments
if os.environ.get('FLASK_ENV') != 'production':
load_dotenv()
app = Flask(__name__)
app.config.from_pyfile('config.py')
app.secret_key = os.environ.get('SECRET_KEY', '2a661781919643cb8a5a8bc57642d99f')
jinja_partials.register_extensions(app)
htmx = HTMX(app)
login_manager = LoginManager(app)
login_manager.login_view = 'auth.login'
login_manager.login_message_category = 'info'
@login_manager.user_loader
def load_user(person_id):
return get_person_by_id(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.register_blueprint(workout_bp) # Register the workout blueprint
@app.after_request
def response_minify(response):
"""
minify html response to decrease site traffic
"""
if response.content_type == u'text/html; charset=utf-8':
response.set_data(
minify_html.minify(response.get_data(
as_text=True), minify_js=True, remove_processing_instructions=True)
)
return response
return response
@ app.route("/")
def dashboard():
selected_people_ids = request.args.getlist('person_id', type=int)
min_date = request.args.get('min_date', type=convert_str_to_date)
max_date = request.args.get('max_date', type=convert_str_to_date)
selected_exercise_ids = request.args.getlist('exercise_id', type=int)
if not selected_people_ids and htmx.trigger_name != 'person_id':
selected_people_ids = db.dashboard.get_people_ids()
if not min_date or not max_date:
db_min_date, db_max_date = db.dashboard.get_earliest_and_latest_workout_dates(selected_people_ids)
min_date = min_date or db_min_date
max_date = max_date or db_max_date
if not selected_exercise_ids and htmx.trigger_name != 'exercise_id':
selected_exercise_ids = db.dashboard.list_of_performed_exercise_ids(selected_people_ids, min_date, max_date)
people = db.dashboard.get_people_with_selection(selected_people_ids)
exercises = db.dashboard.get_exercises_with_selection(selected_people_ids, min_date, max_date, selected_exercise_ids)
tags = db.get_tags_for_dashboard()
dashboard = db.dashboard.get(selected_people_ids, min_date, max_date, selected_exercise_ids)
# Render the appropriate response for HTMX or full page
render_args = {
**dashboard,
"people": people,
"exercises": exercises,
"tags": tags,
"selected_people_ids": selected_people_ids,
"max_date": max_date,
"min_date": min_date,
"selected_exercise_ids": selected_exercise_ids
}
if htmx:
return render_block(app.jinja_env, 'dashboard.html', 'content', **render_args)
return render_template('dashboard.html', **render_args)
@ 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>/workout/overview", methods=['GET'])
def person_overview(person_id):
min_date = request.args.get('min_date', type=convert_str_to_date)
max_date = request.args.get('max_date', type=convert_str_to_date)
selected_exercise_ids = request.args.getlist('exercise_id', type=int)
if not min_date or not max_date:
db_min_date, db_max_date = db.person_overview.get_earliest_and_latest_workout_dates(person_id)
min_date = min_date or db_min_date
max_date = max_date or db_max_date
if not selected_exercise_ids and htmx.trigger_name != 'exercise_id':
selected_exercise_ids = db.person_overview.list_of_performed_exercise_ids(person_id, min_date, max_date)
person = db.person_overview.get(person_id, min_date, max_date, selected_exercise_ids)
exercises = db.person_overview.get_exercises_with_selection(person_id, min_date, max_date, selected_exercise_ids)
tags = db.get_tags_for_person(person_id)
# Render the appropriate response for HTMX or full page
render_args = {
**person,
"exercises": exercises,
"tags": tags,
"selected_exercise_ids": selected_exercise_ids,
"max_date": max_date,
"min_date": min_date
}
if htmx:
return render_block(app.jinja_env, 'person_overview.html', 'content', **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"}
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", methods=['POST'])
def create_person():
name = request.form.get("name")
new_person_id = db.create_person(name)
return render_template('partials/person.html', person_id=new_person_id, name=name), 200, {"HX-Trigger": "updatedPeople"}
@ app.route("/person/<int:person_id>/delete", methods=['DELETE'])
def delete_person(person_id):
db.delete_person(person_id)
return "", 200, {"HX-Trigger": "updatedPeople"}
@ app.route("/person/<int:person_id>/edit_form", methods=['GET'])
def get_person_edit_form(person_id):
name = db.get_person_name(person_id)
return render_template('partials/person.html', person_id=person_id, name=name, is_edit=True)
@ 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 render_template('partials/person.html', person_id=person_id, name=new_name), 200, {"HX-Trigger": "updatedPeople"}
@ app.route("/person/<int:person_id>/name", methods=['GET'])
def get_person_name(person_id):
name = db.get_person_name(person_id)
return render_template('partials/person.html', person_id=person_id, name=name)
@ app.route("/exercise", methods=['POST'])
def create_exercise():
name = request.form.get("name")
new_exercise_id = db.create_exercise(name)
return render_template('partials/exercise.html', exercise_id=new_exercise_id, name=name)
@ app.route("/exercise/<int:exercise_id>", methods=['GET'])
def get_exercise(exercise_id):
exercise = db.get_exercise(exercise_id)
return render_template('partials/exercise.html', exercise_id=exercise_id, name=exercise.name)
@ app.route("/exercise/<int:exercise_id>/edit_form", methods=['GET'])
def get_exercise_edit_form(exercise_id):
exercise = db.get_exercise(exercise_id)
return render_template('partials/exercise.html', exercise_id=exercise_id, name=exercise['name'], is_edit=True)
@ app.route("/exercise/<int:exercise_id>/update", methods=['PUT'])
def update_exercise(exercise_id):
new_name = request.form.get('name')
db.update_exercise(exercise_id, new_name)
return render_template('partials/exercise.html', exercise_id=exercise_id, name=new_name)
""" @ app.route("/exercise/<int:exercise_id>/delete", methods=['DELETE'])
def delete_exercise(exercise_id):
db.delete_exercise(exercise_id)
return "" """
@ app.route("/settings")
def settings():
people = db.get_people()
exercises = db.get_all_exercises()
if htmx:
return render_block(app.jinja_env, "settings.html", "content", people=people, exercises=exercises), 200, {"HX-Trigger": "updatedPeople"}
return render_template('settings.html', people=people, exercises=exercises)
@ app.route("/tag/redirect", methods=['GET'])
def goto_tag():
person_id = request.args.get("person_id")
tag_filter = request.args.get('filter')
if person_id:
return redirect(url_for('person_overview', person_id=int(person_id)) + tag_filter)
return redirect(url_for('dashboard') + tag_filter)
@ app.route("/tag/add", methods=['GET'])
def add_tag():
person_id = request.args.get("person_id")
tag = request.args.get('tag')
tag_filter = request.args.get('filter')
if person_id:
db.add_or_update_tag_for_person(person_id, tag, tag_filter)
else:
db.add_or_update_tag_for_dashboard(tag, tag_filter)
return ""
@ app.route("/tag/<int:tag_id>/delete", methods=['GET'])
def delete_tag(tag_id):
person_id = request.args.get("person_id")
tag_filter = request.args.get("filter")
if person_id:
db.delete_tag_for_person(person_id=person_id, tag_id=tag_id)
return redirect(url_for('get_person', person_id=person_id) + tag_filter)
db.delete_tag_for_dashboard(tag_id)
return redirect(url_for('dashboard') + tag_filter)
@ app.route("/person/<int:person_id>/exercise/<int:exercise_id>/sparkline", methods=['GET'])
def get_exercise_progress_for_user(person_id, exercise_id):
min_date = convert_str_to_date(request.args.get(
'min_date'), '%Y-%m-%d')
max_date = convert_str_to_date(request.args.get(
'max_date'), '%Y-%m-%d')
epoch = request.args.get('epoch', default='All')
degree = request.args.get('degree', type=int, default=1)
if epoch == 'Custom' and (min_date is None or max_date is None):
(min_date, max_date) = db.get_exercise_earliest_and_latest_dates(person_id, exercise_id)
exercise_progress = db.get_exercise_progress_for_user(person_id, exercise_id, min_date, max_date, epoch, degree=degree)
if not exercise_progress:
abort(404)
return render_template('partials/sparkline.html', **exercise_progress)
@app.route("/stats", methods=['GET'])
def get_stats():
selected_people_ids = request.args.getlist('person_id', type=int)
min_date = request.args.get('min_date', type=convert_str_to_date)
max_date = request.args.get('max_date', type=convert_str_to_date)
selected_exercise_ids = request.args.getlist('exercise_id', type=int)
stats = db.stats.fetch_stats(selected_people_ids, min_date, max_date, selected_exercise_ids)
return render_template('partials/stats.html', stats=stats, refresh_url=request.full_path)
@app.route("/graphs", methods=['GET'])
def get_people_graphs():
selected_people_ids = request.args.getlist('person_id', type=int)
min_date = request.args.get('min_date', type=convert_str_to_date)
max_date = request.args.get('max_date', type=convert_str_to_date)
selected_exercise_ids = request.args.getlist('exercise_id', type=int)
graphs = db.people_graphs.get(selected_people_ids, min_date, max_date, selected_exercise_ids)
return render_template('partials/people_graphs.html', graphs=graphs, refresh_url=request.full_path)
@app.route("/exercises/get")
def get_exercises():
query = request.args.get('query')
person_id = request.args.get('person_id', type=int)
exercises = db.exercises.get(query)
return render_template('partials/exercise/exercise_dropdown.html', exercises=exercises, person_id=person_id)
@app.route("/exercise/<int:exercise_id>/edit_name", methods=['GET', 'POST'])
def edit_exercise_name(exercise_id):
exercise = db.exercises.get_exercise(exercise_id)
person_id = request.args.get('person_id', type=int)
if request.method == 'GET':
return render_template('partials/exercise/edit_exercise_name.html', exercise=exercise, person_id=person_id)
else:
updated_name = request.form['name']
updated_exercise = db.exercises.update_exercise_name(exercise_id, updated_name)
return render_template('partials/exercise/exercise_list_item.html', exercise=updated_exercise, person_id=person_id)
@app.route("/exercises/add", methods=['POST'])
def add_exercise():
exercise_name = request.form['query']
new_exercise = db.exercises.add_exercise(exercise_name)
person_id = request.args.get('person_id', type=int)
return render_template('partials/exercise/exercise_list_item.html', exercise=new_exercise, person_id=person_id)
@ app.route("/exercise/<int:exercise_id>/delete", methods=['DELETE'])
def delete_exercise(exercise_id):
db.exercises.delete_exercise(exercise_id)
return ""
@app.route("/sql_explorer", methods=['GET'])
def sql_explorer():
saved_queries = db.sql_explorer.list_saved_queries()
if htmx:
return render_block(app.jinja_env, 'sql_explorer.html', 'content', saved_queries=saved_queries)
return render_template('sql_explorer.html', saved_queries=saved_queries)
@app.route("/sql_query", methods=['POST'])
def sql_query():
query = request.form.get('query')
title = request.form.get('title')
error = db.sql_explorer.save_query(title, query)
saved_queries = db.sql_explorer.list_saved_queries()
return render_template('partials/sql_explorer/sql_query.html',
title=title,
query=query,
error=error,
saved_queries=saved_queries)
@app.route("/sql_query/execute", methods=['POST'])
def execute_sql_query():
query = request.form.get('query')
(results, columns, error) = db.sql_explorer.execute_sql(query)
return render_template('partials/sql_explorer/results.html',
results=results,
columns=columns,
error=error)
@app.route('/load_sql_query/<int:query_id>', methods=['GET'])
def load_sql_query(query_id):
(title, query) = db.sql_explorer.get_saved_query(query_id)
saved_queries = db.sql_explorer.list_saved_queries()
return render_template('partials/sql_explorer/sql_query.html',
title=title,
query=query,
saved_queries=saved_queries)
@app.route('/delete_sql_query/<int:query_id>', methods=['DELETE'])
def delete_sql_query(query_id):
db.sql_explorer.delete_saved_query(query_id)
saved_queries = db.sql_explorer.list_saved_queries()
return render_template('partials/sql_explorer/sql_query.html',
title="",
query="",
saved_queries=saved_queries)
@ app.route("/sql_schema", methods=['GET'])
def sql_schema():
schema_info = db.sql_explorer.get_schema_info()
mermaid_code = db.sql_explorer.generate_mermaid_er(schema_info)
create_sql = db.sql_explorer.generate_create_script(schema_info)
return render_template('partials/sql_explorer/schema.html', mermaid_code=mermaid_code, create_sql=create_sql)
@app.route("/plot/<int:query_id>", methods=['GET'])
def plot_query(query_id):
(title, query) = db.sql_explorer.get_saved_query(query_id)
#(results, columns, error) = db.sql_explorer.execute_sql(query)
results_df = db.read_sql_as_df(query)
plot_div = generate_plot(results_df, title)
return plot_div
@app.route("/plot/show", methods=['POST'])
def plot_unsaved_query(): # Rename?
query = request.form.get('query')
title = request.form.get('title')
results_df = db.read_sql_as_df(query)
plot_div = generate_plot(results_df, title)
return plot_div
def get_routes():
routes = []
for rule in app.url_map.iter_rules():
if rule.endpoint == 'static':
continue
methods = ', '.join(sorted(rule.methods - {'HEAD', 'OPTIONS'}))
route_info = {
'endpoint': rule.endpoint,
'url': rule.rule,
'methods': methods,
'view_func': app.view_functions[rule.endpoint].__name__,
'doc': app.view_functions[rule.endpoint].__doc__
}
routes.append(route_info)
return routes
@app.route('/endpoints')
def list_endpoints():
"""
Lists all API endpoints available in the Flask application.
This endpoint retrieves all registered routes, excluding static routes,
and displays their details such as endpoint name, URL, allowed HTTP methods,
view function name, and a brief description.
"""
routes = get_routes()
if htmx:
return render_block(app.jinja_env, 'endpoints.html', 'content', routes=routes)
return render_template('endpoints.html', routes=routes)
@app.route('/endpoints/search')
def search_endpoints():
routes = get_routes()
search = request.args.get('search', '').lower()
if search:
routes = [
route for route in routes
if search in route['endpoint'].lower()
or search in route['url'].lower()
or search in route['methods'].lower()
or search in route['view_func'].lower()
or (route['doc'] and search in route['doc'].lower())
]
return render_template('partials/endpoints_table.html', routes=routes)
@app.teardown_appcontext
def closeConnection(exception):
db.close_connection()
@app.template_filter('strftime')
def strftime(date, format="%b %d %Y"):
return date.strftime(format)
@ app.context_processor
def my_utility_processor():
def is_selected_page(url):
# if htmx:
# parsed_url = urlparse(htmx.current_url)
# return 'bg-gray-200' if url == parsed_url.path else ''
if url == request.path:
return 'bg-gray-200'
return ''
def strftime(date, format="%b %d %Y"):
return date.strftime(format)
return dict(is_selected_page=is_selected_page, strftime=strftime)
if __name__ == '__main__':
# Bind to PORT if defined, otherwise default to 5000.
port = int(os.environ.get('PORT', 5000))
app.run(host='127.0.0.1', port=port)