From e4a271cfd87d29a38543373111224df92333226a Mon Sep 17 00:00:00 2001 From: cedrick2711 Date: Sat, 5 Sep 2026 14:38:31 -0400 Subject: [PATCH] changer gestion des team intra tryout --- app/routes/tryouts.py | 207 +++++++++++++++++++++++++-- app/templates/pages/view_tryout.html | 177 +++++++++++++++++++---- app/validators.py | 12 ++ 3 files changed, 355 insertions(+), 41 deletions(-) diff --git a/app/routes/tryouts.py b/app/routes/tryouts.py index 33fa9b2..636941a 100644 --- a/app/routes/tryouts.py +++ b/app/routes/tryouts.py @@ -16,6 +16,7 @@ from app.models import ( ESPORT_GAMES, GAME_POSITIONS, Admin, + AppSettings, Coach, Evaluation, Manager, @@ -30,7 +31,6 @@ from app.models import ( Tryout, TryoutRegistration, User, - AppSettings, ) from app.time_utils import utc_now_naive from app.validators import ( @@ -38,6 +38,7 @@ from app.validators import ( TryoutRegistrationStatusSchema, TryoutSchema, TryoutStatusSchema, + TryoutTeamMemberPositionSchema, TryoutTeamMemberSchema, TryoutTeamSchema, ) @@ -140,7 +141,10 @@ def list_tryouts(): def create_tryout(): """Create a new tryout event. Requires Admin or Manager.""" if tryouts_locked(): - flash('Tryouts are currently closed. An admin must open tryouts before changes can be made.', 'danger') + flash( + 'Tryouts are currently closed. An admin must open tryouts before changes can be made.', + 'danger', + ) return redirect(url_for('tryouts.list_tryouts')) if not can_manage(): @@ -204,7 +208,10 @@ def edit_tryout(tryout_id): tryout = db.get_or_404(Tryout, tryout_id) if tryouts_locked(): - flash('Tryouts are currently closed. An admin must open tryouts before changes can be made.', 'danger') + 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 not current_user.can_manage_this_tryout(tryout): @@ -273,9 +280,7 @@ def view_tryout(tryout_id): can_view = False if isinstance(current_user, Admin): can_view = True - elif isinstance(current_user, Manager): - can_view = current_user.can_manage_this_tryout(tryout) - elif isinstance(current_user, Coach): + elif isinstance(current_user, (Manager, Coach)): can_view = current_user.can_manage_this_tryout(tryout) elif isinstance(current_user, Player): is_registered = ( @@ -341,7 +346,12 @@ def view_tryout(tryout_id): member_players = _users_by_id([m.player_id for m in member_rows if m.player_id]) for row in member_rows: members_by_team.setdefault(row.team_id, []).append( - {'player': member_players.get(row.player_id), 'position': row.position} + { + 'player': member_players.get(row.player_id), + 'position': row.position, + 'member_id': row.id, + 'player_id': row.player_id, + } ) team_data = [{'team': team, 'members': members_by_team.get(team.id, [])} for team in teams] @@ -500,7 +510,10 @@ def update_status(tryout_id): """Update the status of a tryout.""" tryout = db.get_or_404(Tryout, tryout_id) if tryouts_locked(): - flash('Tryouts are currently closed. An admin must open tryouts before changes can be made.', 'danger') + 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 not current_user.can_manage_this_tryout(tryout): @@ -524,7 +537,10 @@ def update_registration_status(tryout_id, player_id): """Update a registration's attendance status.""" tryout = db.get_or_404(Tryout, tryout_id) if tryouts_locked(): - flash('Tryouts are currently closed. An admin must open tryouts before changes can be made.', 'danger') + 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 not current_user.can_manage_this_tryout(tryout): @@ -552,7 +568,10 @@ def register_player(tryout_id): """Manually register a player for a tryout (by managers/coaches).""" tryout = locked_tryout_or_404(tryout_id) if tryouts_locked(): - flash('Tryouts are currently closed. An admin must open tryouts before changes can be made.', 'danger') + 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 not current_user.can_manage_this_tryout(tryout): @@ -603,7 +622,10 @@ def remove_player(tryout_id, player_id): """Remove a registered player from a tryout (cascades to teams/matches).""" tryout = db.get_or_404(Tryout, tryout_id) if tryouts_locked(): - flash('Tryouts are currently closed. An admin must open tryouts before changes can be made.', 'danger') + 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 not current_user.can_manage_this_tryout(tryout): @@ -643,7 +665,10 @@ def create_team(tryout_id): """Create a tryout-specific team.""" tryout = db.get_or_404(Tryout, tryout_id) if tryouts_locked(): - flash('Tryouts are currently closed. An admin must open tryouts before changes can be made.', 'danger') + 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 not current_user.can_manage_this_tryout(tryout): @@ -670,7 +695,10 @@ def add_to_team(tryout_id, team_id): team = db.get_or_404(Team, team_id) tryout = db.get_or_404(Tryout, tryout_id) if tryouts_locked(): - flash('Tryouts are currently closed. An admin must open tryouts before changes can be made.', 'danger') + 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 not current_user.can_manage_this_tryout(tryout): @@ -710,13 +738,162 @@ def add_to_team(tryout_id, team_id): return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) +@tryouts_bp.route('//team//edit', methods=['POST']) +@login_required +def edit_team(tryout_id, team_id): + """Rename a tryout-specific team.""" + team = db.get_or_404(Team, team_id) + tryout = db.get_or_404(Tryout, 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)) + + 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 team.tryout_id != tryout_id: + abort(404) + + try: + data = TryoutTeamSchema().load(form_payload(list_fields=())) + except ValidationError as err: + flash_validation_errors(err) + return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) + + team.name = data['team_name'] + db.session.commit() + flash(_('Team renamed to "%(team_name)s".', team_name=data['team_name']), 'success') + return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) + + +@tryouts_bp.route('//team//delete', methods=['POST']) +@login_required +def delete_team(tryout_id, team_id): + """Delete a tryout-specific team and its memberships.""" + team = db.get_or_404(Team, team_id) + tryout = db.get_or_404(Tryout, 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)) + + 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 team.tryout_id != tryout_id: + abort(404) + + name = team.name + + # Personal note references outlive the team. + PersonalNote.query.filter_by(team_id=team_id).update( + {'team_id': None}, synchronize_session=False + ) + + # Matches referencing this team: null the FK so the match survives. + Match.query.filter_by(team1_id=team_id).update({'team1_id': None}, synchronize_session=False) + Match.query.filter_by(team2_id=team_id).update({'team2_id': None}, synchronize_session=False) + + # Clean participants for those matches (they are no longer in that team). + for match in Match.query.filter( + db.or_(Match.team1_id == team_id, Match.team2_id == team_id) + ).all(): + MatchParticipant.query.filter_by(match_id=match.id).delete(synchronize_session=False) + + TeamMember.query.filter_by(team_id=team_id).delete(synchronize_session=False) + db.session.delete(team) + db.session.commit() + flash(_('Team "%(name)s" deleted.', name=name), 'success') + return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) + + +@tryouts_bp.route('//team//remove/', methods=['POST']) +@login_required +def remove_from_team(tryout_id, team_id, player_id): + """Remove a player from a tryout team.""" + team = db.get_or_404(Team, team_id) + tryout = db.get_or_404(Tryout, 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)) + + 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 team.tryout_id != tryout_id: + abort(404) + + TeamMember.query.filter_by(team_id=team_id, player_id=player_id).delete( + synchronize_session=False + ) + # Also remove the player from any match participants that were tied to this team. + 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(_('Player removed from team.'), 'success') + return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) + + +@tryouts_bp.route( + '//team//member//position', methods=['POST'] +) +@login_required +def update_member_position(tryout_id, team_id, member_id): + """Set or clear a team member's in-game position (role).""" + team = db.get_or_404(Team, team_id) + tryout = db.get_or_404(Tryout, tryout_id) + member = db.get_or_404(TeamMember, member_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)) + + 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 team.tryout_id != tryout_id or member.team_id != team_id: + abort(404) + + try: + data = TryoutTeamMemberPositionSchema().load(form_payload(list_fields=())) + except ValidationError as err: + flash_validation_errors(err) + return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) + + member.position = data['position'] or None + db.session.commit() + flash(_('Position updated.'), 'success') + return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) + + @tryouts_bp.route('//delete', methods=['POST']) @login_required def delete_tryout(tryout_id): """Delete a tryout and all associated data (matches, teams, registrations, evaluations).""" tryout = db.get_or_404(Tryout, tryout_id) if tryouts_locked(): - flash('Tryouts are currently closed. An admin must open tryouts before changes can be made.', 'danger') + 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 not current_user.can_manage_this_tryout(tryout): @@ -757,4 +934,4 @@ def delete_tryout(tryout_id): db.session.delete(tryout) db.session.commit() flash(_('Tryout deleted successfully.'), 'success') - return redirect(url_for('tryouts.list_tryouts')) \ No newline at end of file + return redirect(url_for('tryouts.list_tryouts')) diff --git a/app/templates/pages/view_tryout.html b/app/templates/pages/view_tryout.html index 4d8bbd5..595bcec 100644 --- a/app/templates/pages/view_tryout.html +++ b/app/templates/pages/view_tryout.html @@ -249,7 +249,14 @@ {% for team in team_data %}
-

{{ team.team.name }}

+
+

{{ team.team.name }}

+ {% if can_edit and not tryout.is_ended %} + + {% endif %} +
    {% for member in team.members %}
  • @@ -263,32 +270,96 @@
  • {{ _('No players assigned yet.') }}
  • {% endfor %}
- {% if can_edit and not tryout.is_ended and registered_players %} - {% set positions = game_positions.get(tryout.game, []) %} -
- - - {% if positions %} - - {% else %} - - {% endif %} - -
- {% endif %}
+ + {% if can_edit and not tryout.is_ended %} + {% set positions = game_positions.get(tryout.game, []) %} + + + {% endif %} + {% else %}

{{ _('No teams created yet.') }}

{% endfor %} @@ -574,8 +645,56 @@ transform: scale(1.08); box-shadow: 0 1px 3px rgba(0,0,0,0.12); } +/* Team card header — edit button aligned right */ +.team-card-header { + display: flex; + align-items: center; + justify-content: space-between; +} +.team-card-header .team-name { + margin-bottom: 0; +} +/* Roster rows inside modal */ +.roster-rows { + display: flex; + flex-direction: column; + gap: 6px; +} +.roster-row { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 8px; + background: var(--gray-50); + border-radius: var(--radius-sm); +} +.roster-player-name { + flex: 1; + font-size: 0.9rem; + font-weight: 500; +} +.roster-role-select { + width: auto; + font-size: 0.75rem; + padding: 1px 4px; +} +.roster-remove-btn { + padding: 1px 5px; + font-size: 0.7rem; +} +[data-theme="dark"] .roster-row { + background: var(--bg-tertiary); +}