ajout de match régulier pour les équipes et de pratiques

Ajout d'un profil public cliquable pour les utilisateurs
déplacement du profil
This commit is contained in:
cedrick2711
2026-07-28 12:39:53 -04:00
parent 69b6e217ae
commit b982cd0318
20 changed files with 2132 additions and 225 deletions
+45 -7
View File
@@ -91,6 +91,12 @@ def api_events():
start_time_str = match.start_time.strftime('%H:%M') if match.start_time else None
end_time_str = match.end_time.strftime('%H:%M') if match.end_time else None
# Find current user's participant record for presence toggle
user_participant = MatchParticipant.query.filter_by(
match_id=match.id,
player_id=current_user.id
).first()
events.append({
'id': f'match_{match.id}',
'title': match.title,
@@ -106,7 +112,9 @@ def api_events():
'match_id': match.id,
'start_time': start_time_str,
'end_time': end_time_str,
'participants': participants_str
'participants': participants_str,
'user_participant_id': user_participant.id if user_participant else None,
'user_attendance_confirmed': user_participant.attendance_confirmed if user_participant else False
}
})
@@ -297,6 +305,9 @@ def create_match(tryout_id):
all_players = [User.query.get(r.player_id) for r in registrations if User.query.get(r.player_id)]
all_players = sorted([p for p in all_players if p], key=lambda x: x.username)
# Allow pre-filling the date from query param (e.g., from calendar click)
prefill_date = request.args.get('date', '')
if request.method == 'POST':
title = request.form.get('title')
description = request.form.get('description')
@@ -309,13 +320,13 @@ def create_match(tryout_id):
# Start time is now mandatory
if not start_time_str:
flash('Start time is required. Please select a time slot.', 'danger')
return render_template('pages/match_form.html', tryout=tryout, teams=teams, all_players=all_players)
return render_template('pages/match_form.html', tryout=tryout, teams=teams, all_players=all_players, prefill_date=prefill_date)
try:
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() if date_str else tryout.date
except (ValueError, TypeError):
flash('Invalid date format.', 'danger')
return render_template('pages/match_form.html', tryout=tryout, teams=teams, all_players=all_players)
return render_template('pages/match_form.html', tryout=tryout, teams=teams, all_players=all_players, prefill_date=prefill_date)
start_time = None
end_time = None
@@ -638,6 +649,31 @@ def edit_match(match_id):
participants_map=participants_map)
@matches_bp.route('/api/manageable-tryouts')
@login_required
def api_manageable_tryouts():
"""API endpoint returning tryouts the current user can manage.
Used by the calendar's "Create Event" modal to populate the tryout dropdown.
Returns:
Response: JSON array of {id, title, date}.
"""
if not can_schedule_match():
return jsonify([])
tryouts = get_visible_tryouts_for_user()
manageable = []
for t in tryouts:
if current_user.can_manage_this_tryout(t):
manageable.append({
'id': t.id,
'title': t.title,
'date': t.date.strftime('%Y-%m-%d')
})
return jsonify(manageable)
@matches_bp.route('/<int:match_id>/delete', methods=['POST'])
@login_required
def delete_match(match_id):
@@ -738,7 +774,7 @@ def api_available_players(date, time):
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.
Accessible to tryout managers AND the participant themselves.
Args:
match_id: The ID of the match.
@@ -750,13 +786,15 @@ def toggle_presence(match_id, participant_id):
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
# Allow the participant themselves OR a tryout manager
is_self = participant.player_id == current_user.id
if not is_self and not current_user.can_manage_this_tryout(tryout):
return jsonify({'error': 'Unauthorized'}), 403
participant.attendance_confirmed = not participant.attendance_confirmed
db.session.commit()
+388
View File
@@ -0,0 +1,388 @@
"""Team match management routes for regular season matches.
This module handles CRUD operations for team-specific matches that are
not tied to tryouts. Players are pre-filled from the team roster.
"""
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify
from flask_login import login_required, current_user
from extensions import db
from models import OrgTeam, User, TeamMatch, TeamMatchParticipant, TeamPlayer
from datetime import datetime, timedelta
from discord_bot import send_schedule_notification
team_matches_bp = Blueprint('team_matches', __name__, url_prefix='/team-matches')
def can_manage_team_match(team):
"""Check if current user can manage matches for this team.
Returns:
bool: True if user is president, manager, or a coach of this team.
"""
if current_user.role in ['president']:
return True
if current_user.role == 'manager':
return True
if current_user.role == 'coach':
if team.coaches.filter_by(id=current_user.id).first():
return True
if team.coach_id == current_user.id:
return True
return False
@team_matches_bp.route('')
@login_required
def list_matches():
"""List all team matches visible to the current user.
Supports optional ?team_id= query param to pre-filter by team.
Returns:
Response: Rendered team matches list template.
"""
# Optional pre-filter by team_id from query param
filter_team_id = request.args.get('team_id', type=int)
if current_user.role == 'president':
teams = OrgTeam.query.order_by(OrgTeam.name).all()
matches_query = TeamMatch.query
elif current_user.role == 'manager':
teams = OrgTeam.query.order_by(OrgTeam.name).all()
matches_query = TeamMatch.query
elif current_user.role == 'coach':
teams = OrgTeam.query.filter(
db.or_(
OrgTeam.coaches.any(id=current_user.id),
OrgTeam.coach_id == current_user.id
)
).order_by(OrgTeam.name).all()
team_ids = [t.id for t in teams]
matches_query = TeamMatch.query.filter(
TeamMatch.org_team_id.in_(team_ids)
) if team_ids else TeamMatch.query.filter(TeamMatch.id == -1)
elif current_user.role == 'player':
player_team_ids = [tp.org_team_id for tp in current_user.team_placements]
teams = OrgTeam.query.filter(OrgTeam.id.in_(player_team_ids)).all() if player_team_ids else []
matches_query = TeamMatch.query.filter(
TeamMatch.org_team_id.in_(player_team_ids)
) if player_team_ids else TeamMatch.query.filter(TeamMatch.id == -1)
else:
teams = []
matches_query = TeamMatch.query.filter(TeamMatch.id == -1)
# Apply team_id filter if provided
if filter_team_id:
matches_query = matches_query.filter(TeamMatch.org_team_id == filter_team_id)
matches = matches_query.order_by(TeamMatch.date.desc()).all()
# Build participants map for each match
match_data = []
for tm in matches:
confirmed, total = tm.get_confirmed_count()
participants = []
for p in tm.participants.all():
participants.append({
'id': p.id,
'player': p.player,
'is_confirmed': p.is_confirmed
})
match_data.append({
'match': tm,
'participants': participants,
'confirmed_count': confirmed,
'total_count': total
})
return render_template('pages/team_matches.html',
teams=teams,
match_data=match_data,
now=datetime.utcnow())
@team_matches_bp.route('/<int:team_id>/create', methods=['GET', 'POST'])
@login_required
def create_match(team_id):
"""Create a new team match (regular season).
GET: Render the match creation form with pre-filled team roster.
POST: Create the match with all team players as participants.
Args:
team_id: The ID of the org team to create a match for.
Returns:
Response: Create form or redirect to team matches list.
"""
team = OrgTeam.query.get_or_404(team_id)
if not can_manage_team_match(team):
flash('You do not have permission to schedule matches for this team.', '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()]
# Allow pre-filling the date from query param (e.g., from calendar click)
prefill_date = request.args.get('date', '')
# Check if this is a practice (no opponent)
is_practice = request.args.get('type') == 'practice'
default_title = 'Practice' if is_practice else f'Team Match — {team.name}'
# For practices, render the tryout match form (with availability calendar)
if is_practice and request.method == 'GET':
# Build a lightweight proxy for the tryout object the template expects
class TryoutProxy:
def __init__(self, team_obj):
self.id = 0
self.title = team_obj.name
self.date = ''
self.game = ''
self.target_org_team = team_obj
proxy_tryout = TryoutProxy(team)
all_players = [tp.player for tp in team_players if tp.player]
return render_template('pages/match_form.html',
tryout=proxy_tryout,
teams=[],
all_players=all_players,
prefill_date=prefill_date,
is_practice=True,
team_id=team_id,
team=team)
if request.method == 'POST':
title = request.form.get('title', default_title)
opponent = request.form.get('opponent', '').strip() if not is_practice else None
description = request.form.get('description', '')
date_str = request.form.get('date')
start_time_str = request.form.get('start_time')
end_time_str = request.form.get('end_time')
location = request.form.get('location', '')
if not date_str:
flash('Date is required.', 'danger')
return render_template('pages/team_match_form.html', team=team, team_players=team_players, prefill_date=prefill_date)
try:
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
except (ValueError, TypeError):
flash('Invalid date format.', 'danger')
return render_template('pages/team_match_form.html', team=team, team_players=team_players, prefill_date=prefill_date, is_practice=is_practice)
start_time = None
end_time = None
if start_time_str:
try:
start_time = datetime.strptime(start_time_str, '%H:%M').time()
if end_time_str:
end_time = datetime.strptime(end_time_str, '%H:%M').time()
else:
start_dt = datetime.combine(date_obj, start_time)
end_dt = start_dt + timedelta(minutes=30)
end_time = end_dt.time()
except ValueError:
flash('Invalid time format.', 'danger')
return render_template('pages/team_match_form.html', team=team, team_players=team_players, prefill_date=prefill_date, is_practice=is_practice)
team_match = TeamMatch(
org_team_id=team_id,
title=title,
description=description or None,
opponent=opponent or None,
date=date_obj,
start_time=start_time,
end_time=end_time,
location=location or None,
created_by=current_user.id
)
db.session.add(team_match)
db.session.flush() # Get team_match.id
# Auto-add all team players as participants
notified_participant_ids = []
for tp in team_players:
participant = TeamMatchParticipant(
team_match_id=team_match.id,
player_id=tp.player_id
)
db.session.add(participant)
db.session.flush()
notified_participant_ids.append(participant.id)
db.session.commit()
# Send Discord notifications to players
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 i, tp in enumerate(team_players):
reference_id = notified_participant_ids[i] if i < len(notified_participant_ids) else team_match.id
send_schedule_notification(
user_id=tp.player_id,
event_type='match',
event_title=team_match.title,
event_date=event_date_str,
event_time=event_time_str,
reference_id=reference_id
)
flash(f'Team match "{title}" scheduled successfully!', 'success')
return redirect(url_for('team_matches.list_matches'))
return render_template('pages/team_match_form.html', team=team, team_players=team_players)
@team_matches_bp.route('/<int:match_id>/edit', methods=['GET', 'POST'])
@login_required
def edit_match(match_id):
"""Edit an existing team match.
GET: Render the edit form.
POST: Update match details.
Args:
match_id: The ID of the team match to edit.
Returns:
Response: Edit form or redirect to team matches list.
"""
team_match = TeamMatch.query.get_or_404(match_id)
team = team_match.org_team
if not can_manage_team_match(team):
flash('You do not have permission to edit this match.', '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
team_match.opponent = request.form.get('opponent', '').strip() or None
date_str = request.form.get('date')
if date_str:
try:
team_match.date = datetime.strptime(date_str, '%Y-%m-%d').date()
except (ValueError, TypeError):
flash('Invalid date format.', 'danger')
return redirect(url_for('team_matches.edit_match', match_id=match_id))
start_time_str = request.form.get('start_time')
if start_time_str:
try:
team_match.start_time = datetime.strptime(start_time_str, '%H:%M').time()
except ValueError:
pass
end_time_str = request.form.get('end_time')
if end_time_str:
try:
team_match.end_time = datetime.strptime(end_time_str, '%H:%M').time()
except ValueError:
pass
team_match.location = request.form.get('location', '') or None
status = request.form.get('status')
if status in ['scheduled', 'completed', 'cancelled']:
team_match.status = status
db.session.commit()
flash('Match updated successfully!', 'success')
return redirect(url_for('team_matches.list_matches'))
return render_template('pages/team_match_form.html',
match=team_match,
team=team,
team_players=[])
@team_matches_bp.route('/<int:match_id>/delete', methods=['POST'])
@login_required
def delete_match(match_id):
"""Delete a team match.
Args:
match_id: The ID of the team match to delete.
Returns:
Response: Redirect to team matches list.
"""
team_match = TeamMatch.query.get_or_404(match_id)
team = team_match.org_team
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'))
db.session.delete(team_match)
db.session.commit()
flash('Match deleted successfully.', 'success')
return redirect(url_for('team_matches.list_matches'))
@team_matches_bp.route('/api/manageable-teams')
@login_required
def api_manageable_teams():
"""API endpoint returning teams the current user can schedule matches for.
Used by the calendar's "Create Event" modal.
Returns:
Response: JSON array of {id, name}.
"""
if not current_user.can_schedule_matches():
return jsonify([])
if current_user.role in ['president', 'manager']:
teams = OrgTeam.query.order_by(OrgTeam.name).all()
elif current_user.role == 'coach':
teams = OrgTeam.query.filter(
db.or_(
OrgTeam.coaches.any(id=current_user.id),
OrgTeam.coach_id == current_user.id
)
).order_by(OrgTeam.name).all()
else:
return jsonify([])
return jsonify([{'id': t.id, 'name': t.name} for t in teams])
@team_matches_bp.route('/<int:match_id>/toggle-presence/<int:participant_id>', methods=['POST'])
@login_required
def toggle_presence(match_id, participant_id):
"""Toggle the is_confirmed status for a team match participant.
Accessible to team managers/coaches AND the player themselves.
Args:
match_id: The ID of the team match.
participant_id: The ID of the TeamMatchParticipant record.
Returns:
Response: JSON with new status.
"""
team_match = TeamMatch.query.get_or_404(match_id)
team = team_match.org_team
participant = TeamMatchParticipant.query.get_or_404(participant_id)
if participant.team_match_id != match_id:
return jsonify({'error': 'Participant does not belong to this match'}), 400
# Allow manager, coach, president, or the player themselves
can_toggle = can_manage_team_match(team) or participant.player_id == current_user.id
if not can_toggle:
return jsonify({'error': 'Unauthorized'}), 403
participant.is_confirmed = not participant.is_confirmed
db.session.commit()
return jsonify({
'participant_id': participant.id,
'is_confirmed': participant.is_confirmed,
'player_name': participant.player.username if participant.player else 'Unknown'
})
+192 -110
View File
@@ -16,8 +16,8 @@ teams_bp = Blueprint('teams', __name__, url_prefix='/teams')
def list_teams():
"""List all organization teams visible to the current user.
Coaches see only their assigned team. Players and scouts see all teams.
Managers and presidents see all teams and can manage them.
Coaches and managers see only their assigned teams.
President sees all teams.
Returns:
Response: Rendered teams list template.
@@ -25,21 +25,87 @@ def list_teams():
can_manage = current_user.can_manage_teams()
if current_user.role == 'coach':
org_team = OrgTeam.query.filter_by(coach_id=current_user.id).first()
teams = [org_team] if org_team else []
elif current_user.role in ['player', 'scout'] or can_manage:
# Players and scouts can view all teams; managers/presidents can manage
teams = OrgTeam.query.order_by(OrgTeam.name).all()
teams = OrgTeam.query.filter(
db.or_(
OrgTeam.coaches.any(id=current_user.id),
OrgTeam.coach_id == current_user.id
)
).order_by(OrgTeam.name).all()
elif current_user.role == 'manager':
teams = OrgTeam.query.filter(
db.or_(
OrgTeam.managers.any(id=current_user.id),
OrgTeam.manager_id == current_user.id
)
).order_by(OrgTeam.name).all()
elif current_user.role in ['player', 'scout']:
flash('Use My Team(s) to view your teams.', 'info')
return redirect(url_for('teams.my_teams'))
else:
flash('You do not have permission to view teams.', 'danger')
return redirect(url_for('main.dashboard'))
coaches = User.query.filter_by(role='coach').order_by(User.username).all()
managers = User.query.filter_by(role='manager').order_by(User.username).all()
coaches = User.query.filter_by(role='coach', is_active_account=True).order_by(User.username).all()
managers = User.query.filter_by(role='manager', is_active_account=True).order_by(User.username).all()
all_players = User.query.filter_by(role='player').order_by(User.username).all()
return render_template('pages/teams.html', teams=teams, coaches=coaches, managers=managers, all_players=all_players, can_manage=can_manage)
@teams_bp.route('/my-teams')
@login_required
def my_teams():
"""View the player's own teams with upcoming matches.
Players can see their team rosters, coaches, managers, and
upcoming team matches with presence confirmation toggles.
Returns:
Response: Rendered my_teams template.
"""
if current_user.role != 'player':
flash('This page is for players.', 'info')
return redirect(url_for('teams.list_teams'))
from models import TeamMatch, TeamMatchParticipant
from datetime import datetime
player_teams = current_user.get_org_teams()
team_data = []
now = datetime.utcnow()
for org_team in player_teams:
matches = TeamMatch.query.filter(
TeamMatch.org_team_id == org_team.id,
TeamMatch.status == 'scheduled'
).order_by(TeamMatch.date.asc(), TeamMatch.start_time.asc()).all()
matches_data = []
for tm in matches:
confirmed, total = tm.get_confirmed_count()
participant = TeamMatchParticipant.query.filter_by(
team_match_id=tm.id,
player_id=current_user.id
).first()
matches_data.append({
'match': tm,
'participant_id': participant.id if participant else None,
'is_confirmed': participant.is_confirmed if participant else False,
'confirmed_count': confirmed,
'total_count': total
})
team_data.append({
'team': org_team,
'matches': matches_data,
'coaches': org_team.get_coaches(),
'managers': org_team.get_managers()
})
return render_template('pages/my_teams.html',
team_data=team_data,
now=now)
@teams_bp.route('/create', methods=['POST'])
@login_required
def create_team():
@@ -77,6 +143,17 @@ def create_team():
created_by=current_user.id
)
db.session.add(team)
db.session.flush()
if coach_id:
coach_user = User.query.get(int(coach_id))
if coach_user:
team.coaches.append(coach_user)
if manager_id:
manager_user = User.query.get(int(manager_id))
if manager_user:
team.managers.append(manager_user)
db.session.commit()
flash(f'Team "{name}" created successfully!', 'success')
return redirect(url_for('teams.list_teams'))
@@ -85,17 +162,7 @@ def create_team():
@teams_bp.route('/<int:team_id>/edit', methods=['POST'])
@login_required
def edit_team(team_id):
"""Edit an existing organization team.
Args:
team_id: The ID of the team to edit.
name: New team name from form.
coach_id: New coach assignment from form.
manager_id: New manager assignment from form.
Returns:
Response: Redirect to teams list with status message.
"""
"""Edit an existing organization team."""
team = OrgTeam.query.get_or_404(team_id)
if not current_user.can_manage_this_org_team(team):
flash('You do not have permission to edit this team.', 'danger')
@@ -117,6 +184,16 @@ def edit_team(team_id):
team.name = name
team.coach_id = int(coach_id) if coach_id else None
team.manager_id = int(manager_id) if manager_id else None
if coach_id:
coach_user = User.query.get(int(coach_id))
if coach_user and not team.coaches.filter_by(id=coach_user.id).first():
team.coaches.append(coach_user)
if manager_id:
manager_user = User.query.get(int(manager_id))
if manager_user and not team.managers.filter_by(id=manager_user.id).first():
team.managers.append(manager_user)
db.session.commit()
flash(f'Team "{name}" updated successfully!', 'success')
return redirect(url_for('teams.list_teams'))
@@ -125,17 +202,7 @@ def edit_team(team_id):
@teams_bp.route('/<int:team_id>/delete', methods=['POST'])
@login_required
def delete_team(team_id):
"""Delete an organization team.
Removes the team and clears its target_org_team_id reference from
any linked tryouts before deletion.
Args:
team_id: The ID of the team to delete.
Returns:
Response: Redirect to teams list with status message.
"""
"""Delete an organization team."""
if not current_user.can_manage_teams():
flash('You do not have permission to delete teams.', 'danger')
return redirect(url_for('teams.list_teams'))
@@ -143,7 +210,6 @@ def delete_team(team_id):
team = OrgTeam.query.get_or_404(team_id)
name = team.name
# Check if any tryouts are targeting this team
from models import Tryout
tryouts = Tryout.query.filter_by(target_org_team_id=team_id).all()
if tryouts:
@@ -151,7 +217,6 @@ def delete_team(team_id):
t.target_org_team_id = None
db.session.commit()
# Remove all team_players associations
TeamPlayer.query.filter_by(org_team_id=team_id).delete()
db.session.commit()
@@ -161,23 +226,88 @@ def delete_team(team_id):
return redirect(url_for('teams.list_teams'))
@teams_bp.route('/<int:team_id>/remove_coach', methods=['POST'])
@teams_bp.route('/<int:team_id>/add_coach', methods=['POST'])
@login_required
def remove_coach(team_id):
"""Remove the coach from an organization team.
Args:
team_id: The ID of the team.
Returns:
Response: Redirect to teams list with status message.
"""
def add_coach(team_id):
"""Add a coach to an organization team (many-to-many)."""
team = OrgTeam.query.get_or_404(team_id)
if not current_user.can_manage_this_org_team(team):
flash('Permission denied.', 'danger')
return redirect(url_for('teams.list_teams'))
coach_id = request.form.get('coach_id')
if not coach_id:
flash('Please select a coach.', 'danger')
return redirect(url_for('teams.list_teams'))
coach = User.query.get_or_404(int(coach_id))
if coach.role != 'coach':
flash('Only coaches can be assigned as coach.', 'danger')
return redirect(url_for('teams.list_teams'))
if team.coaches.filter_by(id=coach.id).first():
flash(f'{coach.username} is already a coach of {team.name}.', 'info')
return redirect(url_for('teams.list_teams'))
team.coaches.append(coach)
if not team.coach_id:
team.coach_id = coach.id
db.session.commit()
flash(f'{coach.username} added as coach of {team.name}.', 'success')
return redirect(url_for('teams.list_teams'))
team.coach_id = None
@teams_bp.route('/<int:team_id>/add_manager', methods=['POST'])
@login_required
def add_manager(team_id):
"""Add a manager to an organization team (many-to-many)."""
team = OrgTeam.query.get_or_404(team_id)
if not current_user.can_manage_this_org_team(team):
flash('Permission denied.', 'danger')
return redirect(url_for('teams.list_teams'))
manager_id = request.form.get('manager_id')
if not manager_id:
flash('Please select a manager.', 'danger')
return redirect(url_for('teams.list_teams'))
manager = User.query.get_or_404(int(manager_id))
if manager.role != 'manager':
flash('Only managers can be assigned as manager.', 'danger')
return redirect(url_for('teams.list_teams'))
if team.managers.filter_by(id=manager.id).first():
flash(f'{manager.username} is already a manager of {team.name}.', 'info')
return redirect(url_for('teams.list_teams'))
team.managers.append(manager)
if not team.manager_id:
team.manager_id = manager.id
db.session.commit()
flash(f'{manager.username} added as manager of {team.name}.', 'success')
return redirect(url_for('teams.list_teams'))
@teams_bp.route('/<int:team_id>/remove_coach', methods=['POST'])
@login_required
def remove_coach(team_id):
"""Remove a coach from an organization team."""
team = OrgTeam.query.get_or_404(team_id)
if not current_user.can_manage_this_org_team(team):
flash('Permission denied.', 'danger')
return redirect(url_for('teams.list_teams'))
coach_id = request.form.get('coach_id')
if coach_id:
coach = User.query.get(int(coach_id))
if coach and team.coaches.filter_by(id=coach.id).first():
team.coaches.remove(coach)
if team.coach_id == coach.id:
team.coach_id = None
else:
team.coaches = []
team.coach_id = None
db.session.commit()
flash(f'Coach removed from {team.name}.', 'success')
return redirect(url_for('teams.list_teams'))
@@ -186,20 +316,23 @@ def remove_coach(team_id):
@teams_bp.route('/<int:team_id>/remove_manager', methods=['POST'])
@login_required
def remove_manager(team_id):
"""Remove the manager from an organization team.
Args:
team_id: The ID of the team.
Returns:
Response: Redirect to teams list with status message.
"""
"""Remove a manager from an organization team."""
team = OrgTeam.query.get_or_404(team_id)
if not current_user.can_manage_this_org_team(team):
flash('Permission denied.', 'danger')
return redirect(url_for('teams.list_teams'))
team.manager_id = None
manager_id = request.form.get('manager_id')
if manager_id:
manager = User.query.get(int(manager_id))
if manager and team.managers.filter_by(id=manager.id).first():
team.managers.remove(manager)
if team.manager_id == manager.id:
team.manager_id = None
else:
team.managers = []
team.manager_id = None
db.session.commit()
flash(f'Manager removed from {team.name}.', 'success')
return redirect(url_for('teams.list_teams'))
@@ -208,18 +341,7 @@ def remove_manager(team_id):
@teams_bp.route('/<int:team_id>/add_player', methods=['POST'])
@login_required
def add_player(team_id):
"""Add a player to an organization team.
Players can now be in multiple teams.
Args:
team_id: The ID of the team to add the player to.
player_id: The ID of the player to add.
status: The player's status (starter or substitute).
Returns:
Response: Redirect to teams list with status message.
"""
"""Add a player to an organization team."""
team = OrgTeam.query.get_or_404(team_id)
if not current_user.can_manage_this_org_team(team):
flash('Permission denied.', 'danger')
@@ -236,13 +358,11 @@ def add_player(team_id):
flash('Can only assign players to teams.', 'danger')
return redirect(url_for('teams.list_teams'))
# Check if player is already on this team
existing = TeamPlayer.query.filter_by(player_id=player.id, org_team_id=team.id).first()
if existing:
flash(f'{player.username} is already on {team.name}.', 'info')
return redirect(url_for('teams.list_teams'))
# Add player to the team using TeamPlayer model (allows multiple teams)
tp = TeamPlayer(
player_id=player.id,
org_team_id=team.id,
@@ -257,15 +377,7 @@ def add_player(team_id):
@teams_bp.route('/<int:team_id>/remove_player/<int:player_id>', methods=['POST'])
@login_required
def remove_player(team_id, player_id):
"""Remove a player from an organization team.
Args:
team_id: The ID of the team.
player_id: The ID of the player to remove.
Returns:
Response: Redirect to teams list with status message.
"""
"""Remove a player from an organization team."""
team = OrgTeam.query.get_or_404(team_id)
if not current_user.can_manage_this_org_team(team):
flash('Permission denied.', 'danger')
@@ -287,15 +399,7 @@ def remove_player(team_id, player_id):
@teams_bp.route('/<int:team_id>/toggle_status/<int:player_id>', methods=['POST'])
@login_required
def toggle_player_status(team_id, player_id):
"""Toggle a player's status between starter and substitute.
Args:
team_id: The ID of the team.
player_id: The ID of the player.
Returns:
Response: JSON with new status.
"""
"""Toggle a player's status between starter and substitute."""
team = OrgTeam.query.get_or_404(team_id)
if not current_user.can_manage_this_org_team(team):
return jsonify({'error': 'Permission denied'}), 403
@@ -304,7 +408,6 @@ def toggle_player_status(team_id, player_id):
if not tp:
return jsonify({'error': 'Player not found on this team'}), 404
# Toggle status
tp.status = 'substitute' if tp.status == 'starter' else 'starter'
db.session.commit()
@@ -316,23 +419,12 @@ def toggle_player_status(team_id, player_id):
})
# Note actions from team page
@teams_bp.route('/<int:team_id>/add-team-note', methods=['POST'])
@login_required
def add_team_note(team_id):
"""Add a team improvement note from the team page (for coaches).
Args:
team_id: The ID of the team to add notes for.
Returns:
Response: Redirect to teams list with status message.
"""
"""Add a team improvement note from the team page (for coaches)."""
team = OrgTeam.query.get_or_404(team_id)
# Check if user can manage this team (president, manager, or coach)
if not current_user.can_manage_this_org_team(team):
flash('You do not have permission to add notes to this team.', 'danger')
return redirect(url_for('teams.list_teams'))
@@ -355,18 +447,9 @@ def add_team_note(team_id):
@teams_bp.route('/<int:team_id>/add-player-note/<int:player_id>', methods=['POST'])
@login_required
def add_player_note(team_id, player_id):
"""Add a personal note for a player from the team page (for coaches).
Args:
team_id: The ID of the team.
player_id: The ID of the player to add note for.
Returns:
Response: Redirect to teams list with status message.
"""
"""Add a personal note for a player from the team page (for coaches)."""
team = OrgTeam.query.get_or_404(team_id)
# Check if user can manage this team (president, manager, or coach)
if not current_user.can_manage_this_org_team(team):
flash('You do not have permission to add notes to this team.', 'danger')
return redirect(url_for('teams.list_teams'))
@@ -376,7 +459,6 @@ def add_player_note(team_id, player_id):
flash('Can only add notes for players.', 'danger')
return redirect(url_for('teams.list_teams'))
# Verify player belongs to this team
tp = TeamPlayer.query.filter_by(player_id=player_id, org_team_id=team_id).first()
if not tp:
flash(f'{player.username} is not on {team.name}.', 'danger')
+50
View File
@@ -457,6 +457,56 @@ def register_player(tryout_id):
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
@tryouts_bp.route('/<int:tryout_id>/remove_player/<int:player_id>', methods=['POST'])
@login_required
def remove_player(tryout_id, player_id):
"""Remove a registered player from a tryout.
Also removes the player from any tryout teams and match participants
within this tryout.
Args:
tryout_id: The ID of the tryout.
player_id: The ID of the player to remove.
Returns:
Response: Redirect to tryout view with status message.
"""
tryout = Tryout.query.get_or_404(tryout_id)
if not current_user.can_manage_this_tryout(tryout):
flash('Permission denied.', 'danger')
return redirect(url_for('tryouts.list_tryouts'))
player = User.query.get_or_404(player_id)
# Remove the tryout registration
registration = TryoutRegistration.query.filter_by(
tryout_id=tryout_id, player_id=player_id
).first()
if registration:
db.session.delete(registration)
# Remove from tryout teams within this tryout
team_ids = [t.id for t in Team.query.filter_by(tryout_id=tryout_id).all()]
if team_ids:
TeamMember.query.filter(
TeamMember.team_id.in_(team_ids),
TeamMember.player_id == player_id
).delete(synchronize_session=False)
# Remove from match participants in this tryout
match_ids = [m.id for m in Match.query.filter_by(tryout_id=tryout_id).all()]
if match_ids:
MatchParticipant.query.filter(
MatchParticipant.match_id.in_(match_ids),
MatchParticipant.player_id == player_id
).delete(synchronize_session=False)
db.session.commit()
flash(f'{player.username} removed from tryout.', 'success')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
@tryouts_bp.route('/<int:tryout_id>/team/create', methods=['POST'])
@login_required
def create_team(tryout_id):
+18
View File
@@ -270,6 +270,24 @@ def create_user():
return render_template('pages/create_user.html', roles=ROLES)
@users_bp.route('/<int:user_id>/view')
@login_required
def view_user(user_id):
"""View a public profile for any user.
Shows username, games, Discord username, and gamertags.
Does NOT expose full_name, phone, or email.
Args:
user_id: The ID of the user to view.
Returns:
Response: Rendered public profile template.
"""
user = User.query.get_or_404(user_id)
return render_template('pages/view_user.html', profile_user=user)
@users_bp.route('/profile')
@login_required
def profile():