Files
workout/app.py
Peter Stockings a8fe28339b I have refactored the SQL Explorer functionality into its own blueprint (routes/sql_explorer.py) with a /sql URL prefix. This involved moving the relevant routes from app.py, registering the new blueprint, removing the old routes, updating url_for calls in the templates, and documenting the change in the changelog.
Here is a conventional commit message summarizing the changes:

```
feat: Refactor SQL Explorer into blueprint

- Moved SQL Explorer routes (view explorer, save/load/execute/delete queries, view schema, plot queries) from `app.py` into a new blueprint at `routes/sql_explorer.py`.
- Added `/sql` URL prefix to the blueprint.
- Registered the new `sql_explorer_bp` blueprint in `app.py`.
- Removed the original SQL Explorer route definitions from `app.py`.
- Updated `url_for` calls in relevant templates (`sql_explorer.html`, `partials/sql_explorer/sql_query.html`, `base.html`) to reference the new blueprint endpoints (e.g., `sql_explorer.sql_explorer`).
- Updated `templates/changelog/changelog.html` to document this refactoring.
```
2025-03-31 23:00:54 +11:00

387 lines
15 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 routes.sql_explorer import sql_explorer_bp # Import the new SQL explorer 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.register_blueprint(sql_explorer_bp) # Register the SQL explorer blueprint (prefix defined in blueprint file)
@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 ""
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)