Files
ShooterHub/app.py

167 lines
5.9 KiB
Python
Raw Normal View History

2026-03-16 16:09:19 +01:00
import base64
import io
2026-03-16 16:09:19 +01:00
from flask import Flask, request, render_template
from flask_login import current_user
from sqlalchemy import select
2026-03-16 16:09:19 +01:00
from config import Config
from extensions import db, jwt, login_manager, migrate, oauth
2026-03-16 16:09:19 +01:00
def create_app(config_class=Config):
app = Flask(__name__)
app.config.from_object(config_class)
2026-03-16 16:09:19 +01:00
db.init_app(app)
migrate.init_app(app, db)
login_manager.init_app(app)
jwt.init_app(app)
2026-03-16 16:09:19 +01:00
@jwt.unauthorized_loader
def unauthorized_callback(reason):
from flask import jsonify
return jsonify({"error": {"code": "UNAUTHORIZED", "message": reason}}), 401
2026-03-16 16:09:19 +01:00
@jwt.expired_token_loader
def expired_callback(jwt_header, jwt_payload):
from flask import jsonify
return jsonify({"error": {"code": "TOKEN_EXPIRED", "message": "Token has expired"}}), 401
2026-03-16 16:09:19 +01:00
@jwt.invalid_token_loader
def invalid_callback(reason):
from flask import jsonify
return jsonify({"error": {"code": "INVALID_TOKEN", "message": reason}}), 422
2026-03-16 16:09:19 +01:00
oauth.init_app(app)
oauth.register(
name="google",
client_id=app.config["GOOGLE_CLIENT_ID"],
client_secret=app.config["GOOGLE_CLIENT_SECRET"],
server_metadata_url="https://accounts.google.com/.well-known/openid-configuration",
client_kwargs={"scope": "openid email profile"},
)
oauth.register(
name="github",
client_id=app.config["GITHUB_CLIENT_ID"],
client_secret=app.config["GITHUB_CLIENT_SECRET"],
access_token_url="https://github.com/login/oauth/access_token",
authorize_url="https://github.com/login/oauth/authorize",
api_base_url="https://api.github.com/",
client_kwargs={"scope": "user:email"},
2026-03-16 16:09:19 +01:00
)
# Must import models after db is initialised so Alembic can detect them
from models import User # noqa: F401
@login_manager.user_loader
def load_user(user_id):
return db.session.get(User, int(user_id))
from blueprints.auth import auth_bp
from blueprints.dashboard import dashboard_bp
from blueprints.equipment import equipment_bp
from blueprints.sessions import sessions_bp
from blueprints.analyses import analyses_bp
app.register_blueprint(auth_bp)
app.register_blueprint(dashboard_bp)
app.register_blueprint(equipment_bp)
app.register_blueprint(sessions_bp)
app.register_blueprint(analyses_bp)
from blueprints.api import api as api_bp
app.register_blueprint(api_bp)
@app.route("/u/<int:user_id>")
def public_profile(user_id: int):
from models import User, ShootingSession, EquipmentItem
from flask import abort, render_template
user = db.session.get(User, user_id)
if user is None:
abort(404)
public_sessions = db.session.scalars(
db.select(ShootingSession)
.filter_by(user_id=user.id, is_public=True)
.order_by(ShootingSession.session_date.desc())
).all()
equipment = None
if user.show_equipment_public:
equipment = db.session.scalars(
db.select(EquipmentItem)
.filter_by(user_id=user.id)
.order_by(EquipmentItem.category, EquipmentItem.name)
).all()
return render_template("auth/public_profile.html",
profile_user=user,
public_sessions=public_sessions,
equipment=equipment)
@app.route("/")
def index():
from models import ShootingSession
public_sessions = db.session.scalars(
select(ShootingSession)
.where(ShootingSession.is_public == True) # noqa: E712
.order_by(ShootingSession.session_date.desc(), ShootingSession.created_at.desc())
.limit(12)
).all()
return render_template("index.html", public_sessions=public_sessions)
@app.route("/analyze", methods=["GET", "POST"])
def analyze():
from analyzer.parser import parse_csv
from analyzer.grouper import detect_groups
from analyzer.stats import compute_overall_stats, compute_group_stats
from analyzer.charts import render_group_charts, render_overview_chart
from analyzer.pdf_report import generate_pdf
if request.method == "GET":
return render_template("upload.html")
if "csv_file" not in request.files or request.files["csv_file"].filename == "":
return render_template("upload.html", error="No file selected.")
file = request.files["csv_file"]
try:
csv_bytes = file.read()
df = parse_csv(io.BytesIO(csv_bytes))
groups = detect_groups(df)
overall = compute_overall_stats(df)
group_stats = compute_group_stats(groups)
charts = render_group_charts(
groups,
y_min=overall["min_speed"],
y_max=overall["max_speed"],
)
overview_chart = render_overview_chart(group_stats)
except ValueError as e:
return render_template("upload.html", error=str(e))
pdf_bytes = generate_pdf(overall, group_stats, charts, overview_chart)
pdf_b64 = base64.b64encode(pdf_bytes).decode("utf-8")
saved_analysis_id = None
if current_user.is_authenticated:
from storage import save_analysis
saved_analysis_id = save_analysis(
user=current_user,
csv_bytes=csv_bytes,
pdf_bytes=pdf_bytes,
overall=overall,
group_stats=group_stats,
filename=file.filename or "upload.csv",
)
groups_display = list(zip(group_stats, charts))
return render_template(
"results.html",
overall=overall,
groups_display=groups_display,
overview_chart=overview_chart,
pdf_b64=pdf_b64,
saved_analysis_id=saved_analysis_id,
)
2026-03-16 16:09:19 +01:00
return app