From 68e3da660163c94b874dd930962e476a256cb013 Mon Sep 17 00:00:00 2001 From: cedrick2711 Date: Thu, 6 Aug 2026 13:41:53 -0400 Subject: [PATCH 1/4] fix probleme avec dispos --- app/models/tryout/tryout.py | 13 +- app/routes/matches.py | 38 +-- app/routes/tryouts.py | 79 +++++- app/routes/users.py | 10 +- app/templates/pages/calendar.html | 49 +++- app/templates/pages/edit_profile.html | 190 -------------- app/templates/pages/profile.html | 358 ++++++++++++++++++++++++++ app/templates/pages/tryout_form.html | 9 +- app/templates/pages/tryouts.html | 8 +- app/templates/pages/view_tryout.html | 36 ++- migrations/add_tryout_end_date.py | 38 +++ 11 files changed, 582 insertions(+), 246 deletions(-) create mode 100644 migrations/add_tryout_end_date.py diff --git a/app/models/tryout/tryout.py b/app/models/tryout/tryout.py index 752ae81..cfcabde 100644 --- a/app/models/tryout/tryout.py +++ b/app/models/tryout/tryout.py @@ -11,6 +11,7 @@ class Tryout(db.Model): description = db.Column(db.Text, nullable=True) game = db.Column(db.String(50), nullable=False) date = db.Column(db.Date, nullable=False) + end_date = db.Column(db.Date, nullable=True) location = db.Column(db.String(200), nullable=True) status = db.Column(db.String(20), default='upcoming') max_players = db.Column(db.Integer, nullable=True) @@ -26,4 +27,14 @@ class Tryout(db.Model): registrations = db.relationship('TryoutRegistration', backref='tryout', lazy='dynamic') evaluations = db.relationship('Evaluation', backref='tryout', lazy='dynamic') teams = db.relationship('Team', backref='tryout', lazy='dynamic') - target_org_team = db.relationship('OrgTeam', backref='tryouts', foreign_keys=[target_org_team_id]) \ No newline at end of file + target_org_team = db.relationship('OrgTeam', backref='tryouts', foreign_keys=[target_org_team_id]) + + @property + def is_ended(self): + """Tryout is considered ended after its end_date passes. + Falls back to date if end_date is not set.""" + from datetime import date as date_type + today = date_type.today() + if self.end_date is not None: + return self.end_date < today + return self.date < today diff --git a/app/routes/matches.py b/app/routes/matches.py index 649183f..457cacb 100644 --- a/app/routes/matches.py +++ b/app/routes/matches.py @@ -46,19 +46,6 @@ def api_events(): tryouts = get_visible_tryouts_for_user() for tryout in tryouts: - events.append({ - 'id': f'tryout_{tryout.id}', - 'title': tryout.title, - 'date': tryout.date.strftime('%Y-%m-%d'), - 'type': 'tryout', 'color': '#3b82f6', - 'extendedProps': { - 'location': tryout.location or 'TBD', - 'status': tryout.status, - 'description': tryout.description or '', - 'tryout_id': tryout.id, - }, - }) - for match in tryout.matches: match_color = '#10b981' if match.match_type == 'team_vs_team' else '#f59e0b' match_desc = match.description or '' @@ -158,18 +145,7 @@ def api_events_for_tryout(tryout_id): if not can_view and not is_registered and not player_in_match: return jsonify([]) - events = [{ - 'id': f'tryout_{tryout.id}', - 'title': f'Tryout: {tryout.title}', - 'date': tryout.date.strftime('%Y-%m-%d'), - 'type': 'tryout', 'color': '#3b82f6', - 'extendedProps': { - 'location': tryout.location or 'TBD', - 'status': tryout.status, - 'description': tryout.description or '', - 'tryout_id': tryout.id, - }, - }] + events = [] for match in tryout.matches: match_color = '#10b981' if match.match_type in ('team_vs_team', 'player_vs_player') else '#f59e0b' @@ -221,6 +197,10 @@ def create_match(tryout_id): flash('You do not have permission to schedule matches for this tryout.', 'danger') return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) + if tryout.is_ended: + flash('This tryout has ended. Matches can no longer be created or modified.', 'danger') + return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) + teams = Team.query.filter_by(tryout_id=tryout_id).all() registrations = TryoutRegistration.query.filter_by(tryout_id=tryout_id).all() all_players = [User.query.get(r.player_id) for r in registrations if User.query.get(r.player_id)] @@ -348,6 +328,10 @@ def edit_match(match_id): flash('You do not have permission to edit this match.', 'danger') return redirect(url_for('matches.calendar')) + if tryout.is_ended: + flash('This tryout has ended. Matches can no longer be created or modified.', 'danger') + return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id)) + teams = Team.query.filter_by(tryout_id=tryout.id).all() registrations = TryoutRegistration.query.filter_by(tryout_id=tryout.id).all() all_players = [User.query.get(r.player_id) for r in registrations if r.player_id] @@ -503,6 +487,7 @@ def api_manageable_tryouts(): manageable.append({ 'id': t.id, 'title': t.title, 'date': t.date.strftime('%Y-%m-%d'), + 'end_date': t.end_date.strftime('%Y-%m-%d') if t.end_date else None, }) return jsonify(manageable) @@ -516,6 +501,9 @@ def delete_match(match_id): if not current_user.can_manage_this_tryout(tryout): flash('You do not have permission to delete this match.', 'danger') return redirect(url_for('matches.calendar')) + if tryout.is_ended: + flash('This tryout has ended. Matches can no longer be deleted.', 'danger') + return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id)) db.session.delete(match) db.session.commit() flash('Match deleted successfully.', 'success') diff --git a/app/routes/tryouts.py b/app/routes/tryouts.py index c5f4a3a..8f0f26a 100644 --- a/app/routes/tryouts.py +++ b/app/routes/tryouts.py @@ -51,6 +51,7 @@ def create_tryout(): description = request.form.get('description') game = request.form.get('game') date_str = request.form.get('date') + end_date_str = request.form.get('end_date') location = request.form.get('location') max_players = request.form.get('max_players') target_org_team_id = request.form.get('target_org_team_id') @@ -60,12 +61,26 @@ def create_tryout(): try: date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() except (ValueError, TypeError): - flash('Invalid date format.', 'danger') + flash('Invalid start date format.', 'danger') return render_template('pages/tryout_form.html', tryout=None, org_teams=org_teams, managers=managers, coaches=coaches, esport_games=ESPORT_GAMES) + end_date_obj = None + if end_date_str: + try: + end_date_obj = datetime.strptime(end_date_str, '%Y-%m-%d').date() + if end_date_obj < date_obj: + flash('End date cannot be before start date.', 'danger') + return render_template('pages/tryout_form.html', tryout=None, org_teams=org_teams, + managers=managers, coaches=coaches, esport_games=ESPORT_GAMES) + except (ValueError, TypeError): + flash('Invalid end date format.', 'danger') + return render_template('pages/tryout_form.html', tryout=None, org_teams=org_teams, + managers=managers, coaches=coaches, esport_games=ESPORT_GAMES) + tryout = Tryout( title=title, description=description, game=game, date=date_obj, + end_date=end_date_obj, location=location, max_players=int(max_players) if max_players else None, created_by=current_user.id, status='upcoming', @@ -92,6 +107,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 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)) + org_teams = OrgTeam.query.order_by(OrgTeam.name).all() managers = User.query.filter_by(role='manager', is_active_account=True).order_by(User.full_name).all() coaches = User.query.filter_by(role='coach', is_active_account=True).order_by(User.full_name).all() @@ -101,6 +120,7 @@ def edit_tryout(tryout_id): description = request.form.get('description') game = request.form.get('game') date_str = request.form.get('date') + end_date_str = request.form.get('end_date') location = request.form.get('location') max_players = request.form.get('max_players') target_org_team_id = request.form.get('target_org_team_id') @@ -110,14 +130,28 @@ def edit_tryout(tryout_id): try: date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() except (ValueError, TypeError): - flash('Invalid date format.', 'danger') + flash('Invalid start date format.', 'danger') return render_template('pages/tryout_form.html', tryout=tryout, org_teams=org_teams, managers=managers, coaches=coaches, esport_games=ESPORT_GAMES) + end_date_obj = None + if end_date_str: + try: + end_date_obj = datetime.strptime(end_date_str, '%Y-%m-%d').date() + if end_date_obj < date_obj: + flash('End date cannot be before start date.', 'danger') + return render_template('pages/tryout_form.html', tryout=tryout, org_teams=org_teams, + managers=managers, coaches=coaches, esport_games=ESPORT_GAMES) + except (ValueError, TypeError): + flash('Invalid end date format.', 'danger') + return render_template('pages/tryout_form.html', tryout=tryout, org_teams=org_teams, + managers=managers, coaches=coaches, esport_games=ESPORT_GAMES) + tryout.title = title tryout.description = description tryout.game = game tryout.date = date_obj + tryout.end_date = end_date_obj tryout.location = location tryout.max_players = int(max_players) if max_players else None tryout.target_org_team_id = int(target_org_team_id) if target_org_team_id else None @@ -429,4 +463,43 @@ def add_to_team(tryout_id, team_id): db.session.add(member) db.session.commit() flash('Player added to team!', 'success') - return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id)) \ No newline at end of file + 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 = Tryout.query.get_or_404(tryout_id) + if not current_user.can_manage_this_tryout(tryout): + flash('You do not have permission to delete this tryout.', 'danger') + return redirect(url_for('tryouts.list_tryouts')) + + # 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: + MatchParticipant.query.filter( + MatchParticipant.match_id.in_(match_ids) + ).delete(synchronize_session=False) + # Delete matches + Match.query.filter(Match.id.in_(match_ids)).delete(synchronize_session=False) + + # Delete team members for all teams in 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) + ).delete(synchronize_session=False) + # Delete teams + Team.query.filter(Team.id.in_(team_ids)).delete(synchronize_session=False) + + # Delete registrations + TryoutRegistration.query.filter_by(tryout_id=tryout_id).delete() + + # Delete evaluations + Evaluation.query.filter_by(tryout_id=tryout_id).delete() + + 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 diff --git a/app/routes/users.py b/app/routes/users.py index 8cbe3cf..4a0a38b 100644 --- a/app/routes/users.py +++ b/app/routes/users.py @@ -258,7 +258,15 @@ def profile(): contracts = Contract.query.filter_by( player_id=current_user.id, ).order_by(Contract.uploaded_at.desc()).all() - return render_template('pages/profile.html', user=current_user, contracts=contracts) + + existing_availability = None + if isinstance(current_user, Coach): + existing_availability = CoachAvailability.query.filter_by( + coach_id=current_user.id, + ).all() + + return render_template('pages/profile.html', user=current_user, contracts=contracts, + existing_availability=existing_availability) @users_bp.route('/profile/edit', methods=['GET', 'POST']) diff --git a/app/templates/pages/calendar.html b/app/templates/pages/calendar.html index d434cc0..b984ad0 100644 --- a/app/templates/pages/calendar.html +++ b/app/templates/pages/calendar.html @@ -197,7 +197,6 @@ function goToTeamMatch() { } function goToCreateTryout() { - // Tryout creation doesn't support pre-filling date easily, just navigate window.location.href = '/tryouts/create'; } @@ -208,8 +207,13 @@ function fetchTryoutOptions() { .then(function(data) { var sel = document.getElementById('createTryoutSelect'); sel.innerHTML = ''; + var today = new Date().toISOString().split('T')[0]; data.forEach(function(t) { - sel.innerHTML += ''; + // Only show tryouts that haven't ended + var tryoutEndDate = t.end_date || t.date; + if (tryoutEndDate >= today) { + sel.innerHTML += ''; + } }); }) .catch(function() {}); @@ -236,8 +240,8 @@ function showEventModal(event) { var content = '
'; content += '
Type'; - content += ''; - content += (type === 'tryout' ? 'Tryout' : (props.match_type === 'team_vs_team' ? 'Team Match' : (props.match_type === 'player_vs_player' ? 'Player Match' : 'Player Scrim'))) + ''; + 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 + '
'; @@ -250,7 +254,6 @@ function showEventModal(event) { if (type === 'match' && props.participants) { content += '
Teams'; if (props.match_type === 'team_vs_team') { - // For team vs team, participants is "Team1 vs Team2" var teams = props.participants.split(' vs '); if (teams.length >= 2) { content += '
'; @@ -262,8 +265,6 @@ function showEventModal(event) { content += props.participants; } } else if (props.match_type === 'player_vs_player') { - // For player vs player, we need to parse the participants - // The format is "player1, player2 vs player3, player4" var parts = props.participants.split(' vs '); if (parts.length >= 2) { content += '
'; @@ -275,7 +276,6 @@ function showEventModal(event) { content += props.participants; } } else { - // For player scrim, just show the list content += props.participants; } content += '
'; @@ -286,7 +286,7 @@ function showEventModal(event) { } content += '
'; - document.getElementById('modalTitle').textContent = type === 'tryout' ? 'Tryout Details' : 'Match Details'; + document.getElementById('modalTitle').textContent = 'Match Details'; document.getElementById('modalContent').innerHTML = content; // Reset buttons @@ -301,11 +301,11 @@ function showEventModal(event) { document.getElementById('editMatchBtn').onclick = function() { window.location.href = '/matches/' + props.match_id + '/edit'; }; - } else if (type === 'tryout' && canScheduleMatches) { - document.getElementById('modalActions').style.display = 'flex'; - document.getElementById('viewTryoutBtn').style.display = 'inline-flex'; - document.getElementById('viewTryoutBtn').onclick = function() { - window.location.href = '/tryouts/' + props.tryout_id; + document.getElementById('deleteMatchBtn').style.display = 'inline-flex'; + document.getElementById('deleteMatchBtn').onclick = function() { + if (confirm('Are you sure you want to delete this match?')) { + deleteCalendarMatch(props.match_id); + } }; } else { document.getElementById('modalActions').style.display = 'none'; @@ -329,6 +329,27 @@ function showEventModal(event) { document.getElementById('eventModal').classList.remove('hidden'); } +function deleteCalendarMatch(matchId) { + fetch('/matches/' + matchId + '/delete', { + method: 'POST', + headers: { + 'X-CSRFToken': '{{ csrf_token() }}', + 'Content-Type': 'application/json' + } + }) + .then(function(r) { return r.json().catch(function() { return {}; }); }) + .then(function() { + hideEventModal(); + if (window.fcCalendar) { + window.fcCalendar.refetchEvents(); + } + }) + .catch(function(err) { + console.error('Error deleting match:', err); + alert('Failed to delete match.'); + }); +} + function toggleCalendarPresence(matchId, participantId, btn) { fetch('/matches/' + matchId + '/toggle-presence/' + participantId, { method: 'POST', diff --git a/app/templates/pages/edit_profile.html b/app/templates/pages/edit_profile.html index 5c95c29..e126814 100644 --- a/app/templates/pages/edit_profile.html +++ b/app/templates/pages/edit_profile.html @@ -104,25 +104,6 @@
- {% if user.role == 'player' %} -
-

My Disponibilities

-

Select your available time blocks for matches (5pm to 12am). Green = selected, Gray = available to select.

- -
-

Loading...

-
- -
- - -
- {% endif %} -
Cancel @@ -165,177 +146,6 @@ document.addEventListener('DOMContentLoaded', function() { // Show gamertag inputs for already selected games toggleGamertagInputs(); - - {% if user.role == 'player' %} - renderDisponibilityGrid(); - {% endif %} }); - -{% if user.role == 'player' %} -// Generate time slots from 5pm (17:00) to 12am (24:00) -var TIME_SLOTS = []; -for (var h = 17; h <= 24; h++) { - for (var m = 0; m < 60; m += 30) { - if (h === 24 && m > 0) continue; - var displayHour; - var displayAmpm; - if (h === 24) { - displayHour = 12; - displayAmpm = 'AM'; - } else if (h > 12) { - displayHour = h - 12; - displayAmpm = 'PM'; - } else { - displayHour = h; - displayAmpm = 'PM'; - } - var timeStr = (h < 10 ? '0' : '') + h + ':' + (m < 10 ? '0' : '') + m; - var displayTime = displayHour + ':' + (m < 10 ? '0' : '') + m + ' ' + displayAmpm; - TIME_SLOTS.push({ time: timeStr, display: displayTime }); - } -} - -var DAYS = [ - { value: 0, name: 'Monday' }, - { value: 1, name: 'Tuesday' }, - { value: 2, name: 'Wednesday' }, - { value: 3, name: 'Thursday' }, - { value: 4, name: 'Friday' }, - { value: 5, name: 'Saturday' }, - { value: 6, name: 'Sunday' } -]; - -// Store selected slots: {day: [time, time, ...]} -var selectedSlots = {}; - -function renderDisponibilityGrid() { - var grid = document.getElementById('disponibilities-grid'); - grid.innerHTML = '
Click time blocks to select your available hours
'; - - var container = document.createElement('div'); - container.className = 'disponibility-grid'; - - DAYS.forEach(function(day) { - var dayRow = document.createElement('div'); - dayRow.className = 'disponibility-day-row'; - - var dayLabel = document.createElement('div'); - dayLabel.className = 'disponibility-day-label'; - dayLabel.textContent = day.name; - dayRow.appendChild(dayLabel); - - var timeBlocks = document.createElement('div'); - timeBlocks.className = 'disponibility-time-blocks'; - - TIME_SLOTS.forEach(function(slot) { - var block = document.createElement('div'); - block.className = 'disponibility-time-block'; - block.dataset.day = day.value; - block.dataset.time = slot.time; - block.textContent = slot.display; - block.onclick = function() { - toggleSlot(day.value, slot.time, block); - }; - timeBlocks.appendChild(block); - }); - - dayRow.appendChild(timeBlocks); - container.appendChild(dayRow); - }); - - grid.appendChild(container); - loadMyDisponibilities(); -} - -function toggleSlot(day, time, element) { - if (!selectedSlots[day]) selectedSlots[day] = []; - - var index = selectedSlots[day].indexOf(time); - if (index > -1) { - selectedSlots[day].splice(index, 1); - element.classList.remove('selected'); - } else { - selectedSlots[day].push(time); - element.classList.add('selected'); - } -} - -function loadMyDisponibilities() { - fetch('{{ url_for("users.get_my_disponibilities") }}') - .then(function(response) { return response.json(); }) - .then(function(data) { - selectedSlots = {}; - - for (var day in data) { - var slots = data[day]; - slots.forEach(function(slot) { - selectedSlots[day] = selectedSlots[day] || []; - selectedSlots[day].push(slot.start_time); - }); - } - - document.querySelectorAll('.disponibility-time-block').forEach(function(block) { - var day = block.dataset.day; - var time = block.dataset.time; - if (selectedSlots[day] && selectedSlots[day].indexOf(time) > -1) { - block.classList.add('selected'); - } else { - block.classList.remove('selected'); - } - }); - }) - .catch(function(error) { - console.error('Error loading disponibilities:', error); - }); -} - -function saveDisponibilities() { - var slots = []; - for (var day in selectedSlots) { - selectedSlots[day].forEach(function(time) { - slots.push({ day_of_week: parseInt(day), start_time: time }); - }); - } - - fetch('{{ url_for("users.add_disponibilities_bulk") }}', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ slots: slots }) - }) - .then(function(response) { return response.json(); }) - .then(function(data) { - if (data.success) { - var msg = document.createElement('div'); - msg.className = 'alert alert-success'; - msg.style.marginTop = '10px'; - msg.innerHTML = ' Disponibilities saved successfully!'; - document.getElementById('disponibilities-grid').appendChild(msg); - setTimeout(function() { msg.remove(); }, 3000); - } - }) - .catch(function(error) { - console.error('Error saving disponibilities:', error); - }); -} - -function clearDisponibilities() { - if (!confirm('Are you sure you want to clear all your disponibilities?')) return; - - fetch('{{ url_for("users.clear_disponibilities") }}', { - method: 'POST' - }) - .then(function(response) { return response.json(); }) - .then(function(data) { - if (data.success) { - selectedSlots = {}; - document.querySelectorAll('.disponibility-time-block').forEach(function(block) { - block.classList.remove('selected'); - }); - } - }); -} -{% endif %} {% endblock %} \ No newline at end of file diff --git a/app/templates/pages/profile.html b/app/templates/pages/profile.html index 46f4e63..663e719 100644 --- a/app/templates/pages/profile.html +++ b/app/templates/pages/profile.html @@ -180,5 +180,363 @@
{% endif %} + + + {% if user.role == 'player' %} +
+
+

My Disponibilities

+
+
+

Select your available time blocks for matches (5pm to 12am). Green = selected, Gray = available to select.

+
+

Loading...

+
+
+ + +
+
+
+ {% endif %} + + + {% if user.role == 'coach' %} +
+
+

My Coaching Availability

+

Select time slots when you're available for One on One sessions (8am to 10pm).

+
+
+
+

Loading availability grid...

+
+
+ +
+
+
+ {% endif %} {% endblock %} + +{% block scripts %} +{% if user.role == 'coach' %} + + +{% endif %} + +{% if user.role == 'player' %} + +{% endif %} +{% endblock %} \ No newline at end of file diff --git a/app/templates/pages/tryout_form.html b/app/templates/pages/tryout_form.html index 6401816..5ef8e34 100644 --- a/app/templates/pages/tryout_form.html +++ b/app/templates/pages/tryout_form.html @@ -34,10 +34,15 @@ {% endfor %} -
- +
+
+
+ + + Optional. Leave blank for single-day tryout. +
diff --git a/app/templates/pages/tryouts.html b/app/templates/pages/tryouts.html index bd49dde..ba0dd11 100644 --- a/app/templates/pages/tryouts.html +++ b/app/templates/pages/tryouts.html @@ -29,7 +29,13 @@
Date - {{ tryout.date.strftime('%b %d, %Y') }} + + {% if tryout.end_date and tryout.end_date != tryout.date %} + {{ tryout.date.strftime('%b %d') }} - {{ tryout.end_date.strftime('%b %d, %Y') }} + {% else %} + {{ tryout.date.strftime('%b %d, %Y') }} + {% endif %} +
diff --git a/app/templates/pages/view_tryout.html b/app/templates/pages/view_tryout.html index 565bfbd..cf7ffa5 100644 --- a/app/templates/pages/view_tryout.html +++ b/app/templates/pages/view_tryout.html @@ -9,7 +9,7 @@

Tryout Details

- {% if can_edit %} + {% if can_edit and not tryout.is_ended %} Schedule Match @@ -24,11 +24,19 @@ {% endif %} - {% if can_edit %} + {% if can_edit and not tryout.is_ended %} Edit {% endif %} + {% if can_edit %} +
+ + +
+ {% endif %}
@@ -39,7 +47,13 @@
Date - {{ tryout.date.strftime('%A, %B %d, %Y') }} + + {% if tryout.end_date and tryout.end_date != tryout.date %} + {{ tryout.date.strftime('%B %d') }} - {{ tryout.end_date.strftime('%B %d, %Y') }} + {% else %} + {{ tryout.date.strftime('%A, %B %d, %Y') }} + {% endif %} +
Location @@ -90,7 +104,7 @@
{% endif %} - {% if can_edit %} + {% if can_edit and not tryout.is_ended %}

Add Player to Tryout

@@ -176,12 +190,14 @@ {% endif %} + {% if not tryout.is_ended %}
+ {% endif %} {% endif %} @@ -201,14 +217,14 @@

Teams

- {% if can_edit %} + {% if can_edit and not tryout.is_ended %} {% endif %}
- {% if can_edit %} + {% if can_edit and not tryout.is_ended %} {% set gamertags = profile_user.gamertags %} diff --git a/migrations/add_tryout_coaches.py b/migrations/add_tryout_coaches.py new file mode 100644 index 0000000..a1e26be --- /dev/null +++ b/migrations/add_tryout_coaches.py @@ -0,0 +1,64 @@ +"""Migration: Create tryout_coaches association table and migrate existing data. + +Run this script to create the many-to-many relationship between tryouts and coaches. +Usage: python migrations/add_tryout_coaches.py +""" + +import sys +import os +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from app.app import create_app +from app.extensions import db +from sqlalchemy import text + +app = create_app() + +with app.app_context(): + # Check if table already exists + result = db.session.execute(text( + "SELECT COUNT(*) FROM information_schema.tables " + "WHERE table_name = 'tryout_coaches'" + )) + exists = result.scalar() > 0 + + if exists: + print("Table 'tryout_coaches' already exists. Skipping creation.") + else: + db.session.execute(text(""" + CREATE TABLE tryout_coaches ( + tryout_id INTEGER NOT NULL, + coach_id INTEGER NOT NULL, + PRIMARY KEY (tryout_id, coach_id), + FOREIGN KEY (tryout_id) REFERENCES tryouts (id) ON DELETE CASCADE, + FOREIGN KEY (coach_id) REFERENCES users (id) ON DELETE CASCADE + ) + """)) + db.session.commit() + print("Created 'tryout_coaches' association table.") + + # Migrate existing coach_id data into the new table + result = db.session.execute(text( + "SELECT COUNT(*) FROM tryouts WHERE coach_id IS NOT NULL" + )) + count = result.scalar() + + if count > 0: + # Check how many already migrated + migrated = db.session.execute(text( + "SELECT COUNT(*) FROM tryout_coaches" + )).scalar() + + if migrated == 0: + db.session.execute(text(""" + INSERT INTO tryout_coaches (tryout_id, coach_id) + SELECT id, coach_id FROM tryouts WHERE coach_id IS NOT NULL + """)) + db.session.commit() + print(f"Migrated {count} existing coach assignments to tryout_coaches.") + else: + print(f"Skipping data migration — {migrated} rows already exist in tryout_coaches.") + else: + print("No existing coach assignments to migrate.") + + print("Migration complete.") \ No newline at end of file diff --git a/migrations/add_tryout_end_date.py b/migrations/add_tryout_end_date.py deleted file mode 100644 index 42f7158..0000000 --- a/migrations/add_tryout_end_date.py +++ /dev/null @@ -1,38 +0,0 @@ -"""Migration: Add end_date column to tryouts table. - -Run this script to add the end_date column to the tryouts table. -Usage: python migrations/add_tryout_end_date.py -""" - -import sys -import os -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -from app.app import create_app -from app.extensions import db -from sqlalchemy import text - -app = create_app() - -with app.app_context(): - # Check if column already exists - result = db.session.execute(text( - "SELECT COUNT(*) FROM information_schema.columns " - "WHERE table_name = 'tryouts' AND column_name = 'end_date'" - )) - exists = result.scalar() > 0 - - if exists: - print("Column 'end_date' already exists in 'tryouts' table. Skipping.") - else: - db.session.execute(text( - "ALTER TABLE tryouts ADD COLUMN end_date DATE NULL" - )) - # Backfill: set end_date = date for existing tryouts - db.session.execute(text( - "UPDATE tryouts SET end_date = date WHERE end_date IS NULL" - )) - db.session.commit() - print("Successfully added 'end_date' column to 'tryouts' table and backfilled existing rows.") - - print("Migration complete.") \ No newline at end of file From f89e4deb3059c8d0776e9326728a6c767b8c920b Mon Sep 17 00:00:00 2001 From: cedrick2711 Date: Thu, 6 Aug 2026 15:26:00 -0400 Subject: [PATCH 3/4] =?UTF-8?q?r=C3=A9gler=20probl=C3=A8me=20avec=20mise?= =?UTF-8?q?=20a=20jour=20des=20status=20de=20message=20du=20discord=20bot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/discord_bot.py | 33 ++++++++++++++++++++++++++++ app/templates/pages/view_tryout.html | 14 ++++++++++++ discord_pending.json | 1 + 3 files changed, 48 insertions(+) create mode 100644 discord_pending.json diff --git a/app/discord_bot.py b/app/discord_bot.py index 6ce8af0..c1bf3c8 100644 --- a/app/discord_bot.py +++ b/app/discord_bot.py @@ -7,6 +7,7 @@ This module provides a persistent bot that handles: """ import os +import json import logging import asyncio import threading @@ -26,6 +27,9 @@ DISCORD_BOT_TOKEN = os.getenv('DISCORD_BOT_TOKEN') # Configure logging logger = logging.getLogger(__name__) +# File for persisting pending requests across bot restarts +PENDING_FILE = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'discord_pending.json') + # Emoji constants CHECK_EMOJI = '✅' # Green checkmark CROSS_EMOJI = '❌' # Red X @@ -53,8 +57,31 @@ class TeamTryoutsBot(commands.Bot): self.scheduler = AsyncIOScheduler() self.timezone = ZoneInfo('America/Toronto') # EDT timezone + def _load_pending(self): + """Load pending requests from the JSON file.""" + try: + if os.path.exists(PENDING_FILE): + with open(PENDING_FILE, 'r') as f: + data = json.load(f) + # Convert string keys back to int + self.pending_requests = {int(k): v for k, v in data.items()} + logger.info(f"Loaded {len(self.pending_requests)} pending requests from {PENDING_FILE}") + else: + logger.info("No pending requests file found, starting fresh.") + except Exception as e: + logger.error(f"Error loading pending requests: {e}") + + def _save_pending(self): + """Save pending requests to the JSON file.""" + try: + with open(PENDING_FILE, 'w') as f: + json.dump(self.pending_requests, f, indent=2) + except Exception as e: + logger.error(f"Error saving pending requests: {e}") + async def setup_hook(self): """Called when the bot is ready.""" + self._load_pending() logger.info(f'TeamTryoutsBot logged in as {self.user}') async def on_ready(self): @@ -208,6 +235,7 @@ class TeamTryoutsBot(commands.Bot): # Track this pending request self.pending_requests[msg.id] = {'type': 'one_on_one', 'id': request_id} + self._save_pending() logger.info(f"Sent One on One DM with reactions, message_id={msg.id}") return msg.id @@ -266,6 +294,7 @@ class TeamTryoutsBot(commands.Bot): # Track this pending request self.pending_requests[msg.id] = {'type': 'schedule_addition', 'id': reference_id, 'event_type': event_type} + self._save_pending() logger.info(f"Sent {event_type} schedule notification to {db_user.username}, message_id={msg.id}") return msg.id @@ -314,6 +343,7 @@ class TeamTryoutsBot(commands.Bot): approved=True ) del self.pending_requests[message_id] + self._save_pending() except Exception as e: logger.error(f"Error handling approval: {e}\n{traceback.format_exc()}") @@ -371,6 +401,7 @@ class TeamTryoutsBot(commands.Bot): refusal_note=refusal_note ) del self.pending_requests[message_id] + self._save_pending() except Exception as e: logger.error(f"Error handling rejection: {e}\n{traceback.format_exc()}") @@ -399,6 +430,7 @@ class TeamTryoutsBot(commands.Bot): await channel.send("✅ Your attendance has been confirmed!") del self.pending_requests[message_id] + self._save_pending() except Exception as e: logger.error(f"Error handling attendance confirmation: {e}\n{traceback.format_exc()}") @@ -427,6 +459,7 @@ class TeamTryoutsBot(commands.Bot): await channel.send("❌ Your attendance has been declined.") del self.pending_requests[message_id] + self._save_pending() except Exception as e: logger.error(f"Error handling attendance decline: {e}\n{traceback.format_exc()}") diff --git a/app/templates/pages/view_tryout.html b/app/templates/pages/view_tryout.html index 13caa2f..8ec6a85 100644 --- a/app/templates/pages/view_tryout.html +++ b/app/templates/pages/view_tryout.html @@ -374,6 +374,20 @@ {% endfor %}
{% endif %} + + {% if not can_edit and item.player_presence %} + {% for pp in item.player_presence %} + {% if pp.player_id == current_user.id %} +
+ +
+ {% endif %} + {% endfor %} + {% endif %} {% else %} {% endif %} diff --git a/discord_pending.json b/discord_pending.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/discord_pending.json @@ -0,0 +1 @@ +{} \ No newline at end of file From bb0bc1c19229833ea87a9b881b9b642118bb6083 Mon Sep 17 00:00:00 2001 From: cedrick2711 Date: Thu, 6 Aug 2026 20:13:39 -0400 Subject: [PATCH 4/4] ajout de plateforme de base pour les url TRN --- app/models/_constants.py | 6 ++++++ app/models/user_gamertag.py | 14 +++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/app/models/_constants.py b/app/models/_constants.py index ca26638..180c14a 100644 --- a/app/models/_constants.py +++ b/app/models/_constants.py @@ -52,6 +52,12 @@ PLATFORM_CODES = { 'Epic': 'epic', } +PLATFORM_DEFAULTS = { + 'Apex Legends': 'pc', + 'Rainbow Six Siege': 'ubi', + 'Rocket League': 'epic' +} + TRN_URLS = { 'Valorant': 'https://tracker.gg/valorant/profile/riot/{username}', 'League of Legends': 'https://tracker.gg/lol/profile/{username}', diff --git a/app/models/user_gamertag.py b/app/models/user_gamertag.py index aa85eec..ed8b729 100644 --- a/app/models/user_gamertag.py +++ b/app/models/user_gamertag.py @@ -1,6 +1,6 @@ """Store gamertag per game for each user.""" from app.extensions import db -from app.models._constants import TRN_URLS, PLATFORM_CODES +from app.models._constants import TRN_URLS, PLATFORM_CODES, PLATFORM_DEFAULTS from urllib.parse import quote @@ -24,17 +24,21 @@ class UserGamertag(db.Model): return None url = TRN_URLS[self.game] encoded_gamertag = quote(self.gamertag, safe='') + # Resolve platform: use user's selection, or fall back to game default + platform = self.platform + if not platform: + platform = PLATFORM_DEFAULTS.get(self.game, '') if '{platform_code}' in url and '{username}' in url: platform_code = PLATFORM_CODES.get( - self.platform, - self.platform.lower().replace(' ', '-') if self.platform else '', + platform, + platform.lower().replace(' ', '-') if platform else '', ) return url.format(platform_code=platform_code, username=encoded_gamertag) elif '{platform}' in url and '{username}' in url: return url.format( - platform=self.platform.lower().replace(' ', '-'), + platform=platform.lower().replace(' ', '-') if platform else '', username=encoded_gamertag, ) elif '{username}' in url: return url.format(username=encoded_gamertag) - return url \ No newline at end of file + return url