from datetime import datetime, date, timedelta from dateutil.relativedelta import relativedelta import os from flask import Flask, abort, render_template, redirect, request, url_for from jinja2 import Environment, FileSystemLoader, select_autoescape import jinja_partials from jinja2_fragments import render_block from decorators import validate_person, validate_topset, validate_workout from db import DataBase from utils import count_prs_over_time, get_date_info, get_people_and_exercise_rep_maxes, convert_str_to_date, get_earliest_and_latest_workout_date, filter_workout_topsets, first_and_last_visible_days_in_month, get_weekly_pr_graph_model, get_workout_counts from flask_htmx import HTMX import minify_html from urllib.parse import quote app = Flask(__name__) app.config.from_pyfile('config.py') jinja_partials.register_extensions(app) db = DataBase(app) htmx = HTMX(app) @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(): all_topsets = db.get_all_topsets() exercises = db.get_all_exercises() people = db.get_people() tags = db.get_tags_for_dashboard() selected_person_ids = [int(i) for i in request.args.getlist('person_id')] if not selected_person_ids and htmx.trigger_name != 'person_id': selected_person_ids = [p['PersonId'] for p in people] selected_exercise_ids = [int(i) for i in request.args.getlist('exercise_id')] if not selected_exercise_ids and htmx.trigger_name != 'exercise_id': selected_exercise_ids = [e['exercise_id'] for e in exercises] min_date = convert_str_to_date(request.args.get( 'min_date'), '%Y-%m-%d') or min([t['StartDate'] for t in all_topsets]) max_date = convert_str_to_date(request.args.get( 'max_date'), '%Y-%m-%d') or max([t['StartDate'] for t in all_topsets]) people_and_exercise_rep_maxes = get_people_and_exercise_rep_maxes( all_topsets, selected_person_ids, selected_exercise_ids, min_date, max_date) weekly_counts = get_workout_counts(all_topsets, 'week') weekly_pr_counts = count_prs_over_time(all_topsets, 'week') dashboard_graphs = [get_weekly_pr_graph_model('Workouts per week', weekly_counts), get_weekly_pr_graph_model('PRs per week', weekly_pr_counts)] if htmx: return render_block(app.jinja_env, 'dashboard.html', 'content', model=people_and_exercise_rep_maxes, people=people, exercises=exercises, min_date=min_date, max_date=max_date, selected_person_ids=selected_person_ids, selected_exercise_ids=selected_exercise_ids, tags=tags, dashboard_graphs=dashboard_graphs) return render_template('dashboard.html', model=people_and_exercise_rep_maxes, people=people, exercises=exercises, min_date=min_date, max_date=max_date, selected_person_ids=selected_person_ids, selected_exercise_ids=selected_exercise_ids, tags=tags, dashboard_graphs=dashboard_graphs) @ 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/list", methods=['GET']) @ validate_person def get_person(person_id): person = db.get_person(person_id) tags = db.get_tags_for_person(person_id) (min_date, max_date) = get_earliest_and_latest_workout_date(person) min_date = request.args.get( 'min_date', default=min_date, type=convert_str_to_date) max_date = request.args.get( 'max_date', default=max_date, type=convert_str_to_date) selected_exercise_ids = request.args.getlist('exercise_id', type=int) all_exercise_ids_for_person = [e['ExerciseId'] for e in person['Exercises']] if not selected_exercise_ids and htmx.trigger_name != 'exercise_id': selected_exercise_ids = all_exercise_ids_for_person person['Workouts'] = [filter_workout_topsets(workout, selected_exercise_ids) for workout in person['Workouts'] if workout['StartDate'] <= max_date and workout['StartDate'] >= min_date] # Filter out workouts that dont contain any of the selected exercises person['Workouts'] = [workout for workout in person['Workouts'] if workout['TopSets']] filtered_exercises = filter( lambda e: e['ExerciseId'] in selected_exercise_ids, person['Exercises']) person['FilteredExercises'] = list(filtered_exercises) person['ExerciseProgressGraphs'] = list(filter(lambda e: e['ExerciseId'] in selected_exercise_ids, person['ExerciseProgressGraphs'])) if htmx: return render_block(app.jinja_env, 'person.html', 'content', person=person, selected_exercise_ids=selected_exercise_ids, max_date=max_date, min_date=min_date, tags=tags), 200, {"HX-Trigger": "updatedPeople"} return render_template('person.html', person=person, selected_exercise_ids=selected_exercise_ids, max_date=max_date, min_date=min_date, tags=tags), 200, {"HX-Trigger": "updatedPeople"} @ app.route("/person//calendar") @ validate_person def get_calendar(person_id): person = db.get_person(person_id) selected_date = convert_str_to_date(request.args.get( 'date'), '%Y-%m-%d') or date.today() selected_view = request.args.get('view') or 'month' if selected_view == 'all': return redirect(url_for('get_person', person_id=person_id)) # selected_view = month | year | all date_info = get_date_info(selected_date, selected_view) if htmx: return render_block(app.jinja_env, 'calendar.html', 'content', person=person, selected_date=selected_date, selected_view=selected_view, **date_info, datetime=datetime, timedelta=timedelta, relativedelta=relativedelta, first_and_last_visible_days_in_month=first_and_last_visible_days_in_month) return render_template('calendar.html', person=person, selected_date=selected_date, selected_view=selected_view, **date_info, datetime=datetime, timedelta=timedelta, relativedelta=relativedelta, first_and_last_visible_days_in_month=first_and_last_visible_days_in_month) @ app.route("/person//workout//modal", methods=['GET']) @ validate_workout def get_workout_modal(person_id, workout_id): workout = db.get_workout(person_id, workout_id) (person_tags, workout_tags, selected_workout_tag_ids) = db.get_workout_tags( person_id, workout_id) exercises = db.get_all_exercises() return render_template('partials/workout_modal.html', **workout, person_tags=person_tags, workout_tags=workout_tags, selected_workout_tag_ids=selected_workout_tag_ids, exercises=exercises) @ app.route("/person//workout", methods=['POST']) @ validate_person def create_workout(person_id): new_workout_id = db.create_workout(person_id) workout = db.get_workout(person_id, new_workout_id) (person_tags, workout_tags, selected_workout_tag_ids) = db.get_workout_tags( person_id, new_workout_id) exercises = db.get_all_exercises() return render_template('partials/workout_modal.html', **workout, person_tags=person_tags, workout_tags=workout_tags, selected_workout_tag_ids=selected_workout_tag_ids, exercises=exercises), 200, {"HX-Trigger": "updatedPeople"} @ app.route("/person//workout//delete", methods=['DELETE']) @ validate_workout def delete_workout(person_id, workout_id): db.delete_workout(workout_id) return "", 200, {"HX-Trigger": "updatedPeople"} @ app.route("/person//workout//start_date_edit_form", methods=['GET']) @ validate_workout def get_workout_start_date_edit_form(person_id, workout_id): workout = db.get_workout(person_id, workout_id) return render_template('partials/start_date.html', person_id=person_id, workout_id=workout_id, start_date=workout['start_date'], is_edit=True) @ app.route("/person//workout//start_date", methods=['PUT']) @ validate_workout def update_workout_start_date(person_id, workout_id): new_start_date = request.form.get('start-date') db.update_workout_start_date(workout_id, new_start_date) return render_template('partials/start_date.html', person_id=person_id, workout_id=workout_id, start_date=convert_str_to_date(new_start_date, '%Y-%m-%d')) @ app.route("/person//workout//start_date", methods=['GET']) @ validate_workout def get_workout_start_date(person_id, workout_id): workout = db.get_workout(person_id, workout_id) return render_template('partials/start_date.html', person_id=person_id, workout_id=workout_id, start_date=workout['start_date']) @ app.route("/person//workout//topset/", methods=['GET']) @ validate_topset def get_topset(person_id, workout_id, topset_id): topset = db.get_topset(topset_id) return render_template('partials/topset.html', person_id=person_id, workout_id=workout_id, topset_id=topset_id, exercise_id=topset['exercise_id'], exercise_name=topset['exercise_name'], repetitions=topset['repetitions'], weight=topset['weight']) @ app.route("/person//workout//topset//edit_form", methods=['GET']) @ validate_topset def get_topset_edit_form(person_id, workout_id, topset_id): exercises = db.get_all_exercises() topset = db.get_topset(topset_id) return render_template('partials/topset.html', person_id=person_id, workout_id=workout_id, topset_id=topset_id, exercises=exercises, exercise_id=topset['exercise_id'], exercise_name=topset['exercise_name'], repetitions=topset['repetitions'], weight=topset['weight'], is_edit=True) @ app.route("/person//workout//topset", methods=['POST']) @ validate_workout def create_topset(person_id, workout_id): exercise_id = request.form.get("exercise_id") repetitions = request.form.get("repetitions") weight = request.form.get("weight") new_topset_id = db.create_topset( workout_id, exercise_id, repetitions, weight) exercise = db.get_exercise(exercise_id) return render_template('partials/topset.html', person_id=person_id, workout_id=workout_id, topset_id=new_topset_id, exercise_id=exercise_id, exercise_name=exercise['name'], repetitions=repetitions, weight=weight), 200, {"HX-Trigger": "topsetAdded"} @ app.route("/person//workout//topset/", methods=['PUT']) @ validate_workout def update_topset(person_id, workout_id, topset_id): exercise_id = request.form.get("exercise_id") repetitions = request.form.get("repetitions") weight = request.form.get("weight") db.update_topset(exercise_id, repetitions, weight, topset_id) exercise = db.get_exercise(exercise_id) return render_template('partials/topset.html', person_id=person_id, workout_id=workout_id, topset_id=topset_id, exercise_name=exercise['name'], repetitions=repetitions, weight=weight) @ app.route("/person//workout//topset//delete", methods=['DELETE']) @ validate_topset def delete_topset(person_id, workout_id, topset_id): db.delete_topset(topset_id) return "" @ 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): person = db.get_person(person_id) return render_template('partials/person.html', person_id=person_id, name=person['PersonName'], 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): person = db.get_person(person_id) return render_template('partials/person.html', person_id=person_id, name=person['PersonName']) @ 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('get_person', 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//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']) @ app.route("/person//workout//tag/add", methods=['POST']) def add_tag_to_workout(person_id, workout_id): tags_id = [int(i) for i in request.form.getlist('tag_id')] workout_tags = db.add_tag_for_workout(workout_id, tags_id) return render_template('partials/workout_tags_list.html', workout_tags=workout_tags) @ app.route("/person//workout//tag/new", methods=['POST']) def create_new_tag_for_workout(person_id, workout_id): tag_name = request.form.get('tag_name') workout_tags = db.create_tag_for_workout(person_id, workout_id, tag_name) return render_template('partials/workout_tags_list.html', workout_tags=workout_tags) @ app.route("/person//workout//exercise/most_recent_topset_for_exercise", methods=['GET']) def get_most_recent_topset_for_exercise(person_id, workout_id): exercise_id = request.args.get('exercise_id', type=int) exercises = db.get_all_exercises() if not exercise_id: return render_template('partials/new_set_form.html', person_id=person_id, workout_id=workout_id, exercises=exercises, has_value=False) topset = db.get_most_recent_topset_for_exercise(person_id, exercise_id) if not topset: return render_template('partials/new_set_form.html', person_id=person_id, workout_id=workout_id, exercises=exercises, exercise_id=exercise_id, has_value=False) (repetitions, weight) = topset return render_template('partials/new_set_form.html', person_id=person_id, workout_id=workout_id, exercises=exercises, has_value=True, exercise_id=exercise_id, repetitions=repetitions, weight=weight) def calculate_relative_positions(start_dates): min_date = min(start_dates) max_date = max(start_dates) total_span = (max_date - min_date).days if max_date != min_date else 1 return [(date - min_date).days / total_span for date in start_dates] @ 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') exercise_progress = db.get_exercise_progress_for_user(person_id, exercise_id, min_date, max_date) if not exercise_progress: abort(404) return render_template('partials/sparkline.html', **exercise_progress) @app.teardown_appcontext def closeConnection(exception): db.close_connection() @app.template_filter('list_to_string') def list_to_string(list): return [str(i) for i in list] @app.template_filter('strftime') def strftime(date, format="%b %d %Y"): return date.strftime(format) @app.template_filter('get_first_element_from_list_with_matching_attribute') def get_first_element_from_list_with_matching_attribute(list, attribute, value): if not list: return None for element in list: if element[attribute] == value: return element return None @app.template_filter('replace_double_quote_strings_with_single_quote') def replace_double_quote_strings_with_single_quote(str): return str.replace('"', "'") @ 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 get_list_of_people_and_workout_count(): person_id = request.view_args.get('person_id') return db.get_people_and_workout_count(person_id) def get_first_element_from_list_with_matching_attribute(list, attribute, value): if not list: return None for element in list: if element[attribute] == value: return element return None def in_list(val, checked_vals, attr='checked'): if not checked_vals: return attr return attr if val in checked_vals else '' def strftime(date, format="%b %d %Y"): return date.strftime(format) def list_to_string(list): return [str(i) for i in list] return dict(get_list_of_people_and_workout_count=get_list_of_people_and_workout_count, is_selected_page=is_selected_page, get_first_element_from_list_with_matching_attribute=get_first_element_from_list_with_matching_attribute, in_list=in_list, strftime=strftime, datetime=datetime, timedelta=timedelta, relativedelta=relativedelta, first_and_last_visible_days_in_month=first_and_last_visible_days_in_month, list_to_string=list_to_string, quote=quote) 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)