306 lines
12 KiB
Python
306 lines
12 KiB
Python
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()
|
|
|
|
from datetime import date
|
|
from flask import Flask, abort, render_template, redirect, request, url_for
|
|
from flask_login import LoginManager, login_required, current_user
|
|
import jinja_partials
|
|
from jinja2_fragments import render_block
|
|
from decorators import (validate_person, validate_topset, validate_workout,
|
|
require_ownership, get_auth_message, get_person_id_from_context, admin_required)
|
|
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 routes.endpoints import endpoints_bp # Import the new endpoints blueprint
|
|
from routes.export import export_bp # Import the new export blueprint
|
|
from routes.tags import tags_bp # Import the new tags blueprint
|
|
from routes.programs import programs_bp # Import the new programs blueprint
|
|
from routes.exercises import exercises_bp # Import the new exercises blueprint
|
|
from routes.settings import settings_bp # Import the new settings blueprint
|
|
from extensions import db
|
|
from utils import convert_str_to_date
|
|
from flask_htmx import HTMX
|
|
import minify_html
|
|
from flask_compress import Compress
|
|
from flask_caching import Cache
|
|
|
|
app = Flask(__name__)
|
|
app.config['COMPRESS_REGISTER'] = True
|
|
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 31536000 # 1 year
|
|
app.config['CACHE_TYPE'] = 'SimpleCache'
|
|
app.config['CACHE_DEFAULT_TIMEOUT'] = 300 # 5 minutes
|
|
|
|
Compress(app)
|
|
cache = Cache(app)
|
|
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)
|
|
|
|
@login_manager.unauthorized_handler
|
|
def unauthorized():
|
|
from flask import flash
|
|
person_id = get_person_id_from_context()
|
|
msg = get_auth_message(request.endpoint, person_id)
|
|
flash(msg, "info")
|
|
|
|
if request.headers.get('HX-Request'):
|
|
return '', 200, {'HX-Redirect': url_for('auth.login')}
|
|
return redirect(url_for('auth.login'))
|
|
|
|
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.register_blueprint(endpoints_bp) # Register the endpoints blueprint (prefix defined in blueprint file)
|
|
app.register_blueprint(export_bp) # Register the export blueprint (prefix defined in blueprint file)
|
|
app.register_blueprint(tags_bp) # Register the tags blueprint (prefix defined in blueprint file)
|
|
app.register_blueprint(programs_bp) # Register the programs blueprint (prefix defined in blueprint file)
|
|
app.register_blueprint(exercises_bp) # Register the exercises blueprint
|
|
app.register_blueprint(settings_bp) # Register the settings 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)
|
|
|
|
limit = request.args.get('limit', type=int, default=20)
|
|
offset = request.args.get('offset', type=int, default=0)
|
|
|
|
person = db.person_overview.get(person_id, min_date, max_date, selected_exercise_ids, limit=limit, offset=offset)
|
|
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,
|
|
"limit": limit,
|
|
"offset": offset,
|
|
"next_offset": offset + limit
|
|
}
|
|
|
|
if htmx:
|
|
if htmx.target == 'load-more-row':
|
|
return render_template('partials/workout_rows.html', **render_args)
|
|
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'])
|
|
@login_required
|
|
def create_person():
|
|
name = request.form.get("name")
|
|
new_person_id = db.create_person(name)
|
|
db.activityRequest.log(current_user.id, 'CREATE_PERSON', 'person', new_person_id, f"Created 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'])
|
|
@login_required
|
|
@admin_required
|
|
@validate_person
|
|
def delete_person(person_id):
|
|
name = db.get_person_name(person_id)
|
|
db.delete_person(person_id)
|
|
db.activityRequest.log(current_user.id, 'DELETE_PERSON', 'person', person_id, f"Deleted person: {name}")
|
|
return "", 200, {"HX-Trigger": "updatedPeople"}
|
|
|
|
|
|
@ app.route("/person/<int:person_id>/edit_form", methods=['GET'])
|
|
@login_required
|
|
@validate_person
|
|
@require_ownership
|
|
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'])
|
|
@login_required
|
|
@validate_person
|
|
@require_ownership
|
|
def update_person_name(person_id):
|
|
new_name = request.form.get("name")
|
|
old_name = db.get_person_name(person_id)
|
|
db.update_person_name(person_id, new_name)
|
|
db.activityRequest.log(current_user.id, 'UPDATE_PERSON_NAME', 'person', person_id, f"Updated name for {old_name} to {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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Routes moved to routes/tags.py blueprint
|
|
|
|
@ 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'])
|
|
@cache.cached(timeout=300, query_string=True)
|
|
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'])
|
|
@cache.cached(timeout=300, query_string=True)
|
|
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.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)
|