diff options
| author | Anson Bridges <bridges.anson@gmail.com> | 2026-04-03 16:02:56 -0700 |
|---|---|---|
| committer | Anson Bridges <bridges.anson@gmail.com> | 2026-04-03 16:02:56 -0700 |
| commit | 5249744b01849b7158ff9cf796c550924f452320 (patch) | |
| tree | 1404319acfef55d9a99c56792922515875f9b06e /app.py | |
start er up
Diffstat (limited to 'app.py')
| -rw-r--r-- | app.py | 956 |
1 files changed, 956 insertions, 0 deletions
@@ -0,0 +1,956 @@ +import os +import json +import itertools +from flask import Flask, render_template, request, redirect, url_for, flash +from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user +from werkzeug.security import generate_password_hash, check_password_hash +from werkzeug.utils import secure_filename +import sqlite3 +from datetime import datetime, timedelta + +app = Flask(__name__) +app.secret_key = 'super_secret_orchard_key' +app.config['UPLOAD_FOLDER'] = 'static/uploads' + +os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) + +login_manager = LoginManager() +login_manager.init_app(app) +login_manager.login_view = 'login' + +DB_FILE = 'orchard_league.db' + +def get_db(): + conn = sqlite3.connect(DB_FILE) + conn.row_factory = sqlite3.Row + return conn + +class User(UserMixin): + def __init__(self, id, username, is_admin, team_name, team_icon): + self.id = str(id) + self.username = username + self.is_admin = bool(is_admin) + self.team_name = team_name + self.team_icon = team_icon + +@login_manager.user_loader +def load_user(user_id): + conn = get_db() + c = conn.cursor() + c.execute('SELECT * FROM Users WHERE id = ?', (user_id,)) + user_row = c.fetchone() + conn.close() + if user_row: + return User(user_row['id'], user_row['username'], user_row['is_admin'], user_row['team_name'], user_row['team_icon']) + return None + +@app.route('/') +def index(): + season_id = request.args.get('season_id') + team_id = request.args.get('team_id') + tab = request.args.get('tab', 'overview') + + conn = get_db() + c = conn.cursor() + + # Get all seasons for the dropdown + c.execute('SELECT id, name FROM Seasons ORDER BY id DESC') + seasons = c.fetchall() + + if not seasons: + conn.close() + return render_template('season.html', seasons=[], selected_season_id=None) + + if not season_id: + season_id = seasons[0]['id'] + + # Season Overview Data + c.execute('SELECT * FROM Seasons WHERE id = ?', (season_id,)) + season_info = dict(c.fetchone()) + + # Season Runtime (Start/End dates) + c.execute('SELECT MIN(scheduled_date), MAX(scheduled_date) FROM Games WHERE season_id = ?', (season_id,)) + runtime = c.fetchone() + season_info['start_date'] = runtime[0] if runtime[0] else "N/A" + season_info['end_date'] = runtime[1] if runtime[1] else "N/A" + + # Final Placement (if all games are Final and games exist) + c.execute("SELECT COUNT(*) as c FROM Games WHERE season_id = ?", (season_id,)) + has_games = c.fetchone()['c'] > 0 + c.execute("SELECT COUNT(*) as c FROM Games WHERE season_id = ? AND status != 'Final'", (season_id,)) + all_final = c.fetchone()['c'] == 0 + is_finished = has_games and all_final + season_info['is_finished'] = is_finished + + # Join Status for current user + user_season_status = None + if current_user.is_authenticated: + c.execute('SELECT status FROM SeasonTeams WHERE season_id = ? AND user_id = ?', (season_id, current_user.id)) + status_row = c.fetchone() + if status_row: + user_season_status = status_row['status'] + + # Standings Data (Regular Season only) + c.execute(''' + SELECT + u.id as user_id, u.team_name, u.team_icon, + SUM(CASE WHEN (g.home_team_id = u.id AND g.home_score > g.away_score) OR (g.away_team_id = u.id AND g.away_score > g.home_score) THEN 1 ELSE 0 END) as wins, + SUM(CASE WHEN (g.home_team_id = u.id AND g.home_score < g.away_score) OR (g.away_team_id = u.id AND g.away_score < g.home_score) THEN 1 ELSE 0 END) as losses, + SUM(CASE WHEN g.home_team_id = u.id THEN g.home_score WHEN g.away_team_id = u.id THEN g.away_score ELSE 0 END) as runs_for, + SUM(CASE WHEN g.home_team_id = u.id THEN g.away_score WHEN g.away_team_id = u.id THEN g.home_score ELSE 0 END) as runs_against, + (SELECT MIN(scheduled_date) FROM Games WHERE (home_team_id = u.id OR away_team_id = u.id) AND season_id = st.season_id) as first_game + FROM SeasonTeams st + JOIN Users u ON st.user_id = u.id + LEFT JOIN Games g ON (g.home_team_id = u.id OR g.away_team_id = u.id) AND g.season_id = st.season_id AND g.status = 'Final' AND g.is_playoff = 0 + WHERE st.season_id = ? AND st.status = 'Approved' + GROUP BY u.id, u.team_name, u.team_icon + ORDER BY wins DESC, runs_for DESC, runs_against ASC, first_game ASC + ''', (season_id,)) + standings_raw = c.fetchall() + + # Clinch Calculation + standings = [] + if standings_raw: + top_wins = standings_raw[0]['wins'] + top_losses = standings_raw[0]['losses'] + p_count = season_info['playoff_teams'] or 0 + total_sched = season_info['total_games'] or 0 + + # Get remaining games for each team + c.execute(''' + SELECT u.id, COUNT(g.id) as remaining + FROM SeasonTeams st + JOIN Users u ON st.user_id = u.id + LEFT JOIN Games g ON (g.home_team_id = u.id OR g.away_team_id = u.id) + AND g.season_id = st.season_id AND g.status = 'Scheduled' AND g.is_playoff = 0 + WHERE st.season_id = ? + GROUP BY u.id + ''', (season_id,)) + remaining_map = {row['id']: row['remaining'] for row in c.fetchall()} + + team_stats = [] + for row in standings_raw: + d = dict(row) + d['max_wins'] = d['wins'] + remaining_map.get(d['user_id'], 0) + team_stats.append(d) + + # Clinch Playoff Marker + # A team clinches if their wins > max possible wins of the team currently in the first 'out' spot + clinch_out_threshold = float('inf') + if p_count > 0 and len(team_stats) > p_count: + clinch_out_threshold = team_stats[p_count]['max_wins'] + + # Clinch 1st Seed Marker + clinch_1st_threshold = float('inf') + if p_count > 0 and len(team_stats) > 1: + clinch_1st_threshold = team_stats[1]['max_wins'] + + for i, d in enumerate(team_stats): + games_back = ((top_wins - d['wins']) + (d['losses'] - top_losses)) / 2.0 + d['games_back'] = "-" if games_back == 0 else str(games_back) + + d['clinch'] = "" + if d['wins'] > clinch_out_threshold: + d['clinch'] = "*" + if i == 0 and d['wins'] > clinch_1st_threshold: + d['clinch'] = "**" + standings.append(d) + + # Schedule Data + c.execute('SELECT id, team_name FROM Users WHERE is_admin = 0') + all_teams = c.fetchall() + + if not team_id and current_user.is_authenticated and not current_user.is_admin: + team_id = str(current_user.id) + elif not team_id: + team_id = 'all' + + query = ''' + SELECT g.*, + a.team_name as away_team, a.team_icon as away_icon, + h.team_name as home_team, h.team_icon as home_icon, + (SELECT COUNT(*) FROM GameSubmissions gs + WHERE g.status != 'Final' AND gs.game_id = g.id + AND ( + (gs.proposed_date IS NOT NULL AND gs.proposed_date != g.scheduled_date) OR + (gs.away_score IS NOT NULL AND (g.away_score IS NULL OR gs.away_score != g.away_score)) OR + (gs.home_score IS NOT NULL AND (g.home_score IS NULL OR gs.home_score != g.home_score)) + ) + ) as pending_proposals + FROM Games g + LEFT JOIN Users a ON g.away_team_id = a.id + LEFT JOIN Users h ON g.home_team_id = h.id + WHERE g.season_id = ? + ''' + params = [season_id] + + # We fetch ALL games first, then filter regular season games in Python if needed, + # OR we modify the query to include ALL playoff games but only filtered regular games. + if team_id and team_id != 'all': + query += ' AND (g.is_playoff = 1 OR g.away_team_id = ? OR g.home_team_id = ?)' + params.extend([team_id, team_id]) + + query += ' ORDER BY g.scheduled_date ASC' + c.execute(query, params) + games_list = c.fetchall() + + # Get team records for bracket display + team_records = {} + for row in standings: + team_records[row['user_id']] = f"({row['wins']}-{row['losses']})" + + # Group games by date for calendar + games_by_date = {} + playoff_games = [] + for g_row in games_list: + g = dict(g_row) + if g['is_playoff']: + g['away_record'] = team_records.get(g['away_team_id'], "") + g['home_record'] = team_records.get(g['home_team_id'], "") + playoff_games.append(g) + + dt = g['scheduled_date'] + if dt not in games_by_date: + games_by_date[dt] = [] + games_by_date[dt].append(g) + + # Bracket Logic (using series_id) + bracket_series = {} + for pg in playoff_games: + sid = pg['series_id'] + if sid not in bracket_series: + bracket_series[sid] = pg.copy() + c.execute(''' + SELECT + SUM(CASE WHEN away_score > home_score THEN 1 ELSE 0 END) as aw, + SUM(CASE WHEN home_score > away_score THEN 1 ELSE 0 END) as hw + FROM Games WHERE season_id = ? AND series_id = ? AND status = 'Final' + ''', (season_id, sid)) + s_res = c.fetchone() + bracket_series[sid]['away_series_wins'] = s_res['aw'] or 0 + bracket_series[sid]['home_series_wins'] = s_res['hw'] or 0 + + # Group series into rounds + bracket_rounds = {} + for sid, s_data in bracket_series.items(): + rnd = s_data['playoff_round'] + if rnd not in bracket_rounds: + bracket_rounds[rnd] = [] + bracket_rounds[rnd].append(s_data) + + sorted_rounds = sorted(bracket_rounds.items()) + bracket = [r[1] for r in sorted_rounds] + + # Calendar generation logic + import calendar + today = datetime.now().strftime('%Y-%m-%d') + + # Determine range of months to show + if games_list: + dates = [datetime.strptime(g['scheduled_date'], '%Y-%m-%d') for g in games_list if g['scheduled_date'] and g['scheduled_date'] != 'TBD'] + if dates: + min_date = min(dates) + max_date = max(dates) + else: + min_date = datetime.now() + max_date = datetime.now() + else: + min_date = datetime.now() + max_date = datetime.now() + + months = [] + curr = datetime(min_date.year, min_date.month, 1) + while curr <= datetime(max_date.year, max_date.month, 1): + cal = calendar.Calendar(firstweekday=6) # Sunday start + weeks = cal.monthdayscalendar(curr.year, curr.month) + months.append({ + 'name': curr.strftime('%B %Y'), + 'year': curr.year, + 'month': curr.month, + 'weeks': weeks + }) + if curr.month == 12: + curr = datetime(curr.year + 1, 1, 1) + else: + curr = datetime(curr.year, curr.month + 1, 1) + + conn.close() + return render_template('season.html', + seasons=seasons, + selected_season_id=season_id, + season_info=season_info, + standings=standings, + games_by_date=games_by_date, + playoff_games=playoff_games, + bracket=bracket, + months=months, + today=today, + all_teams=all_teams, + selected_team_id=team_id, + active_tab=tab, + user_season_status=user_season_status) + +def run_consensus_check(game_id, c): + # Consensus Check: Find most recent from each team + c.execute('SELECT away_team_id, home_team_id FROM Games WHERE id = ?', (game_id,)) + g_teams = c.fetchone() + if not g_teams: return + + c.execute(''' + SELECT * FROM GameSubmissions + WHERE game_id = ? AND submitted_by_id = ? + ORDER BY id DESC LIMIT 1 + ''', (game_id, g_teams['away_team_id'])) + sub_away = c.fetchone() + + c.execute(''' + SELECT * FROM GameSubmissions + WHERE game_id = ? AND submitted_by_id = ? + ORDER BY id DESC LIMIT 1 + ''', (game_id, g_teams['home_team_id'])) + sub_home = c.fetchone() + + if sub_away and sub_home: + # Check score consensus + if sub_away['away_score'] is not None and sub_home['away_score'] is not None and \ + sub_away['home_score'] is not None and sub_home['home_score'] is not None and \ + sub_away['away_score'] == sub_home['away_score'] and \ + sub_away['home_score'] == sub_home['home_score']: + + # Consensus reached on score + box_score_final = None + if sub_away['box_score_json'] and sub_home['box_score_json'] and sub_away['box_score_json'] == sub_home['box_score_json']: + bs = json.loads(sub_away['box_score_json']) + # bs["away"][10] is the 'runs' column (index 10) + if bs["away"][10] == sub_away['away_score'] and bs["home"][10] == sub_away['home_score']: + box_score_final = sub_away['box_score_json'] + + c.execute(''' + UPDATE Games + SET away_score = ?, home_score = ?, box_score_json = ?, status = 'Final' + WHERE id = ? + ''', (sub_away['away_score'], sub_away['home_score'], box_score_final, game_id)) + + c.execute('DELETE FROM GameSubmissions WHERE game_id = ?', (game_id,)) + + check_and_trigger_playoff_updates(game_id, c) + flash('Scores agreed! Game finalized.') + + # Check date consensus + if sub_away['proposed_date'] and sub_home['proposed_date'] and sub_away['proposed_date'] == sub_home['proposed_date']: + c.execute('UPDATE Games SET scheduled_date = ? WHERE id = ?', (sub_away['proposed_date'], game_id)) + c.execute('DELETE FROM GameSubmissions WHERE game_id = ?', (game_id,)) + flash('Date change agreed! Game rescheduled.') + +@app.route('/game/<int:game_id>/retract', methods=['POST']) +@login_required +def retract_submission(game_id): + conn = get_db() + c = conn.cursor() + c.execute('DELETE FROM GameSubmissions WHERE game_id = ? AND submitted_by_id = ?', (game_id, current_user.id)) + conn.commit() + conn.close() + flash('Submission retracted.') + return redirect(url_for('game', game_id=game_id)) + +@app.route('/game/<int:game_id>/agree/<int:submission_id>', methods=['POST']) +@login_required +def agree_submission(game_id, submission_id): + conn = get_db() + c = conn.cursor() + + # Get the submission to agree with + c.execute('SELECT * FROM GameSubmissions WHERE id = ?', (submission_id,)) + sub = c.fetchone() + + if not sub or sub['game_id'] != game_id: + conn.close() + flash('Submission not found.') + return redirect(url_for('game', game_id=game_id)) + + if str(sub['submitted_by_id']) == str(current_user.id): + conn.close() + flash('You cannot agree with your own submission.') + return redirect(url_for('game', game_id=game_id)) + + # Create an identical submission for the current user to trigger consensus + c.execute(''' + INSERT INTO GameSubmissions (game_id, submitted_by_id, away_score, home_score, box_score_json, proposed_date) + VALUES (?, ?, ?, ?, ?, ?) + ''', (game_id, current_user.id, sub['away_score'], sub['home_score'], sub['box_score_json'], sub['proposed_date'])) + + run_consensus_check(game_id, c) + + conn.commit() + conn.close() + return redirect(url_for('game', game_id=game_id)) +@app.route('/login', methods=['GET', 'POST']) +def login(): + if request.method == 'POST': + username = request.form['username'] + password = request.form['password'] + + conn = get_db() + c = conn.cursor() + c.execute('SELECT * FROM Users WHERE username = ?', (username,)) + user_row = c.fetchone() + conn.close() + + if user_row and check_password_hash(user_row['password_hash'], password): + user = User(user_row['id'], user_row['username'], user_row['is_admin'], user_row['team_name'], user_row['team_icon']) + login_user(user) + return redirect(url_for('index')) + else: + flash('Invalid username or password') + + return render_template('login.html') + +@app.route('/logout') +@login_required +def logout(): + logout_user() + return redirect(url_for('index')) + +@app.route('/game/<int:game_id>') +def game(game_id): + conn = get_db() + c = conn.cursor() + c.execute(''' + SELECT g.*, + a.team_name as away_team, a.team_icon as away_icon, + h.team_name as home_team, h.team_icon as home_icon + FROM Games g + LEFT JOIN Users a ON g.away_team_id = a.id + LEFT JOIN Users h ON g.home_team_id = h.id + WHERE g.id = ? + ''', (game_id,)) + game_row = c.fetchone() + + if not game_row: + conn.close() + return "Game not found", 404 + + game_dict = dict(game_row) + if game_dict['box_score_json']: + game_dict['box_score'] = json.loads(game_dict['box_score_json']) + else: + game_dict['box_score'] = None + + # Fetch pending submissions + c.execute(''' + SELECT s.*, u.username, u.team_name + FROM GameSubmissions s + JOIN Users u ON s.submitted_by_id = u.id + WHERE s.game_id = ? + ORDER BY s.id DESC + ''', (game_id,)) + submissions_raw = c.fetchall() + submissions = [] + for s in submissions_raw: + s_dict = dict(s) + + # Hide components that match current game state + if s_dict['proposed_date'] == game_dict['scheduled_date']: + s_dict['proposed_date'] = None + + if game_dict['status'] == 'Final': + if s_dict['away_score'] == game_dict['away_score'] and s_dict['home_score'] == game_dict['home_score']: + s_dict['away_score'] = None + s_dict['home_score'] = None + + # Only show if there's still something to show + if s_dict['proposed_date'] or s_dict['away_score'] is not None: + if s_dict['box_score_json']: + s_dict['box_score'] = json.loads(s_dict['box_score_json']) + else: + s_dict['box_score'] = None + submissions.append(s_dict) + + conn.close() + return render_template('game.html', game=game_dict, submissions=submissions) + +def generate_tbd_playoffs(season_id, playoff_teams, series_length, c): + # Generates TBD playoff series + # For N=4: Series 1 (Semi A), Series 2 (Semi B), Series 3 (Finals) + # For N=2: Series 1 (Finals) + games_to_create = [] + wins_needed = (series_length // 2) + 1 + start_date = datetime.now() + timedelta(days=30) + + series_map = { + 2: [1], # Finals + 4: [1, 2, 3] # Semis then Finals + } + + rounds_map = { + 2: {1: 1}, + 4: {1: 1, 2: 1, 3: 2} + } + + active_series = series_map.get(playoff_teams, []) + for sid in active_series: + rnd = rounds_map[playoff_teams][sid] + for g_num in range(1, series_length + 1): + is_conditional = 1 if g_num > wins_needed else 0 + games_to_create.append(( + season_id, None, None, 'TBD', + (start_date + timedelta(days=sid*7 + g_num)).strftime('%Y-%m-%d'), + 1, sid, rnd, g_num, is_conditional + )) + + c.executemany(''' + INSERT INTO Games (season_id, away_team_id, home_team_id, status, scheduled_date, is_playoff, series_id, playoff_round, playoff_game_num, is_conditional) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''', games_to_create) + +def check_and_trigger_playoff_updates(game_id, c): + c.execute('SELECT season_id, is_playoff, series_id, playoff_round FROM Games WHERE id = ?', (game_id,)) + game = c.fetchone() + if not game: return + season_id = game['season_id'] + + if not game['is_playoff']: + # Regular season check + c.execute("SELECT COUNT(*) as c FROM Games WHERE season_id = ? AND is_playoff = 0 AND status != 'Final'", (season_id,)) + if c.fetchone()['c'] == 0: + c.execute('SELECT playoff_teams FROM Seasons WHERE id = ?', (season_id,)) + p_teams = c.fetchone()['playoff_teams'] or 0 + + c.execute(''' + SELECT u.id + FROM SeasonTeams st + JOIN Users u ON st.user_id = u.id + LEFT JOIN Games g ON (g.home_team_id = u.id OR g.away_team_id = u.id) + AND g.season_id = st.season_id AND g.status = 'Final' AND g.is_playoff = 0 + WHERE st.season_id = ? AND st.status = 'Approved' + GROUP BY u.id ORDER BY SUM(CASE WHEN (g.home_team_id = u.id AND g.home_score > g.away_score) OR (g.away_team_id = u.id AND g.away_score > g.home_score) THEN 1 ELSE 0 END) DESC, + SUM(CASE WHEN g.home_team_id = u.id THEN g.home_score ELSE g.away_score END) DESC + LIMIT ? + ''', (season_id, p_teams)) + top_teams = [row['id'] for row in c.fetchall()] + + if len(top_teams) == 2: + c.execute("UPDATE Games SET away_team_id = ?, home_team_id = ?, status = 'Scheduled' WHERE season_id = ? AND series_id = 1", (top_teams[1], top_teams[0], season_id)) + elif len(top_teams) == 4: + # 1v4 (S1), 2v3 (S2) + c.execute("UPDATE Games SET away_team_id = ?, home_team_id = ?, status = 'Scheduled' WHERE season_id = ? AND series_id = 1", (top_teams[3], top_teams[0], season_id)) + c.execute("UPDATE Games SET away_team_id = ?, home_team_id = ?, status = 'Scheduled' WHERE season_id = ? AND series_id = 2", (top_teams[2], top_teams[1], season_id)) + else: + # Playoff advancement + c.execute('SELECT playoff_teams, playoff_series_length FROM Seasons WHERE id = ?', (season_id,)) + s_info = c.fetchone() + wins_needed = (s_info['playoff_series_length'] // 2) + 1 + sid = game['series_id'] + + c.execute(''' + SELECT + SUM(CASE WHEN away_score > home_score THEN 1 ELSE 0 END) as aw, + SUM(CASE WHEN home_score > away_score THEN 1 ELSE 0 END) as hw, + MAX(away_team_id) as aid, MAX(home_team_id) as hid + FROM Games WHERE season_id = ? AND series_id = ? AND status = 'Final' + ''', (season_id, sid)) + res = c.fetchone() + + winner = None + if res['aw'] >= wins_needed: winner = res['aid'] + elif res['hw'] >= wins_needed: winner = res['hid'] + + if winner: + c.execute("UPDATE Games SET status = 'Canceled' WHERE season_id = ? AND series_id = ? AND status IN ('Scheduled', 'TBD')", (season_id, sid)) + if s_info['playoff_teams'] == 4: + if sid == 1: c.execute("UPDATE Games SET away_team_id = ? WHERE season_id = ? AND series_id = 3", (winner, season_id)) + elif sid == 2: c.execute("UPDATE Games SET home_team_id = ? WHERE season_id = ? AND series_id = 3", (winner, season_id)) + c.execute("UPDATE Games SET status = 'Scheduled' WHERE season_id = ? AND series_id = 3 AND away_team_id IS NOT NULL AND home_team_id IS NOT NULL AND status = 'TBD'", (season_id,)) + +@app.route('/submit_game/<int:game_id>', methods=['POST']) +@login_required +def submit_game(game_id): + away_score = request.form.get('away_score') + home_score = request.form.get('home_score') + proposed_date = request.form.get('proposed_date') + + # Collect Box Score from grid (Optional) + box_score = {"away": [], "home": []} + cols = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "extra", "runs", "hits", "errors"] + has_box_data = False + for team in ["away", "home"]: + for col in cols: + val = request.form.get(f"{team}_inn_{col}") + if val and val != "0" and val != "": has_box_data = True + box_score[team].append(int(val) if val and val.isdigit() else 0) + + box_score_json = json.dumps(box_score) if has_box_data else None + + conn = get_db() + c = conn.cursor() + + # Check if user is part of the game + c.execute('SELECT away_team_id, home_team_id, status FROM Games WHERE id = ?', (game_id,)) + game = c.fetchone() + + if not game or game['status'] == 'Final': + conn.close() + flash('Game not found or already finalized.') + return redirect(url_for('game', game_id=game_id)) + + if str(current_user.id) not in (str(game['away_team_id']), str(game['home_team_id'])): + conn.close() + flash('You are not authorized to submit for this game.') + return redirect(url_for('game', game_id=game_id)) + + # Save submission (Update existing or Insert new) + away_score_val = int(away_score) if away_score else None + home_score_val = int(home_score) if home_score else None + + c.execute('SELECT * FROM GameSubmissions WHERE game_id = ? AND submitted_by_id = ?', (game_id, current_user.id)) + existing = c.fetchone() + + if existing: + if away_score_val is not None: + c.execute(''' + UPDATE GameSubmissions + SET away_score = ?, home_score = ?, box_score_json = ? + WHERE id = ? + ''', (away_score_val, home_score_val, box_score_json, existing['id'])) + if proposed_date: + c.execute(''' + UPDATE GameSubmissions + SET proposed_date = ? + WHERE id = ? + ''', (proposed_date, existing['id'])) + else: + c.execute(''' + INSERT INTO GameSubmissions (game_id, submitted_by_id, away_score, home_score, box_score_json, proposed_date) + VALUES (?, ?, ?, ?, ?, ?) + ''', (game_id, current_user.id, away_score_val, home_score_val, box_score_json, proposed_date)) + + run_consensus_check(game_id, c) + + conn.commit() + conn.close() + flash('Submission recorded.') + return redirect(url_for('game', game_id=game_id)) + +@app.route('/team', methods=['GET', 'POST']) +@login_required +def team(): + if request.method == 'POST': + conn = get_db() + c = conn.cursor() + + new_team_name = request.form.get('team_name') + if new_team_name: + c.execute('UPDATE Users SET team_name = ? WHERE id = ?', (new_team_name, current_user.id)) + current_user.team_name = new_team_name + + new_password = request.form.get('new_password') + if new_password: + c.execute('UPDATE Users SET password_hash = ? WHERE id = ?', (generate_password_hash(new_password), current_user.id)) + + icon_file = request.files.get('team_icon') + if icon_file and icon_file.filename != '': + filename = secure_filename(icon_file.filename) + filepath = os.path.join(app.config['UPLOAD_FOLDER'], f"{current_user.id}_{filename}") + icon_file.save(filepath) + c.execute('UPDATE Users SET team_icon = ? WHERE id = ?', (f"{current_user.id}_{filename}", current_user.id)) + current_user.team_icon = f"{current_user.id}_{filename}" + + conn.commit() + conn.close() + flash('Team profile updated successfully.') + return redirect(url_for('team')) + + conn = get_db() + c = conn.cursor() + c.execute('SELECT * FROM Seasons') + seasons = c.fetchall() + + c.execute('SELECT season_id, status FROM SeasonTeams WHERE user_id = ?', (current_user.id,)) + my_seasons = {row['season_id']: row['status'] for row in c.fetchall()} + conn.close() + + return render_template('team.html', seasons=seasons, my_seasons=my_seasons) + +@app.route('/team/join_season/<int:season_id>', methods=['POST']) +@login_required +def join_season(season_id): + if current_user.is_admin: + flash('Admins cannot join seasons as teams.') + return redirect(url_for('team')) + + conn = get_db() + c = conn.cursor() + try: + c.execute('INSERT INTO SeasonTeams (season_id, user_id, status) VALUES (?, ?, ?)', (season_id, current_user.id, 'Pending')) + conn.commit() + flash('Requested to join season.') + except sqlite3.IntegrityError: + flash('Already requested or joined this season.') + finally: + conn.close() + + return redirect(url_for('index', season_id=season_id)) + +@app.route('/admin', methods=['GET', 'POST']) +@login_required +def admin(): + if not current_user.is_admin: + return "Access Denied", 403 + + conn = get_db() + c = conn.cursor() + + if request.method == 'POST': + action = request.form.get('action') + if action == 'create_season': + name = request.form['name'] + c.execute('INSERT INTO Seasons (name) VALUES (?)', (name,)) + conn.commit() + flash('Season created.') + + elif action == 'create_team': + username = request.form['username'] + password = request.form['password'] + team_name = request.form['team_name'] + + try: + c.execute(''' + INSERT INTO Users (username, password_hash, team_name) + VALUES (?, ?, ?) + ''', (username, generate_password_hash(password), team_name)) + conn.commit() + flash('Team created.') + except sqlite3.IntegrityError: + flash('Username already exists.') + + c.execute('SELECT * FROM Seasons') + seasons = c.fetchall() + + c.execute('SELECT * FROM Users WHERE is_admin = 0') + teams = c.fetchall() + + conn.close() + return render_template('admin.html', seasons=seasons, teams=teams) + +@app.route('/admin/season/<int:season_id>/manage_teams', methods=['GET', 'POST']) +@login_required +def admin_manage_teams(season_id): + if not current_user.is_admin: + return "Access Denied", 403 + + conn = get_db() + c = conn.cursor() + + if request.method == 'POST': + action = request.form.get('action') + user_id = request.form.get('user_id') + + if action == 'approve': + c.execute("UPDATE SeasonTeams SET status = 'Approved' WHERE season_id = ? AND user_id = ?", (season_id, user_id)) + flash('Team approved.') + elif action == 'deny' or action == 'remove': + c.execute("DELETE FROM SeasonTeams WHERE season_id = ? AND user_id = ?", (season_id, user_id)) + flash(f"Team {action}d.") + elif action == 'add': + team_ids = request.form.getlist('team_ids') + for tid in team_ids: + try: + c.execute("INSERT INTO SeasonTeams (season_id, user_id, status) VALUES (?, ?, 'Approved')", (season_id, tid)) + except sqlite3.IntegrityError: + c.execute("UPDATE SeasonTeams SET status = 'Approved' WHERE season_id = ? AND user_id = ?", (season_id, tid)) + flash('Selected teams added directly.') + + conn.commit() + return redirect(url_for('admin_manage_teams', season_id=season_id)) + + c.execute('SELECT * FROM Seasons WHERE id = ?', (season_id,)) + season = c.fetchone() + + c.execute('SELECT * FROM Users WHERE is_admin = 0') + all_teams = c.fetchall() + + c.execute(''' + SELECT u.id, u.team_name, u.username, st.status + FROM SeasonTeams st + JOIN Users u ON st.user_id = u.id + WHERE st.season_id = ? + ''', (season_id,)) + enrolled_data = c.fetchall() + + pending_teams = [t for t in enrolled_data if t['status'] == 'Pending'] + approved_teams = [t for t in enrolled_data if t['status'] == 'Approved'] + enrolled_ids = [t['id'] for t in enrolled_data] + + conn.close() + return render_template('admin_manage_teams.html', season=season, all_teams=all_teams, pending_teams=pending_teams, approved_teams=approved_teams, enrolled_ids=enrolled_ids) + +@app.route('/admin/season/<int:season_id>/delete', methods=['POST']) +@login_required +def admin_delete_season(season_id): + if not current_user.is_admin: + return "Access Denied", 403 + + conn = get_db() + c = conn.cursor() + c.execute('DELETE FROM GameSubmissions WHERE game_id IN (SELECT id FROM Games WHERE season_id = ?)', (season_id,)) + c.execute('DELETE FROM Games WHERE season_id = ?', (season_id,)) + c.execute('DELETE FROM SeasonTeams WHERE season_id = ?', (season_id,)) + c.execute('DELETE FROM Seasons WHERE id = ?', (season_id,)) + conn.commit() + conn.close() + flash('Season deleted.') + return redirect(url_for('admin')) + +@app.route('/admin/season/<int:season_id>/generate_schedule', methods=['POST']) +@login_required +def admin_generate_schedule(season_id): + if not current_user.is_admin: + return "Access Denied", 403 + + confirm = request.form.get('confirm') == 'true' + total_games_per_team = int(request.form['total_games']) + gpw = int(request.form['games_per_week']) + p_teams = int(request.form['playoff_teams']) + p_series_len = int(request.form['playoff_series_length']) + + conn = get_db() + c = conn.cursor() + + # Check if any games are finalized + c.execute("SELECT COUNT(*) as c FROM Games WHERE season_id = ? AND status = 'Final'", (season_id,)) + final_count = c.fetchone()['c'] + + if final_count > 0 and not confirm: + conn.close() + return {"status": "confirm_required", "message": f"There are {final_count} finalized games. Regenerating will delete ALL games. Proceed?"}, 200 + + # Update Season Parameters + c.execute(''' + UPDATE Seasons + SET total_games = ?, games_per_week = ?, playoff_teams = ?, playoff_series_length = ? + WHERE id = ? + ''', (total_games_per_team, gpw, p_teams, p_series_len, season_id)) + + c.execute('SELECT user_id FROM SeasonTeams WHERE season_id = ? AND status = "Approved"', (season_id,)) + team_ids = [row['user_id'] for row in c.fetchall()] + + if len(team_ids) < 2: + conn.close() + flash('Not enough approved teams to generate schedule.') + return redirect(url_for('admin')) + + # Delete ALL existing games for this season + c.execute("DELETE FROM Games WHERE season_id = ?", (season_id,)) + c.execute("DELETE FROM GameSubmissions WHERE game_id NOT IN (SELECT id FROM Games)") + + # Per-team schedule generation using the circle method + num_teams = len(team_ids) + + if num_teams % 2 != 0: + team_ids.append(None) # Add a bye team + num_teams += 1 + + games_count = {tid: 0 for tid in team_ids if tid is not None} + current_teams = list(team_ids) + games_to_create = [] + start_date = datetime.now() + + round_num = 0 + while any(count < total_games_per_team for count in games_count.values()): + for i in range(num_teams // 2): + team1 = current_teams[i] + team2 = current_teams[num_teams - 1 - i] + + if team1 is not None and team2 is not None: + if games_count[team1] < total_games_per_team and games_count[team2] < total_games_per_team: + if round_num % 2 == 0: away_id, home_id = team1, team2 + else: away_id, home_id = team2, team1 + + day_offset = int((round_num * 7) / gpw) + game_date = (start_date + timedelta(days=day_offset)).strftime('%Y-%m-%d') + games_to_create.append((season_id, away_id, home_id, game_date)) + games_count[team1] += 1 + games_count[team2] += 1 + + current_teams = [current_teams[0]] + [current_teams[-1]] + current_teams[1:-1] + round_num += 1 + + c.executemany(''' + INSERT INTO Games (season_id, away_team_id, home_team_id, scheduled_date) + VALUES (?, ?, ?, ?) + ''', games_to_create) + + # Also generate Playoff TBDs + generate_tbd_playoffs(season_id, p_teams, p_series_len, c) + + conn.commit() + conn.close() + flash(f'Schedule and Playoffs generated.') + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + return {"status": "success", "redirect": url_for('admin')} + return redirect(url_for('admin')) + +@app.route('/admin/game/<int:game_id>/edit', methods=['GET', 'POST']) +@login_required +def admin_edit_game(game_id): + if not current_user.is_admin: + return "Access Denied", 403 + + conn = get_db() + c = conn.cursor() + + if request.method == 'POST': + scheduled_date = request.form.get('scheduled_date') + status = request.form.get('status') + away_score = request.form.get('away_score') + home_score = request.form.get('home_score') + + # Collect Box Score from grid + box_score = {"away": [], "home": []} + cols = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "extra", "runs", "hits", "errors"] + has_box_data = False + for team in ["away", "home"]: + for col in cols: + val = request.form.get(f"{team}_inn_{col}") + if val and val != "0" and val != "": has_box_data = True + box_score[team].append(int(val) if val and val.isdigit() else 0) + + box_score_json = json.dumps(box_score) if has_box_data else None + + c.execute(''' + UPDATE Games + SET scheduled_date = ?, status = ?, away_score = ?, home_score = ?, box_score_json = ? + WHERE id = ? + ''', (scheduled_date, status, away_score if away_score else None, home_score if home_score else None, box_score_json, game_id)) + + if status == 'Final': + check_and_trigger_playoff_updates(game_id, c) + c.execute('DELETE FROM GameSubmissions WHERE game_id = ?', (game_id,)) + + conn.commit() + flash('Game updated.') + return redirect(url_for('game', game_id=game_id)) + + c.execute(''' + SELECT g.*, a.team_name as away_team, h.team_name as home_team + FROM Games g + LEFT JOIN Users a ON g.away_team_id = a.id + LEFT JOIN Users h ON g.home_team_id = h.id + WHERE g.id = ? + ''', (game_id,)) + game_row = c.fetchone() + if not game_row: + conn.close() + return "Game not found", 404 + + game_dict = dict(game_row) + if game_dict['box_score_json']: + game_dict['box_score'] = json.loads(game_dict['box_score_json']) + else: + game_dict['box_score'] = None + + conn.close() + return render_template('admin_edit_game.html', game=game_dict) + +if __name__ == '__main__': + app.run(debug=True, port=5000)
\ No newline at end of file |
