93 lines
3.5 KiB
Python
93 lines
3.5 KiB
Python
from flask import Blueprint, render_template
|
|
from app.auth import login_required
|
|
from app.db import query, query_one
|
|
from app.config import SYDNEY_TZ
|
|
from app.utils import calculate_streaks_bulk, calculate_weight_change
|
|
from datetime import timezone
|
|
|
|
bp = Blueprint("leaderboard", __name__)
|
|
|
|
|
|
@bp.route("/leaderboard")
|
|
@login_required
|
|
def index():
|
|
# Get all users with weight stats using window functions (no correlated subqueries)
|
|
users = query("""
|
|
WITH user_weights AS (
|
|
SELECT
|
|
user_id,
|
|
FIRST_VALUE(weight_kg) OVER (PARTITION BY user_id ORDER BY checked_in_at ASC) AS first_weight,
|
|
FIRST_VALUE(weight_kg) OVER (PARTITION BY user_id ORDER BY checked_in_at DESC) AS current_weight,
|
|
COUNT(*) OVER (PARTITION BY user_id) AS total_checkins,
|
|
MAX(checked_in_at) OVER (PARTITION BY user_id) AS last_checkin,
|
|
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY checked_in_at DESC) AS rn
|
|
FROM checkins
|
|
)
|
|
SELECT
|
|
u.id,
|
|
u.display_name,
|
|
u.username,
|
|
u.starting_weight_kg,
|
|
u.goal_weight_kg,
|
|
uw.first_weight,
|
|
uw.current_weight,
|
|
uw.total_checkins,
|
|
uw.last_checkin
|
|
FROM users u
|
|
JOIN user_weights uw ON uw.user_id = u.id AND uw.rn = 1
|
|
WHERE u.is_private = FALSE
|
|
ORDER BY u.created_at
|
|
""")
|
|
|
|
# Batch-compute streaks for all users in one query
|
|
user_ids = [u["id"] for u in users]
|
|
all_streaks = calculate_streaks_bulk(user_ids)
|
|
|
|
# Calculate rankings
|
|
ranked = []
|
|
for u in users:
|
|
start_w = float(u["starting_weight_kg"] or u["first_weight"] or 0)
|
|
current_w = float(u["current_weight"] or start_w)
|
|
weight_lost, pct_lost = calculate_weight_change(start_w, current_w)
|
|
|
|
goal = float(u["goal_weight_kg"]) if u["goal_weight_kg"] else None
|
|
goal_progress = None
|
|
if goal and start_w > goal:
|
|
total_to_lose = start_w - goal
|
|
goal_progress = min(100, round((weight_lost / total_to_lose) * 100, 1)) if total_to_lose > 0 else 0
|
|
|
|
streak = all_streaks.get(u["id"], {"current": 0, "best": 0})
|
|
ranked.append({
|
|
**u,
|
|
"weight_lost": weight_lost,
|
|
"pct_lost": pct_lost,
|
|
"goal_progress": goal_progress,
|
|
"streak": streak["current"],
|
|
})
|
|
|
|
# Sort by % lost (descending)
|
|
ranked.sort(key=lambda x: x["pct_lost"], reverse=True)
|
|
|
|
# Get earliest and latest check-in dates for date pickers
|
|
date_range = query_one("""
|
|
SELECT
|
|
MIN(c.checked_in_at) AS earliest,
|
|
MAX(c.checked_in_at) AS latest
|
|
FROM checkins c
|
|
JOIN users u ON u.id = c.user_id
|
|
WHERE u.is_private = FALSE
|
|
""")
|
|
earliest = ""
|
|
latest = ""
|
|
if date_range and date_range["earliest"]:
|
|
e = date_range["earliest"]
|
|
l = date_range["latest"]
|
|
if e.tzinfo is None:
|
|
e = e.replace(tzinfo=timezone.utc)
|
|
if l.tzinfo is None:
|
|
l = l.replace(tzinfo=timezone.utc)
|
|
earliest = e.astimezone(SYDNEY_TZ).strftime("%Y-%m-%d")
|
|
latest = l.astimezone(SYDNEY_TZ).strftime("%Y-%m-%d")
|
|
|
|
return render_template("leaderboard.html", ranked=ranked, earliest=earliest, latest=latest)
|