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//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//delete", methods=['DELETE']) def delete_person(person_id): db.delete_person(person_id) return "", 200, {"HX-Trigger": "updatedPeople"} @ app.route("/person//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//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//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/", 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//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//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//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//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//exercise//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//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//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/', 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/', 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/", 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)