From 6232b7709438ca63c8cac530b7217e82a4efb8b8 Mon Sep 17 00:00:00 2001 From: cedrick2711 Date: Tue, 25 Aug 2026 13:29:22 -0400 Subject: [PATCH] ajout d'un paneau admin --- app/app.py | 18 +- app/images/UdeS_Esports_logo.png | Bin 0 -> 93493 bytes app/models/__init__.py | 5 +- app/models/admin_settings.py | 55 +++++ app/models/audit_log.py | 32 +++ app/models/backup_record.py | 21 ++ app/routes/admin.py | 355 ++++++++++++++++++++++++++++ app/routes/matches.py | 16 +- app/routes/team_matches.py | 23 ++ app/routes/tryouts.py | 46 ++++ app/routes/users.py | 146 ++++++++---- app/supporting_scripts/backup.py | 250 ++++++++++---------- app/templates/layouts/base.html | 6 + app/templates/pages/calendar.html | 128 ++++++---- app/templates/pages/one_on_one.html | 339 +++++++++++++------------- 15 files changed, 1053 insertions(+), 387 deletions(-) create mode 100644 app/images/UdeS_Esports_logo.png create mode 100644 app/models/admin_settings.py create mode 100644 app/models/audit_log.py create mode 100644 app/models/backup_record.py create mode 100644 app/routes/admin.py diff --git a/app/app.py b/app/app.py index f15ffa3..ce8a0c2 100644 --- a/app/app.py +++ b/app/app.py @@ -12,9 +12,11 @@ from sqlalchemy import text from werkzeug.exceptions import HTTPException import markupsafe from dotenv import load_dotenv +import os as _os -load_dotenv() - +# Load .env from the app directory (independent of the process working directory) +_env_path = _os.path.join(_os.path.dirname(_os.path.abspath(__file__)), '.env') +load_dotenv(_env_path) def nl2br(value): """Convert newlines to HTML line breaks. @@ -55,11 +57,17 @@ def create_app(): app.config['SECRET_KEY'] = os.getenv('SECRET_KEY') if not app.config['SECRET_KEY']: raise RuntimeError('SECRET_KEY environment variable must be set for security') - app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL') - if not app.config['SQLALCHEMY_DATABASE_URI']: + database_url = os.getenv('DATABASE_URL') + if not database_url: raise RuntimeError( 'DATABASE_URL environment variable must be set to a PostgreSQL connection string' ) + # Use psycopg2 driver for PostgreSQL + if database_url.startswith('postgresql://'): + database_url = database_url.replace('postgresql://', 'postgresql+psycopg2://', 1) + elif database_url.startswith('postgres://'): + database_url = database_url.replace('postgres://', 'postgresql+psycopg2://', 1) + app.config['SQLALCHEMY_DATABASE_URI'] = database_url app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config['WTF_CSRF_ENABLED'] = True @@ -111,6 +119,7 @@ def create_app(): from app.routes.teams import teams_bp from app.routes.matches import matches_bp from app.routes.team_matches import team_matches_bp + from app.routes.admin import admin_bp app.register_blueprint(auth_bp) app.register_blueprint(tryouts_bp) @@ -120,6 +129,7 @@ def create_app(): app.register_blueprint(teams_bp) app.register_blueprint(matches_bp) app.register_blueprint(team_matches_bp) + app.register_blueprint(admin_bp) # Register custom Jinja filters app.jinja_env.filters['nl2br'] = nl2br diff --git a/app/images/UdeS_Esports_logo.png b/app/images/UdeS_Esports_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..b5756f1bf4804d5cca2a5452c5ac4bfd1b6d6cef GIT binary patch literal 93493 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≪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 literal 0 HcmV?d00001 diff --git a/app/models/__init__.py b/app/models/__init__.py index e82c7e9..06e22ad 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -92,4 +92,7 @@ from app.models.user_gamertag import UserGamertag from app.models.contract import Contract from app.models.team_note import TeamNote from app.models.personal_note import PersonalNote -from app.models.one_on_one_request import OneOnOneRequest \ No newline at end of file +from app.models.one_on_one_request import OneOnOneRequest +from app.models.admin_settings import AppSettings +from app.models.backup_record import BackupRecord +from app.models.audit_log import AuditLog diff --git a/app/models/admin_settings.py b/app/models/admin_settings.py new file mode 100644 index 0000000..73bb7b6 --- /dev/null +++ b/app/models/admin_settings.py @@ -0,0 +1,55 @@ +"""Global application settings stored as key-value pairs in the database.""" + +from app.extensions import db + + +class AppSettings(db.Model): + """Key-value store for global application settings. + + Stores toggles and configuration that admins control through the + admin panel, such as whether tryouts are open, season state, etc. + """ + + __tablename__ = 'app_settings' + + key = db.Column(db.String(100), primary_key=True) + value = db.Column(db.Text, nullable=True) + + # ------------------------------------------------------------------ + # Well-known keys (documented here for discoverability) + # ------------------------------------------------------------------ + # tryouts_open – "true" / "false" (default: "true") + # season_active – "true" / "false" (default: "false") + # season_name – e.g. "Fall 2026" + # season_start – ISO date string + # season_end – ISO date string + + @staticmethod + def get(key, default=None): + """Return the value for *key*, or *default* if not set.""" + row = db.session.get(AppSettings, key) + return row.value if row is not None else default + + @staticmethod + def set(key, value): + """Upsert a setting.""" + row = db.session.get(AppSettings, key) + if row is None: + row = AppSettings(key=key, value=str(value) if value is not None else None) + db.session.add(row) + else: + row.value = str(value) if value is not None else None + db.session.commit() + + @staticmethod + def get_bool(key, default=False): + """Return a boolean setting.""" + val = AppSettings.get(key) + if val is None: + return default + return val.lower() in ('true', '1', 'yes', 'on') + + @staticmethod + def set_bool(key, value): + """Store a boolean setting as 'true' / 'false'.""" + AppSettings.set(key, 'true' if value else 'false') \ No newline at end of file diff --git a/app/models/audit_log.py b/app/models/audit_log.py new file mode 100644 index 0000000..611ad57 --- /dev/null +++ b/app/models/audit_log.py @@ -0,0 +1,32 @@ +"""Audit log for tracking sensitive administrative actions.""" + +from app.extensions import db +from datetime import datetime + + +class AuditLog(db.Model): + """Append-only log of critical admin actions for accountability.""" + + __tablename__ = 'audit_logs' + + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True) + action = db.Column(db.String(100), nullable=False) + details = db.Column(db.Text, nullable=True) + ip_address = db.Column(db.String(64), nullable=True) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + user = db.relationship('User', foreign_keys=[user_id], backref='audit_logs') + + @staticmethod + def record(user_id, action, details=None, ip_address=None): + """Create a new audit log entry.""" + entry = AuditLog( + user_id=user_id, + action=action, + details=details, + ip_address=ip_address, + ) + db.session.add(entry) + db.session.commit() + return entry \ No newline at end of file diff --git a/app/models/backup_record.py b/app/models/backup_record.py new file mode 100644 index 0000000..248d9cf --- /dev/null +++ b/app/models/backup_record.py @@ -0,0 +1,21 @@ +"""Records of database backups created through the admin panel.""" + +from app.extensions import db +from datetime import datetime + + +class BackupRecord(db.Model): + """Metadata for a database backup stored on disk.""" + + __tablename__ = 'backup_records' + + id = db.Column(db.Integer, primary_key=True) + filename = db.Column(db.String(255), nullable=False) + file_path = db.Column(db.String(500), nullable=False) + size_bytes = db.Column(db.BigInteger, nullable=True) + backup_type = db.Column(db.String(20), default='manual') # 'manual' or 'auto' + notes = db.Column(db.Text, nullable=True) + created_by_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + creator = db.relationship('User', foreign_keys=[created_by_id], backref='backups') \ No newline at end of file diff --git a/app/routes/admin.py b/app/routes/admin.py new file mode 100644 index 0000000..19a682c --- /dev/null +++ b/app/routes/admin.py @@ -0,0 +1,355 @@ +"""Admin panel routes for system-level management. + +Provides backup/restore, tryout open/close toggling, season lifecycle +management, team wiping, and audit log viewing. All routes are restricted +to administrators. +""" + +import os +from datetime import datetime + +from flask import ( + Blueprint, render_template, redirect, url_for, flash, request, + send_file, +) +from flask_login import login_required, current_user + +from app.extensions import db +from app.models import ( + Admin, User, Tryout, OrgTeam, TeamPlayer, TeamMatch, + TeamMatchParticipant, AppSettings, BackupRecord, AuditLog, +) +from app.supporting_scripts import backup as backup_helper + +admin_bp = Blueprint('admin', __name__, url_prefix='/admin') + + +def require_admin(): + """Return True if current user is an Admin, else flash and redirect. + + This returns False for non-admins so the caller can stop processing, + but the redirect/abort should be handled by the caller. + """ + if isinstance(current_user, Admin): + return True + flash('Only the president can access the admin panel.', 'danger') + return False + + +def _client_ip(): + """Best-effort client IP for audit logging.""" + if request.headers.get('X-Forwarded-For'): + return request.headers.get('X-Forwarded-For').split(',')[0].strip() + return request.remote_addr + + +def _log(action, details=None): + """Record an audit log entry for the current user.""" + AuditLog.record( + user_id=current_user.id, + action=action, + details=details, + ip_address=_client_ip(), + ) + + +# --------------------------------------------------------------------------- +# Dashboard +# --------------------------------------------------------------------------- + +@admin_bp.route('') +@login_required +def dashboard(): + """Render the admin panel dashboard.""" + if not require_admin(): + return redirect(url_for('main.dashboard')) + + stats = { + 'total_users': User.query.count(), + 'total_players': User.query.filter_by(role='player').count(), + 'total_org_teams': OrgTeam.query.count(), + 'active_tryouts': Tryout.query.filter_by(status='in_progress').count(), + 'upcoming_tryouts': Tryout.query.filter_by(status='upcoming').count(), + } + + settings = { + 'tryouts_open': AppSettings.get_bool('tryouts_open', default=True), + 'season_active': AppSettings.get_bool('season_active', default=False), + 'season_name': AppSettings.get('season_name') or 'Not set', + 'season_start': AppSettings.get('season_start') or 'Not set', + 'season_end': AppSettings.get('season_end') or 'Not set', + } + + backups = BackupRecord.query.order_by(BackupRecord.created_at.desc()).limit(20).all() + audit_logs = AuditLog.query.order_by(AuditLog.created_at.desc()).limit(20).all() + + return render_template( + 'pages/admin.html', + stats=stats, + settings=settings, + backups=backups, + audit_logs=audit_logs, + ) + + +# --------------------------------------------------------------------------- +# Backups +# --------------------------------------------------------------------------- + +@admin_bp.route('/backup/create', methods=['POST']) +@login_required +def create_backup(): + """Create a manual database backup.""" + if not require_admin(): + return redirect(url_for('main.dashboard')) + + notes = request.form.get('notes', '').strip() or None + try: + result = backup_helper.create_backup(backup_type='manual', notes=notes) + except RuntimeError as e: + flash(f'Backup failed: {e}', 'danger') + _log('backup_failed', f'Error: {e}') + return redirect(url_for('admin.dashboard')) + + record = BackupRecord( + filename=result['filename'], + file_path=result['file_path'], + size_bytes=result['size_bytes'], + backup_type='manual', + notes=notes, + created_by_id=current_user.id, + ) + db.session.add(record) + db.session.commit() + + _log('backup_created', f'File: {result["filename"]} ({result["size_bytes"]} bytes)') + flash(f'Backup created successfully: {result["filename"]}', 'success') + return redirect(url_for('admin.dashboard')) + + +@admin_bp.route('/backup//download') +@login_required +def download_backup(backup_id): + """Download a backup file.""" + if not require_admin(): + return redirect(url_for('main.dashboard')) + + record = BackupRecord.query.get_or_404(backup_id) + if not os.path.exists(record.file_path): + flash('Backup file is missing from disk.', 'danger') + return redirect(url_for('admin.dashboard')) + + _log('backup_downloaded', f'File: {record.filename}') + return send_file(record.file_path, as_attachment=True, download_name=record.filename) + + +@admin_bp.route('/backup//delete', methods=['POST']) +@login_required +def delete_backup(backup_id): + """Delete a backup file and its record.""" + if not require_admin(): + return redirect(url_for('main.dashboard')) + + record = BackupRecord.query.get_or_404(backup_id) + if os.path.exists(record.file_path): + try: + os.remove(record.file_path) + except OSError as e: + flash(f'Could not remove backup file: {e}', 'danger') + return redirect(url_for('admin.dashboard')) + + _log('backup_deleted', f'File: {record.filename}') + db.session.delete(record) + db.session.commit() + flash(f'Backup {record.filename} deleted.', 'success') + return redirect(url_for('admin.dashboard')) + + +@admin_bp.route('/backup//restore', methods=['POST']) +@login_required +def restore_backup(backup_id): + """Restore a selected backup. + + A safety backup of the current state is created first, then the + selected dump is restored via psql. + """ + if not require_admin(): + return redirect(url_for('main.dashboard')) + + record = BackupRecord.query.get_or_404(backup_id) + if not os.path.exists(record.file_path): + flash('Backup file is missing from disk.', 'danger') + return redirect(url_for('admin.dashboard')) + + # Create a safety backup of the current state before restoring. + try: + safety = backup_helper.create_backup(backup_type='pre_restore', notes='Pre-restore safety backup') + safety_record = BackupRecord( + filename=safety['filename'], + file_path=safety['file_path'], + size_bytes=safety['size_bytes'], + backup_type='auto', + notes='Automatic safety backup before restoring ' + record.filename, + created_by_id=current_user.id, + ) + db.session.add(safety_record) + db.session.commit() + except RuntimeError as e: + flash(f'Could not create safety backup, restore aborted: {e}', 'danger') + return redirect(url_for('admin.dashboard')) + + try: + backup_helper.restore_backup(record.file_path) + except RuntimeError as e: + flash(f'Restore failed: {e}', 'danger') + _log('backup_restore_failed', f'File: {record.filename}, Error: {e}') + return redirect(url_for('admin.dashboard')) + + _log('backup_restored', f'File: {record.filename}') + flash(f'Backup {record.filename} restored successfully. The database has been rolled back.', 'success') + return redirect(url_for('admin.dashboard')) + + +# --------------------------------------------------------------------------- +# Tryouts open/close toggle +# --------------------------------------------------------------------------- + +@admin_bp.route('/toggle-tryouts', methods=['POST']) +@login_required +def toggle_tryouts(): + """Toggle the global tryout open/close state.""" + if not require_admin(): + return redirect(url_for('main.dashboard')) + + current = AppSettings.get_bool('tryouts_open', default=True) + new_value = not current + AppSettings.set_bool('tryouts_open', new_value) + + state = 'opened' if new_value else 'closed' + _log('tryouts_toggled', f'Tryouts {state}') + flash(f'Tryouts are now {state}.', 'success') + return redirect(url_for('admin.dashboard')) + + +# --------------------------------------------------------------------------- +# Season lifecycle +# --------------------------------------------------------------------------- + +@admin_bp.route('/season/start', methods=['POST']) +@login_required +def start_season(): + """Begin a new regular season.""" + if not require_admin(): + return redirect(url_for('main.dashboard')) + + if AppSettings.get_bool('season_active', default=False): + flash('A season is already active. End it before starting a new one.', 'danger') + return redirect(url_for('admin.dashboard')) + + name = request.form.get('season_name', '').strip() + start_date = request.form.get('season_start', '').strip() + if not start_date: + flash('A season start date is required.', 'danger') + return redirect(url_for('admin.dashboard')) + + try: + datetime.strptime(start_date, '%Y-%m-%d') + except ValueError: + flash('Invalid season start date format.', 'danger') + return redirect(url_for('admin.dashboard')) + + AppSettings.set('season_name', name or 'Untitled Season') + AppSettings.set('season_start', start_date) + AppSettings.set('season_end', None) + AppSettings.set_bool('season_active', True) + + _log('season_started', f'Season: {name or "Untitled Season"}, Start: {start_date}') + flash(f'Season "{name or "Untitled Season"}" has begun.', 'success') + return redirect(url_for('admin.dashboard')) + + +@admin_bp.route('/season/end', methods=['POST']) +@login_required +def end_season(): + """End the current regular season.""" + if not require_admin(): + return redirect(url_for('main.dashboard')) + + if not AppSettings.get_bool('season_active', default=False): + flash('No season is currently active.', 'danger') + return redirect(url_for('admin.dashboard')) + + end_date = request.form.get('season_end', '').strip() + if not end_date: + flash('A season end date is required.', 'danger') + return redirect(url_for('admin.dashboard')) + + try: + datetime.strptime(end_date, '%Y-%m-%d') + except ValueError: + flash('Invalid season end date format.', 'danger') + return redirect(url_for('admin.dashboard')) + + name = AppSettings.get('season_name') + AppSettings.set('season_end', end_date) + AppSettings.set_bool('season_active', False) + + _log('season_ended', f'Season: {name}, End: {end_date}') + flash(f'Season "{name}" has ended.', 'success') + return redirect(url_for('admin.dashboard')) + + +# --------------------------------------------------------------------------- +# Wipe teams for new season +# --------------------------------------------------------------------------- + +@admin_bp.route('/teams/wipe', methods=['POST']) +@login_required +def wipe_teams(): + """Wipe team rosters and season matches for a new season. + + Players are removed from org teams (TeamPlayer records), regular-season + matches and their participants are deleted. OrgTeam structures, coaches, + and managers are preserved. + """ + if not require_admin(): + return redirect(url_for('main.dashboard')) + + confirm = request.form.get('confirm', '').strip() + if confirm != 'WIPE': + flash("Type 'WIPE' in the confirmation box to proceed.", 'danger') + return redirect(url_for('admin.dashboard')) + + # Safety backup before destructive operation + try: + safety = backup_helper.create_backup(backup_type='pre_wipe', notes='Pre-wipe safety backup') + safety_record = BackupRecord( + filename=safety['filename'], + file_path=safety['file_path'], + size_bytes=safety['size_bytes'], + backup_type='auto', + notes='Automatic safety backup before wiping teams', + created_by_id=current_user.id, + ) + db.session.add(safety_record) + db.session.commit() + except RuntimeError as e: + flash(f'Wipe aborted — could not create safety backup: {e}', 'danger') + return redirect(url_for('admin.dashboard')) + + roster_count = TeamPlayer.query.count() + match_count = TeamMatch.query.count() + + # Delete team match participants first (FK order) + TeamMatchParticipant.query.delete() + TeamMatch.query.delete() + TeamPlayer.query.delete() + db.session.commit() + + _log('teams_wiped', f'Removed {roster_count} roster entries and {match_count} season matches') + flash( + f'Teams wiped for the new season. Removed {roster_count} roster entries ' + f'and {match_count} regular-season matches.', + 'success', + ) + return redirect(url_for('admin.dashboard')) \ No newline at end of file diff --git a/app/routes/matches.py b/app/routes/matches.py index 457cacb..702f4bd 100644 --- a/app/routes/matches.py +++ b/app/routes/matches.py @@ -104,7 +104,7 @@ def api_events(): one_on_ones = [] for ooo in one_on_ones: - events.append({ + event = { 'id': f'one_on_one_{ooo.id}', 'title': f'1:1 - {ooo.player.full_name} & {ooo.coach.full_name}', 'date': ooo.date.strftime('%Y-%m-%d'), @@ -117,8 +117,20 @@ def api_events(): 'start_time': ooo.start_time.strftime('%H:%M') if ooo.start_time else None, 'end_time': ooo.end_time.strftime('%H:%M') if ooo.end_time else None, 'participants': f"{ooo.player.full_name} with {ooo.coach.full_name}", + 'player_name': ooo.player.full_name, + 'coach_name': ooo.coach.full_name, }, - }) + } + + # Provide real start/end datetimes so FullCalendar places the event + # in the correct time slot (Week/Day views) instead of all-day. + if ooo.start_time and ooo.end_time: + start_dt = datetime.combine(ooo.date, ooo.start_time) + end_dt = datetime.combine(ooo.date, ooo.end_time) + event['start'] = start_dt.isoformat() + event['end'] = end_dt.isoformat() + + events.append(event) return jsonify(events) diff --git a/app/routes/team_matches.py b/app/routes/team_matches.py index 3d2115f..9dcf370 100644 --- a/app/routes/team_matches.py +++ b/app/routes/team_matches.py @@ -9,6 +9,7 @@ from app.extensions import db from app.models import ( Admin, Manager, Coach, Player, OrgTeam, User, TeamMatch, TeamMatchParticipant, TeamPlayer, + AppSettings, ) from datetime import datetime, timedelta from app.discord_bot import send_schedule_notification @@ -30,6 +31,17 @@ def can_manage_team_match(team): return False +def season_locked(): + """Return True when the regular season is inactive for non-admins. + + Admins bypass the lock. Coaches and managers may only schedule regular + season matches while the season is active. + """ + if isinstance(current_user, Admin): + return False + return not AppSettings.get_bool('season_active', default=False) + + @team_matches_bp.route('') @login_required def list_matches(): @@ -96,6 +108,10 @@ def create_match(team_id): flash('You do not have permission to schedule matches for this team.', 'danger') return redirect(url_for('team_matches.list_matches')) + if season_locked(): + flash('The regular season is not active. An admin must begin a season before scheduling matches.', 'danger') + return redirect(url_for('team_matches.list_matches')) + team_players = [tp for tp in TeamPlayer.query.filter_by(org_team_id=team_id).all()] prefill_date = request.args.get('date', '') is_practice = request.args.get('type') == 'practice' @@ -208,6 +224,10 @@ def edit_match(match_id): flash('You do not have permission to edit this match.', 'danger') return redirect(url_for('team_matches.list_matches')) + if season_locked(): + flash('The regular season is not active. An admin must begin a season before editing matches.', 'danger') + return redirect(url_for('team_matches.list_matches')) + if request.method == 'POST': team_match.title = request.form.get('title', team_match.title) team_match.description = request.form.get('description', '') or None @@ -257,6 +277,9 @@ def delete_match(match_id): if not can_manage_team_match(team): flash('You do not have permission to delete this match.', 'danger') return redirect(url_for('team_matches.list_matches')) + if season_locked(): + flash('The regular season is not active. An admin must begin a season before deleting matches.', 'danger') + return redirect(url_for('team_matches.list_matches')) db.session.delete(team_match) db.session.commit() flash('Match deleted successfully.', 'success') diff --git a/app/routes/tryouts.py b/app/routes/tryouts.py index df11f97..417dfaf 100644 --- a/app/routes/tryouts.py +++ b/app/routes/tryouts.py @@ -12,6 +12,7 @@ from app.models import ( User, Tryout, TryoutRegistration, Evaluation, Team, TeamMember, OrgTeam, Match, MatchParticipant, ESPORT_GAMES, GAME_POSITIONS, + AppSettings, ) from datetime import datetime @@ -23,6 +24,17 @@ def can_manage(): return isinstance(current_user, (Admin, Manager)) +def tryouts_locked(): + """Return True when tryouts are globally closed to coaches/managers. + + Admins are always allowed to bypass the lock. Coaches and managers can + only make changes when the global tryout switch is open. + """ + if isinstance(current_user, Admin): + return False + return not AppSettings.get_bool('tryouts_open', default=True) + + @tryouts_bp.route('') @login_required def list_tryouts(): @@ -42,6 +54,10 @@ def create_tryout(): flash('You do not have permission to create tryouts.', 'danger') return redirect(url_for('tryouts.list_tryouts')) + if tryouts_locked(): + flash('Tryouts are currently closed. An admin must open tryouts before changes can be made.', 'danger') + return redirect(url_for('tryouts.list_tryouts')) + org_teams = OrgTeam.query.order_by(OrgTeam.name).all() managers = User.query.filter_by(role='manager', is_active_account=True).order_by(User.username).all() coaches = User.query.filter_by(role='coach', is_active_account=True).order_by(User.username).all() @@ -113,6 +129,10 @@ def edit_tryout(tryout_id): flash('You do not have permission to edit this tryout.', 'danger') return redirect(url_for('tryouts.list_tryouts')) + if tryouts_locked(): + flash('Tryouts are currently closed. An admin must open tryouts before changes can be made.', 'danger') + return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id)) + if tryout.is_ended: flash('This tryout has ended and can no longer be modified.', 'danger') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id)) @@ -339,6 +359,9 @@ def update_status(tryout_id): if not current_user.can_manage_this_tryout(tryout): flash('Permission denied.', 'danger') return redirect(url_for('tryouts.list_tryouts')) + if tryouts_locked(): + flash('Tryouts are currently closed. An admin must open tryouts before changes can be made.', 'danger') + return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) new_status = request.form.get('status') if new_status in ['upcoming', 'in_progress', 'completed']: tryout.status = new_status @@ -356,6 +379,10 @@ def update_registration_status(tryout_id, player_id): flash('Permission denied.', 'danger') return redirect(url_for('tryouts.list_tryouts')) + if tryouts_locked(): + flash('Tryouts are currently closed. An admin must open tryouts before changes can be made.', 'danger') + return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) + registration = TryoutRegistration.query.filter_by( tryout_id=tryout_id, player_id=player_id).first_or_404() new_status = request.form.get('status') @@ -374,6 +401,9 @@ def register_player(tryout_id): if not current_user.can_manage_this_tryout(tryout): flash('Permission denied.', 'danger') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) + if tryouts_locked(): + flash('Tryouts are currently closed. An admin must open tryouts before changes can be made.', 'danger') + return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) player_id = request.form.get('player_id') if not player_id: flash('Please select a player.', 'danger') @@ -412,6 +442,10 @@ def remove_player(tryout_id, player_id): flash('Permission denied.', 'danger') return redirect(url_for('tryouts.list_tryouts')) + if tryouts_locked(): + flash('Tryouts are currently closed. An admin must open tryouts before changes can be made.', 'danger') + return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) + player = User.query.get_or_404(player_id) registration = TryoutRegistration.query.filter_by( @@ -447,6 +481,10 @@ def create_team(tryout_id): flash('Permission denied.', 'danger') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) + if tryouts_locked(): + flash('Tryouts are currently closed. An admin must open tryouts before changes can be made.', 'danger') + return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) + team_name = request.form.get('team_name') if team_name: team = Team(tryout_id=tryout_id, name=team_name, created_by=current_user.id) @@ -466,6 +504,10 @@ def add_to_team(tryout_id, team_id): flash('Permission denied.', 'danger') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) + if tryouts_locked(): + flash('Tryouts are currently closed. An admin must open tryouts before changes can be made.', 'danger') + return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) + player_id = request.form.get('player_id') position = request.form.get('position', '') existing = TeamMember.query.filter_by(team_id=team_id, player_id=player_id).first() @@ -488,6 +530,10 @@ def delete_tryout(tryout_id): flash('You do not have permission to delete this tryout.', 'danger') return redirect(url_for('tryouts.list_tryouts')) + if tryouts_locked(): + flash('Tryouts are currently closed. An admin must open tryouts before changes can be made.', 'danger') + return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) + # Delete match participants for all matches in this tryout match_ids = [m.id for m in Match.query.filter_by(tryout_id=tryout_id).all()] if match_ids: diff --git a/app/routes/users.py b/app/routes/users.py index f7619f9..f029da9 100644 --- a/app/routes/users.py +++ b/app/routes/users.py @@ -708,6 +708,70 @@ def send_discord_notification(player_name, points, date_str, start_time_str, end logger.warning(f"Failed to send Discord notification: {e}") +def _get_player_coach_options(player): + """Build a deduplicated list of coaches (with their teams) for a player. + + Returns a list of dicts: + [{'coach_id': int, 'coach_name': str, 'teams': [{'team_id': int, 'team_name': str}]}] + """ + coach_map = {} + for org_team in player.get_org_teams(): + for coach in org_team.get_coaches(): + entry = coach_map.setdefault(coach.id, { + 'coach_id': coach.id, + 'coach_name': coach.full_name or coach.username, + 'teams': [], + }) + entry['teams'].append({ + 'team_id': org_team.id, + 'team_name': org_team.name, + }) + return list(coach_map.values()) + + +def _get_player_coach_ids(player): + """Return the set of coach ids the player is allowed to request.""" + return {c['coach_id'] for c in _get_player_coach_options(player)} + + +@users_bp.route('/one-on-one/coaches') +@login_required +def one_on_one_coaches(): + """API: list coaches (and their teams) available to the current player.""" + if not isinstance(current_user, Player): + return jsonify({'error': 'Only players can request One on One sessions.'}), 403 + + coaches = _get_player_coach_options(current_user) + return jsonify({'coaches': coaches}) + + +@users_bp.route('/one-on-one/coaches//availability') +@login_required +def one_on_one_coach_availability(coach_id): + """API: return a single coach's availability for the current player.""" + if not isinstance(current_user, Player): + return jsonify({'error': 'Only players can request One on One sessions.'}), 403 + + if coach_id not in _get_player_coach_ids(current_user): + return jsonify({'error': 'Coach is not assigned to any of your teams.'}), 403 + + coach = User.query.get_or_404(coach_id) + availabilities = CoachAvailability.query.filter_by(coach_id=coach_id).all() + availability = [ + { + 'day_of_week': av.day_of_week, + 'start_time': av.start_time.strftime('%H:%M'), + 'end_time': av.end_time.strftime('%H:%M'), + } + for av in availabilities + ] + return jsonify({ + 'coach_id': coach.id, + 'coach_name': coach.full_name or coach.username, + 'availability': availability, + }) + + @users_bp.route('/one-on-one', methods=['GET', 'POST']) @login_required def one_on_one(): @@ -716,41 +780,37 @@ def one_on_one(): flash('Only players can request One on One sessions.', 'danger') return redirect(url_for('main.dashboard')) - org_teams = current_user.get_org_teams() - org_team = org_teams[0] if org_teams else None - coach = User.query.get(org_team.coach_id) if org_team and org_team.coach_id else None - - if not coach: - flash('You do not have a coach assigned to your team.', 'info') - - team_notes = [] - if org_team: - team_notes = TeamNote.query.filter_by(org_team_id=org_team.id).order_by(TeamNote.created_at.desc()).all() - - personal_notes = PersonalNote.query.filter_by(player_id=current_user.id).order_by(PersonalNote.created_at.desc()).all() - - coach_availability = [] - if coach: - availabilities = CoachAvailability.query.filter_by(coach_id=coach.id).all() - coach_availability = [ - { - 'day_of_week': av.day_of_week, - 'start_time': av.start_time.strftime('%H:%M'), - 'end_time': av.end_time.strftime('%H:%M'), - } - for av in availabilities - ] + coach_options = _get_player_coach_options(current_user) if request.method == 'POST': + coach_id = request.form.get('coach_id', type=int) + org_team_id = request.form.get('org_team_id', type=int) date_str = request.form.get('date') start_time_str = request.form.get('start_time') end_time_str = request.form.get('end_time') points = request.form.get('points', '').strip() - if not coach: + if not coach_options: flash('Cannot request One on One - no coach assigned.', 'danger') return redirect(url_for('users.one_on_one')) + if coach_id not in _get_player_coach_ids(current_user): + flash('Selected coach is not assigned to any of your teams.', 'danger') + return redirect(url_for('users.one_on_one')) + + coach = User.query.get_or_404(coach_id) + + # Validate org_team_id belongs to the player and includes this coach + org_team = None + if org_team_id: + org_team = OrgTeam.query.get(org_team_id) + if not org_team or org_team not in current_user.get_org_teams(): + flash('Invalid team selection.', 'danger') + return redirect(url_for('users.one_on_one')) + if coach not in org_team.get_coaches(): + flash('Selected coach is not part of the selected team.', 'danger') + return redirect(url_for('users.one_on_one')) + try: date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() start_time = datetime.strptime(start_time_str, '%H:%M').time() @@ -759,14 +819,14 @@ def one_on_one(): flash('Invalid date or time format.', 'danger') return redirect(url_for('users.one_on_one')) - check_date = datetime.strptime(date_str, '%Y-%m-%d') - day_of_week = check_date.weekday() + day_of_week = date_obj.weekday() + availabilities = CoachAvailability.query.filter_by(coach_id=coach.id).all() is_available = any( - av['day_of_week'] == day_of_week - and av['start_time'] <= start_time_str - and av['end_time'] >= end_time_str - for av in coach_availability + av.day_of_week == day_of_week + and av.start_time <= start_time + and av.end_time >= end_time + for av in availabilities ) if not is_available: @@ -796,30 +856,13 @@ def one_on_one(): flash('Your One on One request has been submitted!', 'success') return redirect(url_for('users.one_on_one')) - # Build list of upcoming dates that have coach availability - from datetime import date as date_cls, timedelta as td - today = date_cls.today() - available_days = {av['day_of_week'] for av in coach_availability} - dates = [] - for i in range(14): # Next 14 days - d = today + td(days=i) - if d.weekday() in available_days: - dates.append({ - 'value': d.strftime('%Y-%m-%d'), - 'day_of_week': d.weekday(), - 'display': d.strftime('%B %d, %Y (%A)'), - }) - # Player's own One on One request history my_requests = OneOnOneRequest.query.filter_by( player_id=current_user.id ).order_by(OneOnOneRequest.created_at.desc()).all() return render_template('pages/one_on_one.html', - org_team=org_team, coach=coach, - team_notes=team_notes, personal_notes=personal_notes, - coach_availability=coach_availability, - dates=dates, + coach_options=coach_options, my_requests=my_requests) @@ -1031,12 +1074,13 @@ def notes_dashboard(): coach_id=current_user.id, ).order_by(PersonalNote.created_at.desc()).all() - # One on One requests from team players + # One on One requests addressed to this coach from their team players one_on_one_requests = [] if org_team and players: player_ids_list = [p.id for p in players] one_on_one_requests = OneOnOneRequest.query.filter( - OneOnOneRequest.player_id.in_(player_ids_list) + OneOnOneRequest.player_id.in_(player_ids_list), + OneOnOneRequest.coach_id == current_user.id, ).order_by(OneOnOneRequest.created_at.desc()).all() # For context selectors in the form diff --git a/app/supporting_scripts/backup.py b/app/supporting_scripts/backup.py index e9d1d77..f6b10d4 100644 --- a/app/supporting_scripts/backup.py +++ b/app/supporting_scripts/backup.py @@ -1,27 +1,61 @@ -"""Database backup script for the Team Tryouts application. +"""PostgreSQL database backup and restore helpers for the admin panel. -This module provides a simple backup mechanism for the SQLite database -and uploaded contract documents. Designed to be run as a scheduled task -(Windows Task Scheduler) or cron job. +Unlike the original SQLite-based script, this module works against the +PostgreSQL database configured via DATABASE_URL. It shells out to the +standard ``pg_dump`` / ``pg_restore`` / ``psql`` command-line utilities. -Usage: - python backup.py - Configuration via environment variables: BACKUP_DIR: Directory to store backups (default: ./backups) BACKUP_RETENTION_DAYS: Number of days to keep backups (default: 30) + DATABASE_URL: PostgreSQL connection string (required for connectivity) """ import os -import shutil -import sqlite3 +import re +import subprocess from datetime import datetime, timedelta +from urllib.parse import urlparse -# Configuration BACKUP_DIR = os.getenv('BACKUP_DIR', os.path.join(os.getcwd(), 'backups')) BACKUP_RETENTION_DAYS = int(os.getenv('BACKUP_RETENTION_DAYS', 30)) -DATABASE_PATH = os.getenv('DATABASE_PATH', os.path.join(os.getcwd(), 'instance', 'team_tryouts.db')) -DOCUMENTS_DIR = os.path.join(os.getcwd(), 'documents') + + +def _database_url(): + """Return the DATABASE_URL, normalizing to the psycopg2 SQLAlchemy form.""" + url = os.getenv('DATABASE_URL') + if not url: + raise RuntimeError('DATABASE_URL environment variable must be set') + # SQLAlchemy may prefix driver; strip it for command-line tools. + return re.sub(r'^postgresql\+[^:]+://', 'postgresql://', url) + + +def _pg_env(): + """Return the dict of PG* variables required by pg_dump / psql.""" + parsed = urlparse(_database_url()) + env = os.environ.copy() + # PGPASSWORD avoids interactive password prompts. + if parsed.password is not None: + env['PGPASSWORD'] = parsed.password + env.pop('PGHOST', None) + env.pop('PGPORT', None) + env.pop('PGUSER', None) + env.pop('PGDATABASE', None) + return env + + +def _pg_args(): + """Return [host, port, user, dbname] positional args for pg tools.""" + parsed = urlparse(_database_url()) + args = [] + if parsed.hostname: + args += ['--host', parsed.hostname] + if parsed.port: + args += ['--port', str(parsed.port)] + if parsed.username: + args += ['--username', parsed.username] + if parsed.path and parsed.path.lstrip('/'): + args += ['--dbname', parsed.path.lstrip('/')] + return args def create_backup_dir(): @@ -29,155 +63,113 @@ def create_backup_dir(): os.makedirs(BACKUP_DIR, exist_ok=True) -def backup_database(): - """Backup the SQLite database using sqlite3's built-in backup API. - +def create_backup(backup_type='manual', notes=None): + """Create a PostgreSQL dump backup. + Returns: - str: Path to the created backup file, or None if failed. + dict: {'filename', 'file_path', 'size_bytes'} or None on failure. """ - if not os.path.exists(DATABASE_PATH): - print(f'[WARNING] Database not found at {DATABASE_PATH}. Skipping database backup.') - return None - + create_backup_dir() timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') - backup_filename = f'db_backup_{timestamp}.db' - backup_path = os.path.join(BACKUP_DIR, backup_filename) + filename = f'db_backup_{backup_type}_{timestamp}.sql' + file_path = os.path.join(BACKUP_DIR, filename) + cmd = ['pg_dump'] + _pg_args() + ['--format', 'plain', '--no-owner', '--file', file_path] try: - source = sqlite3.connect(DATABASE_PATH) - destination = sqlite3.connect(backup_path) - source.backup(destination) - source.close() - destination.close() - print(f'[OK] Database backed up to: {backup_path}') - return backup_path - except Exception as e: - print(f'[ERROR] Database backup failed: {e}') - return None + result = subprocess.run(cmd, env=_pg_env(), capture_output=True, text=True) + if result.returncode != 0: + _cleanup_failed_file(file_path) + raise RuntimeError(result.stderr.strip() or 'pg_dump failed') + except FileNotFoundError: + _cleanup_failed_file(file_path) + raise RuntimeError('pg_dump executable not found on PATH') + except Exception: + _cleanup_failed_file(file_path) + raise + + if not os.path.exists(file_path): + raise RuntimeError('Backup file was not created') + + size = os.path.getsize(file_path) + return {'filename': filename, 'file_path': file_path, 'size_bytes': size} -def backup_documents(): - """Backup the uploaded contract documents directory. - - Returns: - str: Path to the created archive, or None if no documents exist. +def _cleanup_failed_file(file_path): + """Remove a partially-written dump file, ignoring errors.""" + if file_path and os.path.exists(file_path): + try: + os.remove(file_path) + except OSError: + pass + + +def restore_backup(file_path): + """Restore a PostgreSQL dump file using psql. + + Args: + file_path: Path to a plain-text .sql dump produced by pg_dump. + + Raises: + RuntimeError: If psql fails or is unavailable. """ - if not os.path.exists(DOCUMENTS_DIR): - print('[INFO] No documents directory found. Skipping document backup.') - return None - - timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') - archive_basename = f'documents_backup_{timestamp}' - archive_path = os.path.join(BACKUP_DIR, archive_basename) + if not file_path or not os.path.exists(file_path): + raise RuntimeError('Backup file does not exist') + # The dump was produced with --no-owner + plain format, so we can + # pipe it into psql. Use --set ON_ERROR_STOP=1 to fail fast. + cmd = ['psql'] + _pg_args() + ['--set', 'ON_ERROR_STOP=1', '--file', file_path] try: - shutil.make_archive(archive_path, 'zip', DOCUMENTS_DIR) - zip_path = f'{archive_path}.zip' - print(f'[OK] Documents backed up to: {zip_path}') - return zip_path - except Exception as e: - print(f'[ERROR] Document backup failed: {e}') - return None + result = subprocess.run(cmd, env=_pg_env(), capture_output=True, text=True) + if result.returncode != 0: + raise RuntimeError(result.stderr.strip() or 'psql restore failed') + except FileNotFoundError: + raise RuntimeError('psql executable not found on PATH') def cleanup_old_backups(): """Remove backup files older than BACKUP_RETENTION_DAYS.""" if not os.path.exists(BACKUP_DIR): - return + return 0 cutoff = datetime.now() - timedelta(days=BACKUP_RETENTION_DAYS) removed_count = 0 - for filename in os.listdir(BACKUP_DIR): file_path = os.path.join(BACKUP_DIR, filename) - if os.path.isfile(file_path): - file_time = datetime.fromtimestamp(os.path.getmtime(file_path)) - if file_time < cutoff: - try: - os.remove(file_path) - removed_count += 1 - print(f'[CLEANUP] Removed old backup: {filename}') - except OSError as e: - print(f'[WARNING] Could not remove {filename}: {e}') - - if removed_count > 0: - print(f'[CLEANUP] Removed {removed_count} old backup(s).') - else: - print('[CLEANUP] No old backups to remove.') - - -def verify_backup(backup_path): - """Verify a database backup by running a quick integrity check. - - Args: - backup_path: Path to the backup file to verify. - - Returns: - bool: True if backup is valid, False otherwise. - """ - if not backup_path or not os.path.exists(backup_path): - return False - - try: - conn = sqlite3.connect(backup_path) - cursor = conn.cursor() - cursor.execute('PRAGMA integrity_check') - result = cursor.fetchone() - conn.close() - is_valid = result[0] == 'ok' - if is_valid: - print(f'[OK] Backup integrity verified: {backup_path}') - else: - print(f'[ERROR] Backup integrity check failed: {backup_path} - {result[0]}') - return is_valid - except Exception as e: - print(f'[ERROR] Backup verification failed: {e}') - return False + if not os.path.isfile(file_path): + continue + file_time = datetime.fromtimestamp(os.path.getmtime(file_path)) + if file_time < cutoff: + try: + os.remove(file_path) + removed_count += 1 + except OSError: + pass + return removed_count def main(): - """Run the full backup process. - - Steps: - 1. Create backup directory - 2. Backup database - 3. Backup documents (if any) - 4. Verify database backup - 5. Clean up old backups - - Returns: - int: 0 on success, 1 on failure. - """ - print(f'=== Team Tryouts Backup ===') + """Run a manual backup from the CLI (for cron/Task Scheduler).""" + print('=== Team Tryouts PostgreSQL Backup ===') print(f'Started at: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}') print(f'Backup directory: {BACKUP_DIR}') print(f'Retention period: {BACKUP_RETENTION_DAYS} days') print() - create_backup_dir() + try: + result = create_backup(backup_type='scheduled') + except RuntimeError as e: + print(f'[ERROR] {e}') + return 1 - # 1. Backup database - db_backup_path = backup_database() - success = True + print(f'[OK] Database backed up to: {result["file_path"]}') - # 2. Verify database backup - if db_backup_path: - if not verify_backup(db_backup_path): - success = False - - # 3. Backup documents - backup_documents() - - # 4. Cleanup old backups - cleanup_old_backups() + removed = cleanup_old_backups() + if removed: + print(f'[CLEANUP] Removed {removed} old backup(s).') print() - if success: - print('=== Backup completed successfully ===') - else: - print('=== Backup completed with warnings ===') - - return 0 if success else 1 + print('=== Backup completed successfully ===') + return 0 if __name__ == '__main__': diff --git a/app/templates/layouts/base.html b/app/templates/layouts/base.html index 8961f03..7ec564b 100644 --- a/app/templates/layouts/base.html +++ b/app/templates/layouts/base.html @@ -75,6 +75,12 @@ Manage Users +
  • + + + Admin Panel + +
  • {% endif %} {% if current_user.role == 'player' %}
  • diff --git a/app/templates/pages/calendar.html b/app/templates/pages/calendar.html index b984ad0..2516e9e 100644 --- a/app/templates/pages/calendar.html +++ b/app/templates/pages/calendar.html @@ -239,54 +239,79 @@ function showEventModal(event) { var date = event.start ? event.start.toDateString() : ''; var content = '
    '; - content += '
    Type'; - content += ''; - content += (props.match_type === 'team_vs_team' ? 'Team Match' : (props.match_type === 'player_vs_player' ? 'Player Match' : 'Player Scrim')) + ''; - content += '
    '; - content += '
    Title' + title + '
    '; - content += '
    Date' + date + '
    '; - content += '
    Location' + (props.location || 'TBD') + '
    '; - content += '
    Status'; - content += '' + (props.status || 'scheduled') + ''; - content += '
    '; - // Add team separation for matches - if (type === 'match' && props.participants) { - content += '
    Teams'; - if (props.match_type === 'team_vs_team') { - var teams = props.participants.split(' vs '); - if (teams.length >= 2) { - content += '
    '; - content += '
    ' + teams[0] + '
    '; - content += '
    vs
    '; - content += '
    ' + teams[1] + '
    '; - content += '
    '; - } else { - content += props.participants; - } - } else if (props.match_type === 'player_vs_player') { - var parts = props.participants.split(' vs '); - if (parts.length >= 2) { - content += '
    '; - content += '
    Team 1
    • ' + parts[0].split(', ').join('
    • ') + '
    '; - content += '
    vs
    '; - content += '
    Team 2
    • ' + parts[1].split(', ').join('
    • ') + '
    '; - content += '
    '; - } else { - content += props.participants; - } - } else { - content += props.participants; - } + if (type === 'one_on_one') { + // One on One session modal + var startTime = props.start_time ? formatTime(props.start_time) : null; + var endTime = props.end_time ? formatTime(props.end_time) : null; + var timeRange = startTime ? startTime + (endTime ? ' - ' + endTime : '') : 'TBD'; + + content += '
    Type'; + content += 'One on One Session'; content += '
    '; + content += '
    Title' + title + '
    '; + content += '
    Date' + date + '
    '; + content += '
    Time' + timeRange + '
    '; + content += '
    Location' + (props.location || 'TBD') + '
    '; + content += '
    Participants' + (props.participants || '') + '
    '; + content += '
    Status'; + content += 'Approved'; + content += '
    '; + if (props.description) { + content += '
    Discussion Points' + props.description + '
    '; + } + } else { + // Match modal (existing behavior) + content += '
    Type'; + content += ''; + content += (props.match_type === 'team_vs_team' ? 'Team Match' : (props.match_type === 'player_vs_player' ? 'Player Match' : 'Player Scrim')) + ''; + content += '
    '; + content += '
    Title' + title + '
    '; + content += '
    Date' + date + '
    '; + content += '
    Location' + (props.location || 'TBD') + '
    '; + content += '
    Status'; + content += '' + (props.status || 'scheduled') + ''; + content += '
    '; + + // Add team separation for matches + if (props.participants) { + content += '
    Teams'; + if (props.match_type === 'team_vs_team') { + var teams = props.participants.split(' vs '); + if (teams.length >= 2) { + content += '
    '; + content += '
    ' + teams[0] + '
    '; + content += '
    vs
    '; + content += '
    ' + teams[1] + '
    '; + content += '
    '; + } else { + content += props.participants; + } + } else if (props.match_type === 'player_vs_player') { + var parts = props.participants.split(' vs '); + if (parts.length >= 2) { + content += '
    '; + content += '
    Team 1
    • ' + parts[0].split(', ').join('
    • ') + '
    '; + content += '
    vs
    '; + content += '
    Team 2
    • ' + parts[1].split(', ').join('
    • ') + '
    '; + content += '
    '; + } else { + content += props.participants; + } + } else { + content += props.participants; + } + content += '
    '; + } + + if (props.description) { + content += '
    Description' + props.description + '
    '; + } } - if (props.description) { - content += '
    Description' + props.description + '
    '; - } content += '
    '; - document.getElementById('modalTitle').textContent = 'Match Details'; + document.getElementById('modalTitle').textContent = (type === 'one_on_one' ? 'One on One Session' : 'Match Details'); document.getElementById('modalContent').innerHTML = content; // Reset buttons @@ -294,6 +319,14 @@ function showEventModal(event) { document.getElementById('editMatchBtn').style.display = 'none'; document.getElementById('viewTryoutBtn').style.display = 'none'; + // Hide match actions and presence toggle for one-on-one events + if (type === 'one_on_one') { + document.getElementById('modalActions').style.display = 'none'; + document.getElementById('modalPresenceToggle').style.display = 'none'; + document.getElementById('eventModal').classList.remove('hidden'); + return; + } + // Show action buttons for matches (coaches and above) if (type === 'match' && canScheduleMatches) { document.getElementById('modalActions').style.display = 'flex'; @@ -329,6 +362,17 @@ function showEventModal(event) { document.getElementById('eventModal').classList.remove('hidden'); } +function formatTime(timeStr) { + if (!timeStr) return null; + var parts = timeStr.split(':'); + var h = parseInt(parts[0]); + var m = parts[1]; + var ampm = h >= 12 ? 'PM' : 'AM'; + var displayHour = h % 12; + if (displayHour === 0) displayHour = 12; + return displayHour + ':' + m + ' ' + ampm; +} + function deleteCalendarMatch(matchId) { fetch('/matches/' + matchId + '/delete', { method: 'POST', diff --git a/app/templates/pages/one_on_one.html b/app/templates/pages/one_on_one.html index 177c3ba..08180b1 100644 --- a/app/templates/pages/one_on_one.html +++ b/app/templates/pages/one_on_one.html @@ -4,60 +4,8 @@ {% block breadcrumb %}Home / One on One{% endblock %} {% block content %} -
    - -
    -
    -

    Team Notes

    - {% if org_team %} - {{ org_team.name }} - {% endif %} -
    -
    - {% if team_notes %} - {% for note in team_notes %} -
    -
    - Coach: {{ note.coach.username if note.coach else 'Unknown Coach' }} - {{ note.content | nl2br }} -
    -
    -

    Updated: {{ note.updated_at.strftime('%B %d, %Y at %I:%M %p') }}

    - {% endfor %} - {% else %} -

    No team notes have been added yet. Your coach will post improvement suggestions here.

    - {% endif %} -
    -
    - - -
    -
    -

    Personal Notes

    - {% if coach %} - From: {{ coach.username }} - {% endif %} -
    -
    - {% if personal_notes %} - {% for note in personal_notes %} -
    -
    - Note from {{ note.coach.username if note.coach else 'Unknown Coach' }} - {{ note.content | nl2br }} -
    -
    -

    Added: {{ note.created_at.strftime('%B %d, %Y at %I:%M %p') }}

    - {% endfor %} - {% else %} -

    No personal notes have been added yet. Your coach may provide individual feedback here.

    - {% endif %} -
    -
    -
    - -
    +

    My One on One Requests

    @@ -67,6 +15,8 @@ + + @@ -77,6 +27,8 @@ {% for req in my_requests %} + + @@ -113,71 +65,78 @@ -
    - -
    -
    -

    Request One on One Session

    - {% if coach %} - Coach: {{ coach.username }} - {% endif %} -
    -
    - {% if coach %} -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    + +
    +
    +

    Request One on One Session

    +
    +
    + {% if coach_options %} + + +
    +
    + + +
    +
    + + +
    +
    + +
    - - + +
    +
    + + +
    +
    + + +
    +
    -
    - -
    - - {% else %} -

    You need to be assigned to a team with a coach to request a One on One session.

    - {% endif %} -
    +
    + + +
    + +
    + +
    + + {% else %} +

    You need to be assigned to a team with a coach to request a One on One session.

    + {% endif %}
    - -{% if coach %} - - -{% endif %} {% endblock %} {% block scripts %} {% endblock %} \ No newline at end of file
    CoachTeam Date Time Discussion Points
    {{ req.coach.full_name if req.coach else 'Unknown Coach' }}{{ req.team.name if req.team else '—' }} {{ req.date.strftime('%b %d, %Y') }} {{ req.start_time.strftime('%I:%M %p') }} - {{ req.end_time.strftime('%I:%M %p') }} {{ req.points or 'N/A' }}