Files
boker/utils.py
T
SowinskiBraeden 2fc7bab183 feat: per-league eligible sessions and break-even threshold settings (2.5.0)
Add two configurable leaderboard settings per league, replacing the global
constants with per-row DB columns:

- eligible_min_sessions (default 3): minimum sessions before a player ranks
  on the main leaderboard rather than showing as provisional
- break_even_cents (default 100 = $1.00): sessions within ±this of $0 net
  are classified as break-even rather than a win or loss

Changes:
- Migration 0003 adds both columns with server defaults for existing leagues
- League DB model gains eligible_min_sessions and break_even_cents columns
- net_result_bucket() and build_leaderboard() accept optional tolerance param
- summarize_player_runs() passes tolerance to net_result_bucket for streaks
- League leaderboard route reads values from the league row instead of config
- League settings form exposes both fields with validation (1-100 sessions,
  $0.00-$100.00 threshold)
2026-06-26 21:37:47 -07:00

91 lines
2.6 KiB
Python

#!/usr/bin/env python3
"""Formatting, sorting, and classification helpers shared across the app."""
from __future__ import annotations
from datetime import datetime
import re
from markupsafe import Markup
from models import SessionEntry, SessionSummary
BREAK_EVEN_TOLERANCE_CENTS = 100
def slugify(value: str, fallback: str = "league") -> str:
slug = re.sub(r"[^a-z0-9]+", "-", value.strip().casefold()).strip("-")
return slug or fallback
def cents_to_dollars(cents: int) -> Markup:
value = cents / 100
return Markup(f'<span class="currency-symbol">$</span>{value:,.2f}')
def safe_date_label(raw_date: str) -> str:
try:
return datetime.strptime(raw_date, "%Y-%m-%d").strftime("%b %d, %Y")
except ValueError:
return raw_date
def session_sequence_number(session_id: str, session_date: str) -> int:
suffix = session_id.strip().replace(f"{session_date.strip()}-", "", 1)
if session_id.strip() == session_date.strip():
return 1
if suffix.isdigit():
return int(suffix)
if suffix.lower().startswith("s") and suffix[1:].isdigit():
return int(suffix[1:])
return 9999
def session_display_suffix(session_id: str, session_date: str) -> str:
suffix = session_id.strip().replace(f"{session_date.strip()}-", "", 1)
if session_id.strip() == session_date.strip():
return ""
if suffix.isdigit():
return f"S{int(suffix)}"
if suffix.lower().startswith("s") and suffix[1:].isdigit():
return f"S{int(suffix[1:])}"
return suffix
def session_chart_label(session: SessionSummary) -> str:
suffix = session_display_suffix(session.session_id, session.session_date)
date_label = safe_date_label(session.session_date)
return f"{date_label} · {suffix}" if suffix else date_label
def session_label(session: SessionSummary) -> str:
return session_chart_label(session)
def session_sort_key(session: SessionSummary) -> tuple[str, int, str, str]:
session_id = session.session_id.strip()
session_date = session.session_date.strip()
return (
session_date,
session_sequence_number(session_id, session_date),
session.opened_at,
session_id,
)
def entry_sort_key(entry: SessionEntry) -> tuple[str, int, str]:
return (
entry.session_date,
session_sequence_number(entry.session_id, entry.session_date),
entry.session_id,
)
def net_result_bucket(value_cents: int, tolerance_cents: int = BREAK_EVEN_TOLERANCE_CENTS) -> str:
if value_cents > tolerance_cents:
return "win"
if value_cents < -tolerance_cents:
return "loss"
return "even"