diff --git a/.gitgnore b/.gitgnore deleted file mode 100644 index 4f509e5..0000000 --- a/.gitgnore +++ /dev/null @@ -1 +0,0 @@ -*.env \ No newline at end of file diff --git a/.gitignore b/.gitignore index c38aae2..a97b897 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ instance/ documents/ __pycache__/ +*.cpython-313.pyc +*.cpython-312.pyc *.pyc .pytest_cache/ diff --git a/__pycache__/extensions.cpython-312.pyc b/__pycache__/extensions.cpython-312.pyc index 823d716..52b4558 100644 Binary files a/__pycache__/extensions.cpython-312.pyc and b/__pycache__/extensions.cpython-312.pyc differ diff --git a/__pycache__/models.cpython-312.pyc b/__pycache__/models.cpython-312.pyc index 4b21baa..cea274c 100644 Binary files a/__pycache__/models.cpython-312.pyc and b/__pycache__/models.cpython-312.pyc differ diff --git a/__pycache__/seed.cpython-312.pyc b/__pycache__/seed.cpython-312.pyc index 7561a58..f1b1a9f 100644 Binary files a/__pycache__/seed.cpython-312.pyc and b/__pycache__/seed.cpython-312.pyc differ diff --git a/instance/team_tryouts.db b/instance/team_tryouts.db index bb6b999..c634599 100644 Binary files a/instance/team_tryouts.db and b/instance/team_tryouts.db differ diff --git a/models.py b/models.py index e076b32..5e6334d 100644 --- a/models.py +++ b/models.py @@ -636,6 +636,7 @@ class MatchParticipant(db.Model): player_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) team_side = db.Column(db.Integer, nullable=True) # 1 for team 1, 2 for team 2 (for player_vs_player matches) position = db.Column(db.String(50), nullable=True) # Position for this match + attendance_confirmed = db.Column(db.Boolean, default=False) # Whether the player confirmed via Discord or manual toggle added_at = db.Column(db.DateTime, default=datetime.utcnow) player = db.relationship('User') diff --git a/routes/__pycache__/auth.cpython-312.pyc b/routes/__pycache__/auth.cpython-312.pyc index ad1c4d8..6b58423 100644 Binary files a/routes/__pycache__/auth.cpython-312.pyc and b/routes/__pycache__/auth.cpython-312.pyc differ diff --git a/routes/__pycache__/evaluations.cpython-312.pyc b/routes/__pycache__/evaluations.cpython-312.pyc index 4c53e91..5fbe3cd 100644 Binary files a/routes/__pycache__/evaluations.cpython-312.pyc and b/routes/__pycache__/evaluations.cpython-312.pyc differ diff --git a/routes/__pycache__/main.cpython-312.pyc b/routes/__pycache__/main.cpython-312.pyc index 23d733c..521d863 100644 Binary files a/routes/__pycache__/main.cpython-312.pyc and b/routes/__pycache__/main.cpython-312.pyc differ diff --git a/routes/__pycache__/matches.cpython-312.pyc b/routes/__pycache__/matches.cpython-312.pyc index 1f27da0..e434465 100644 Binary files a/routes/__pycache__/matches.cpython-312.pyc and b/routes/__pycache__/matches.cpython-312.pyc differ diff --git a/routes/__pycache__/teams.cpython-312.pyc b/routes/__pycache__/teams.cpython-312.pyc index b9a27bd..53eb1c9 100644 Binary files a/routes/__pycache__/teams.cpython-312.pyc and b/routes/__pycache__/teams.cpython-312.pyc differ diff --git a/routes/__pycache__/tryouts.cpython-312.pyc b/routes/__pycache__/tryouts.cpython-312.pyc index 958c08e..8c94770 100644 Binary files a/routes/__pycache__/tryouts.cpython-312.pyc and b/routes/__pycache__/tryouts.cpython-312.pyc differ diff --git a/routes/__pycache__/users.cpython-312.pyc b/routes/__pycache__/users.cpython-312.pyc index 5d58f5b..fc9fe61 100644 Binary files a/routes/__pycache__/users.cpython-312.pyc and b/routes/__pycache__/users.cpython-312.pyc differ diff --git a/routes/main.py b/routes/main.py index 43ee72c..c160724 100644 --- a/routes/main.py +++ b/routes/main.py @@ -6,7 +6,7 @@ This module provides the main dashboard view with role-specific statistics. from flask import Blueprint, render_template, redirect, url_for, flash from flask_login import login_required, current_user from extensions import db -from models import User, Tryout, Evaluation, TryoutRegistration, Team, TeamMember, Match, MatchParticipant +from models import User, Tryout, Evaluation, TryoutRegistration, Team, TeamMember, Match, MatchParticipant, OrgTeam from sqlalchemy import func from datetime import datetime, date diff --git a/routes/matches.py b/routes/matches.py index 04131aa..dcdc51d 100644 --- a/routes/matches.py +++ b/routes/matches.py @@ -356,11 +356,22 @@ def create_match(tryout_id): team2_id = request.form.get('team2_id') match.team1_id = int(team1_id) if team1_id else None match.team2_id = int(team2_id) if team2_id else None - # Get players from both teams + # Create MatchParticipant records for all team members AND get notified player IDs + notified_participant_ids = [] if match.team1_id: - notified_player_ids.extend([m.player_id for m in TeamMember.query.filter_by(team_id=match.team1_id).all()]) + for m in TeamMember.query.filter_by(team_id=match.team1_id).all(): + participant = MatchParticipant(match_id=match.id, player_id=m.player_id, team_side=1) + db.session.add(participant) + db.session.flush() + notified_participant_ids.append(participant.id) + notified_player_ids.append(m.player_id) if match.team2_id: - notified_player_ids.extend([m.player_id for m in TeamMember.query.filter_by(team_id=match.team2_id).all()]) + for m in TeamMember.query.filter_by(team_id=match.team2_id).all(): + participant = MatchParticipant(match_id=match.id, player_id=m.player_id, team_side=2) + db.session.add(participant) + db.session.flush() + notified_participant_ids.append(participant.id) + notified_player_ids.append(m.player_id) # Handle player vs player matches elif match_type == 'player_vs_player': @@ -398,17 +409,17 @@ def create_match(tryout_id): event_date_str = date_obj.strftime('%Y-%m-%d') event_time_str = f"{start_time.strftime('%I:%M %p')} - {end_time.strftime('%I:%M %p')}" if start_time and end_time else 'TBD' - # For team_vs_team, notify all players but use match.id (no individual confirmation) - # For player_vs_player/player_scrim, notify each player individually with participant ID + # Send Discord notifications with proper participant reference IDs if match_type == 'team_vs_team': - for player_id in notified_player_ids: + for i, player_id in enumerate(notified_player_ids): + reference_id = notified_participant_ids[i] if i < len(notified_participant_ids) else match.id send_schedule_notification( user_id=player_id, event_type='match', event_title=match.title, event_date=event_date_str, event_time=event_time_str, - reference_id=match.id + reference_id=reference_id ) else: for i, player_id in enumerate(notified_player_ids): @@ -500,16 +511,38 @@ def edit_match(match_id): notified_player_ids = [] # Handle team vs team matches + notified_participant_ids = [] if match.match_type == 'team_vs_team': team1_id = request.form.get('team1_id') team2_id = request.form.get('team2_id') - match.team1_id = int(team1_id) if team1_id else None - match.team2_id = int(team2_id) if team2_id else None - # Get players from both teams - if match.team1_id: - notified_player_ids.extend([m.player_id for m in TeamMember.query.filter_by(team_id=match.team1_id).all()]) - if match.team2_id: - notified_player_ids.extend([m.player_id for m in TeamMember.query.filter_by(team_id=match.team2_id).all()]) + new_team1_id = int(team1_id) if team1_id else None + new_team2_id = int(team2_id) if team2_id else None + + # If teams changed, recreate MatchParticipant records + if new_team1_id != match.team1_id or new_team2_id != match.team2_id: + MatchParticipant.query.filter_by(match_id=match.id).delete() + match.team1_id = new_team1_id + match.team2_id = new_team2_id + if match.team1_id: + for m in TeamMember.query.filter_by(team_id=match.team1_id).all(): + participant = MatchParticipant(match_id=match.id, player_id=m.player_id, team_side=1) + db.session.add(participant) + db.session.flush() + notified_participant_ids.append(participant.id) + notified_player_ids.append(m.player_id) + if match.team2_id: + for m in TeamMember.query.filter_by(team_id=match.team2_id).all(): + participant = MatchParticipant(match_id=match.id, player_id=m.player_id, team_side=2) + db.session.add(participant) + db.session.flush() + notified_participant_ids.append(participant.id) + notified_player_ids.append(m.player_id) + else: + # Teams didn't change, still get notified player IDs + if match.team1_id: + notified_player_ids.extend([m.player_id for m in TeamMember.query.filter_by(team_id=match.team1_id).all()]) + if match.team2_id: + notified_player_ids.extend([m.player_id for m in TeamMember.query.filter_by(team_id=match.team2_id).all()]) # Handle player vs player matches - update participants elif match.match_type == 'player_vs_player': @@ -565,20 +598,44 @@ def edit_match(match_id): event_date_str = match.date.strftime('%Y-%m-%d') end_time_val = match.end_time if match.end_time else match.start_time if match.start_time else None event_time_str = f"{match.start_time.strftime('%I:%M %p')} - {end_time_val.strftime('%I:%M %p')}" if match.start_time and end_time_val else 'TBD' - for player_id in notified_player_ids: - send_schedule_notification( - user_id=player_id, - event_type='match', - event_title=match.title, - event_date=event_date_str, - event_time=event_time_str, - reference_id=match.id - ) + if notified_participant_ids: + for i, player_id in enumerate(notified_player_ids): + reference_id = notified_participant_ids[i] if i < len(notified_participant_ids) else match.id + send_schedule_notification( + user_id=player_id, + event_type='match', + event_title=match.title, + event_date=event_date_str, + event_time=event_time_str, + reference_id=reference_id + ) + else: + for player_id in notified_player_ids: + send_schedule_notification( + user_id=player_id, + event_type='match', + event_title=match.title, + event_date=event_date_str, + event_time=event_time_str, + reference_id=match.id + ) flash('Match updated successfully!', 'success') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id)) - return render_template('pages/match_form.html', match=match, tryout=tryout, teams=teams, all_players=all_players, current_player_ids=current_player_ids, team1_player_ids=team1_player_ids, team2_player_ids=team2_player_ids) + # Build participant attendance map for the template + participants_map = {} + for p in match.participants.all(): + participants_map[p.player_id] = { + 'participant_id': p.id, + 'attendance_confirmed': p.attendance_confirmed, + 'team_side': p.team_side + } + + return render_template('pages/match_form.html', match=match, tryout=tryout, teams=teams, + all_players=all_players, current_player_ids=current_player_ids, + team1_player_ids=team1_player_ids, team2_player_ids=team2_player_ids, + participants_map=participants_map) @matches_bp.route('//delete', methods=['POST']) @@ -673,4 +730,38 @@ def api_available_players(date, time): return jsonify({'error': 'Unauthorized'}), 403 player_ids = get_players_available_at_time(date, time) - return jsonify({'available_player_ids': player_ids}) \ No newline at end of file + return jsonify({'available_player_ids': player_ids}) + + +@matches_bp.route('//toggle-presence/', methods=['POST']) +@login_required +def toggle_presence(match_id, participant_id): + """Toggle the attendance_confirmed status for a match participant. + + Accessible only to users who can manage the tryout. + + Args: + match_id: The ID of the match. + participant_id: The ID of the MatchParticipant record. + + Returns: + Response: JSON with new status. + """ + match = Match.query.get_or_404(match_id) + tryout = match.tryout + + if not current_user.can_manage_this_tryout(tryout): + return jsonify({'error': 'Unauthorized'}), 403 + + participant = MatchParticipant.query.get_or_404(participant_id) + if participant.match_id != match_id: + return jsonify({'error': 'Participant does not belong to this match'}), 400 + + participant.attendance_confirmed = not participant.attendance_confirmed + db.session.commit() + + return jsonify({ + 'participant_id': participant.id, + 'attendance_confirmed': participant.attendance_confirmed, + 'player_name': participant.player.username if participant.player else 'Unknown' + }) diff --git a/routes/tryouts.py b/routes/tryouts.py index e5e0723..a3d207c 100644 --- a/routes/tryouts.py +++ b/routes/tryouts.py @@ -271,6 +271,11 @@ def view_tryout(tryout_id): matches = Match.query.filter_by(tryout_id=tryout_id).order_by(Match.date, Match.start_time).all() match_data = [] for match in matches: + # Calculate presence stats + all_participants = list(match.participants.all()) + confirmed_count = sum(1 for p in all_participants if p.attendance_confirmed) + total_count = len(all_participants) + if match.match_type == 'team_vs_team': participants = { 'team1': match.team1.name if match.team1 else 'TBD', @@ -289,9 +294,12 @@ def view_tryout(tryout_id): } else: participants = [p.player.username for p in match.participants.all()] + match_data.append({ 'match': match, - 'participants': participants + 'participants': participants, + 'confirmed_count': confirmed_count, + 'total_count': total_count }) return render_template('pages/view_tryout.html', diff --git a/templates/pages/match_form.html b/templates/pages/match_form.html index c91a832..8cdae1b 100644 --- a/templates/pages/match_form.html +++ b/templates/pages/match_form.html @@ -141,7 +141,19 @@
{{ match.team1.name }}
    {% for member in match.team1.members %} -
  • {{ member.player.username if member.player else 'Unknown Player' }}{% if member.position %} {{ member.position }}{% endif %}
  • +
  • + {{ member.player.username if member.player else 'Unknown Player' }}{% if member.position %} {{ member.position }}{% endif %} + {% set pdata = participants_map.get(member.player_id) %} + {% if pdata %} + + {{ '✅ Confirmed' if pdata.attendance_confirmed else '⏳ Pending' }} + + {% endif %} +
  • {% else %}
  • No players assigned
  • {% endfor %} @@ -155,7 +167,19 @@
    {{ match.team2.name }}
      {% for member in match.team2.members %} -
    • {{ member.player.username if member.player else 'Unknown Player' }}{% if member.position %} {{ member.position }}{% endif %}
    • +
    • + {{ member.player.username if member.player else 'Unknown Player' }}{% if member.position %} {{ member.position }}{% endif %} + {% set pdata = participants_map.get(member.player_id) %} + {% if pdata %} + + {{ '✅ Confirmed' if pdata.attendance_confirmed else '⏳ Pending' }} + + {% endif %} +
    • {% else %}
    • No players assigned
    • {% endfor %} @@ -382,6 +406,38 @@ .randomize-btn:hover { background: var(--primary-dark); } + +/* Presence badge styles */ +.presence-badge { + display: inline-block; + padding: 2px 8px; + border-radius: 12px; + font-size: 12px; + cursor: pointer; + margin-left: 8px; + transition: all 0.2s ease; + user-select: none; +} +.presence-badge:hover { + transform: scale(1.05); + box-shadow: 0 1px 3px rgba(0,0,0,0.2); +} +.presence-confirmed { + background: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; +} +.presence-pending { + background: #fff3cd; + color: #856404; + border: 1px solid #ffeeba; +} +.team-members-list li { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 4px; +} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/pages/view_tryout.html b/templates/pages/view_tryout.html index 090092a..66e57e6 100644 --- a/templates/pages/view_tryout.html +++ b/templates/pages/view_tryout.html @@ -278,11 +278,12 @@ - + + {% if can_edit %} @@ -307,6 +308,28 @@ + + -
      MatchMatch Type Date Participants TimePresence StatusActions {{ m.date.strftime('%m/%d/%Y') }} + {% if m.start_time and m.end_time %} + {{ m.start_time.strftime('%H:%M') }} - {{ m.end_time.strftime('%H:%M') }} + {% else %} + TBD + {% endif %} + + {% if item.total_count > 0 %} + + {% if item.confirmed_count == item.total_count and item.total_count > 0 %} + ✅ {{ item.confirmed_count }}/{{ item.total_count }} + {% elif item.confirmed_count > 0 %} + ✅ {{ item.confirmed_count }}/{{ item.total_count }} + {% else %} + ⏳ 0/{{ item.total_count }} + {% endif %} + + {% else %} + + {% endif %} + {% if m.match_type == 'team_vs_team' %}
      @@ -360,13 +383,6 @@ {{ participants | join(', ') }} {% endif %}
      - {% if m.start_time and m.end_time %} - {{ m.start_time.strftime('%H:%M') }} - {{ m.end_time.strftime('%H:%M') }} - {% else %} - TBD - {% endif %} - {{ m.status }}