fix probleme de match dans les calendriers et ajout de boutton delete pour les matchs
CI - Security, Lint & Tests / validate (push) Failing after 1m0s
CI - Security, Lint & Tests / validate (push) Failing after 1m0s
This commit is contained in:
+125
-61
@@ -26,11 +26,13 @@ from app.models import (
|
|||||||
PlayerDisponibility,
|
PlayerDisponibility,
|
||||||
Scout,
|
Scout,
|
||||||
Team,
|
Team,
|
||||||
|
TeamMatch,
|
||||||
TeamMember,
|
TeamMember,
|
||||||
Tryout,
|
Tryout,
|
||||||
TryoutRegistration,
|
TryoutRegistration,
|
||||||
User,
|
User,
|
||||||
)
|
)
|
||||||
|
from app.permissions import visible_org_teams
|
||||||
from app.services.scheduling import notify_participants, zip_participants
|
from app.services.scheduling import notify_participants, zip_participants
|
||||||
from app.validators import MatchEditSchema, MatchSchema
|
from app.validators import MatchEditSchema, MatchSchema
|
||||||
|
|
||||||
@@ -74,6 +76,25 @@ def default_end_time(date, start_time):
|
|||||||
return (datetime.combine(date, start_time) + timedelta(minutes=DEFAULT_MATCH_MINUTES)).time()
|
return (datetime.combine(date, start_time) + timedelta(minutes=DEFAULT_MATCH_MINUTES)).time()
|
||||||
|
|
||||||
|
|
||||||
|
def calendar_event_time_fields(date, start_time, end_time):
|
||||||
|
"""FullCalendar date/time fields for a match.
|
||||||
|
|
||||||
|
A date-only event renders in the all-day row; a ``start``/``end`` with a
|
||||||
|
time places it on the clock for the same calendar day. Matches, team
|
||||||
|
matches and one-on-one sessions are stored as a date plus optional time
|
||||||
|
columns, so the feed translates that into the shape FullCalendar expects.
|
||||||
|
"""
|
||||||
|
if not start_time:
|
||||||
|
return {'date': date.strftime('%Y-%m-%d'), 'allDay': True}
|
||||||
|
fields = {
|
||||||
|
'start': f'{date.isoformat()}T{start_time.strftime("%H:%M:%S")}',
|
||||||
|
'allDay': False,
|
||||||
|
}
|
||||||
|
if end_time:
|
||||||
|
fields['end'] = f'{date.isoformat()}T{end_time.strftime("%H:%M:%S")}'
|
||||||
|
return fields
|
||||||
|
|
||||||
|
|
||||||
def create_participants(match, data):
|
def create_participants(match, data):
|
||||||
"""Attach participants to a match, per its type.
|
"""Attach participants to a match, per its type.
|
||||||
|
|
||||||
@@ -165,13 +186,17 @@ def api_events():
|
|||||||
president — and then issue one MatchParticipant query per match to find
|
president — and then issue one MatchParticipant query per match to find
|
||||||
out whether the viewer was in it. The calendar's cost grew with the
|
out whether the viewer was in it. The calendar's cost grew with the
|
||||||
whole history, on every navigation.
|
whole history, on every navigation.
|
||||||
|
|
||||||
|
Tryout matches, regular-season team matches and approved one-on-one
|
||||||
|
sessions share this feed. Timed rows carry `start`/`end` so
|
||||||
|
FullCalendar places them on the clock instead of the all-day row.
|
||||||
"""
|
"""
|
||||||
events = []
|
events = []
|
||||||
tryouts = get_visible_tryouts_for_user()
|
tryouts = get_visible_tryouts_for_user()
|
||||||
|
window_start, window_end = calendar_window(request.args)
|
||||||
tryouts_by_id = {tryout.id: tryout for tryout in tryouts}
|
tryouts_by_id = {tryout.id: tryout for tryout in tryouts}
|
||||||
|
|
||||||
if tryouts_by_id:
|
if tryouts_by_id:
|
||||||
window_start, window_end = calendar_window(request.args)
|
|
||||||
query = Match.query.filter(Match.tryout_id.in_(tryouts_by_id))
|
query = Match.query.filter(Match.tryout_id.in_(tryouts_by_id))
|
||||||
if window_start:
|
if window_start:
|
||||||
query = query.filter(Match.date >= window_start)
|
query = query.filter(Match.date >= window_start)
|
||||||
@@ -225,30 +250,71 @@ def api_events():
|
|||||||
|
|
||||||
user_participant = mine_by_match.get(match.id)
|
user_participant = mine_by_match.get(match.id)
|
||||||
|
|
||||||
events.append(
|
event = {
|
||||||
{
|
'id': f'match_{match.id}',
|
||||||
'id': f'match_{match.id}',
|
'title': match.title,
|
||||||
'title': match.title,
|
'type': 'match',
|
||||||
'date': match.date.strftime('%Y-%m-%d'),
|
'color': match_color,
|
||||||
'type': 'match',
|
'extendedProps': {
|
||||||
'color': match_color,
|
'location': match.location or tryout.location or 'TBD',
|
||||||
'extendedProps': {
|
'status': match.status,
|
||||||
'location': match.location or tryout.location or 'TBD',
|
'description': match.description or '',
|
||||||
'status': match.status,
|
'match_type': match.match_type,
|
||||||
'description': match.description or '',
|
'tryout_id': tryout.id,
|
||||||
'match_type': match.match_type,
|
'match_id': match.id,
|
||||||
'tryout_id': tryout.id,
|
'start_time': start_time_str,
|
||||||
'match_id': match.id,
|
'end_time': end_time_str,
|
||||||
'start_time': start_time_str,
|
'participants': participants_str,
|
||||||
'end_time': end_time_str,
|
'user_participant_id': user_participant.id if user_participant else None,
|
||||||
'participants': participants_str,
|
'user_attendance_confirmed': user_participant.attendance_confirmed
|
||||||
'user_participant_id': user_participant.id if user_participant else None,
|
if user_participant
|
||||||
'user_attendance_confirmed': user_participant.attendance_confirmed
|
else False,
|
||||||
if user_participant
|
},
|
||||||
else False,
|
}
|
||||||
},
|
event.update(calendar_event_time_fields(match.date, match.start_time, match.end_time))
|
||||||
}
|
events.append(event)
|
||||||
)
|
|
||||||
|
# Regular-season team matches: a separate table from tryout matches, but
|
||||||
|
# the same calendar. Visible to the roles that can see the org team.
|
||||||
|
if isinstance(current_user, (Admin, Manager)):
|
||||||
|
team_query = TeamMatch.query
|
||||||
|
elif isinstance(current_user, (Coach, Player)):
|
||||||
|
team_ids = [team.id for team in visible_org_teams(current_user)]
|
||||||
|
team_query = (
|
||||||
|
TeamMatch.query.filter(TeamMatch.org_team_id.in_(team_ids))
|
||||||
|
if team_ids
|
||||||
|
else TeamMatch.query.filter(TeamMatch.id == -1)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
team_query = TeamMatch.query.filter(TeamMatch.id == -1)
|
||||||
|
|
||||||
|
if window_start:
|
||||||
|
team_query = team_query.filter(TeamMatch.date >= window_start)
|
||||||
|
if window_end:
|
||||||
|
team_query = team_query.filter(TeamMatch.date <= window_end)
|
||||||
|
|
||||||
|
for team_match in team_query.options(joinedload(TeamMatch.org_team)).all():
|
||||||
|
start_time_str = team_match.start_time.strftime('%H:%M') if team_match.start_time else None
|
||||||
|
end_time_str = team_match.end_time.strftime('%H:%M') if team_match.end_time else None
|
||||||
|
event = {
|
||||||
|
'id': f'team_match_{team_match.id}',
|
||||||
|
'title': team_match.title,
|
||||||
|
'type': 'team_match',
|
||||||
|
'color': '#3b82f6',
|
||||||
|
'extendedProps': {
|
||||||
|
'location': team_match.location or 'TBD',
|
||||||
|
'status': team_match.status,
|
||||||
|
'description': team_match.description or '',
|
||||||
|
'org_team': team_match.org_team.name if team_match.org_team else '',
|
||||||
|
'opponent': team_match.opponent or '',
|
||||||
|
'start_time': start_time_str,
|
||||||
|
'end_time': end_time_str,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
event.update(
|
||||||
|
calendar_event_time_fields(team_match.date, team_match.start_time, team_match.end_time)
|
||||||
|
)
|
||||||
|
events.append(event)
|
||||||
|
|
||||||
# Add approved One on One sessions for the current user (player or coach)
|
# Add approved One on One sessions for the current user (player or coach)
|
||||||
if isinstance(current_user, Player):
|
if isinstance(current_user, Player):
|
||||||
@@ -263,23 +329,22 @@ def api_events():
|
|||||||
one_on_ones = []
|
one_on_ones = []
|
||||||
|
|
||||||
for ooo in one_on_ones:
|
for ooo in one_on_ones:
|
||||||
events.append(
|
event = {
|
||||||
{
|
'id': f'one_on_one_{ooo.id}',
|
||||||
'id': f'one_on_one_{ooo.id}',
|
'title': f'1:1 - {ooo.player.full_name} & {ooo.coach.full_name}',
|
||||||
'title': f'1:1 - {ooo.player.full_name} & {ooo.coach.full_name}',
|
'type': 'one_on_one',
|
||||||
'date': ooo.date.strftime('%Y-%m-%d'),
|
'color': '#8b5cf6',
|
||||||
'type': 'one_on_one',
|
'extendedProps': {
|
||||||
'color': '#8b5cf6',
|
'location': 'Discord / Voice Chat',
|
||||||
'extendedProps': {
|
'status': 'approved',
|
||||||
'location': 'Discord / Voice Chat',
|
'description': ooo.points or 'One on One session',
|
||||||
'status': 'approved',
|
'start_time': ooo.start_time.strftime('%H:%M') if ooo.start_time else None,
|
||||||
'description': ooo.points or 'One on One session',
|
'end_time': ooo.end_time.strftime('%H:%M') if ooo.end_time else None,
|
||||||
'start_time': ooo.start_time.strftime('%H:%M') if ooo.start_time else None,
|
'participants': f"{ooo.player.full_name} with {ooo.coach.full_name}",
|
||||||
'end_time': ooo.end_time.strftime('%H:%M') if ooo.end_time else None,
|
},
|
||||||
'participants': f"{ooo.player.full_name} with {ooo.coach.full_name}",
|
}
|
||||||
},
|
event.update(calendar_event_time_fields(ooo.date, ooo.start_time, ooo.end_time))
|
||||||
}
|
events.append(event)
|
||||||
)
|
|
||||||
|
|
||||||
return jsonify(events)
|
return jsonify(events)
|
||||||
|
|
||||||
@@ -351,25 +416,24 @@ def api_events_for_tryout(tryout_id):
|
|||||||
start_time_str = match.start_time.strftime('%H:%M') if match.start_time else None
|
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
|
end_time_str = match.end_time.strftime('%H:%M') if match.end_time else None
|
||||||
|
|
||||||
events.append(
|
event = {
|
||||||
{
|
'id': f'match_{match.id}',
|
||||||
'id': f'match_{match.id}',
|
'title': match.title,
|
||||||
'title': match.title,
|
'type': 'match',
|
||||||
'date': match.date.strftime('%Y-%m-%d'),
|
'color': match_color,
|
||||||
'type': 'match',
|
'extendedProps': {
|
||||||
'color': match_color,
|
'location': match.location or tryout.location or 'TBD',
|
||||||
'extendedProps': {
|
'status': match.status,
|
||||||
'location': match.location or tryout.location or 'TBD',
|
'match_type': match.match_type,
|
||||||
'status': match.status,
|
'tryout_id': tryout.id,
|
||||||
'match_type': match.match_type,
|
'match_id': match.id,
|
||||||
'tryout_id': tryout.id,
|
'participants': participants_str,
|
||||||
'match_id': match.id,
|
'start_time': start_time_str,
|
||||||
'participants': participants_str,
|
'end_time': end_time_str,
|
||||||
'start_time': start_time_str,
|
},
|
||||||
'end_time': end_time_str,
|
}
|
||||||
},
|
event.update(calendar_event_time_fields(match.date, match.start_time, match.end_time))
|
||||||
}
|
events.append(event)
|
||||||
)
|
|
||||||
|
|
||||||
return jsonify(events)
|
return jsonify(events)
|
||||||
|
|
||||||
|
|||||||
@@ -346,6 +346,14 @@ function showEventModal(event) {
|
|||||||
grid.appendChild(detailItem('Teams', buildTeamsNode(props), true));
|
grid.appendChild(detailItem('Teams', buildTeamsNode(props), true));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Team match details
|
||||||
|
if (type === 'team_match') {
|
||||||
|
grid.appendChild(detailItem('Team', document.createTextNode(props.org_team || 'Team')));
|
||||||
|
if (props.opponent) {
|
||||||
|
grid.appendChild(detailItem('Opponent', document.createTextNode(props.opponent)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (props.description) {
|
if (props.description) {
|
||||||
grid.appendChild(detailItem('Description', multilineNode(props.description), true));
|
grid.appendChild(detailItem('Description', multilineNode(props.description), true));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -458,6 +458,12 @@
|
|||||||
<a href="{{ url_for('matches.edit_match', match_id=m.id) }}" class="btn btn-sm btn-outline">
|
<a href="{{ url_for('matches.edit_match', match_id=m.id) }}" class="btn btn-sm btn-outline">
|
||||||
<i class="fas fa-edit"></i> {{ _('Edit') }}
|
<i class="fas fa-edit"></i> {{ _('Edit') }}
|
||||||
</a>
|
</a>
|
||||||
|
<form method="POST" action="{{ url_for('matches.delete_match', match_id=m.id) }}" class="inline-form" data-confirm="{{ _('Delete this match?') }}">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
|
<button type="submit" class="btn btn-sm btn-danger" title="{{ _('Delete Match') }}">
|
||||||
|
<i class="fas fa-trash"></i> {{ _('Delete') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{{ url_for('users.add_note_from_match', match_id=m.id) }}" class="btn btn-sm btn-primary" title="{{ _('Add Note for this Match') }}">
|
<a href="{{ url_for('users.add_note_from_match', match_id=m.id) }}" class="btn btn-sm btn-primary" title="{{ _('Add Note for this Match') }}">
|
||||||
<i class="fas fa-sticky-note"></i> {{ _('Note') }}
|
<i class="fas fa-sticky-note"></i> {{ _('Note') }}
|
||||||
|
|||||||
+103
-3
@@ -168,7 +168,7 @@ class TestTheWindow:
|
|||||||
'/matches/api/events?start=2030-05-01T00:00:00-04:00&end=2030-05-31T00:00:00-04:00'
|
'/matches/api/events?start=2030-05-01T00:00:00-04:00&end=2030-05-31T00:00:00-04:00'
|
||||||
).get_json()
|
).get_json()
|
||||||
|
|
||||||
assert _match_event(payload)['date'] == '2030-05-02'
|
assert _match_event(payload)['start'] == '2030-05-02T18:00:00'
|
||||||
|
|
||||||
def test_a_match_outside_the_window_is_not(
|
def test_a_match_outside_the_window_is_not(
|
||||||
self, app, client, as_role, make_user, match_factory
|
self, app, client, as_role, make_user, match_factory
|
||||||
@@ -192,7 +192,7 @@ class TestTheWindow:
|
|||||||
|
|
||||||
payload = client.get('/matches/api/events').get_json()
|
payload = client.get('/matches/api/events').get_json()
|
||||||
|
|
||||||
assert _match_event(payload)['date'] == '2030-05-02'
|
assert _match_event(payload)['start'] == '2030-05-02T18:00:00'
|
||||||
|
|
||||||
def test_unparseable_bounds_are_ignored_rather_than_rejected(
|
def test_unparseable_bounds_are_ignored_rather_than_rejected(
|
||||||
self, app, client, as_role, make_user, match_factory
|
self, app, client, as_role, make_user, match_factory
|
||||||
@@ -206,7 +206,7 @@ class TestTheWindow:
|
|||||||
response = client.get('/matches/api/events?start=whenever&end=')
|
response = client.get('/matches/api/events?start=whenever&end=')
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert _match_event(response.get_json())['date'] == '2030-05-02'
|
assert _match_event(response.get_json())['start'] == '2030-05-02T18:00:00'
|
||||||
|
|
||||||
def test_the_viewers_own_participation_still_comes_back(
|
def test_the_viewers_own_participation_still_comes_back(
|
||||||
self, app, client, as_role, make_user, match_factory
|
self, app, client, as_role, make_user, match_factory
|
||||||
@@ -258,3 +258,103 @@ class TestTheWindowParser:
|
|||||||
from flask import request
|
from flask import request
|
||||||
|
|
||||||
assert calendar_window(request.args) == (None, None)
|
assert calendar_window(request.args) == (None, None)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTimedEvents:
|
||||||
|
"""Matches with time fields render on the clock, not as all-day."""
|
||||||
|
|
||||||
|
def test_timed_match_has_start_and_end(self, app, client, as_role, make_user, match_factory):
|
||||||
|
player_id = make_user('player')
|
||||||
|
admin_id = as_role('admin')
|
||||||
|
match_factory(admin_id, player_id)
|
||||||
|
|
||||||
|
ev = _match_event(client.get('/matches/api/events').get_json())
|
||||||
|
|
||||||
|
assert ev['start'] == '2030-05-02T18:00:00'
|
||||||
|
assert ev['end'] == '2030-05-02T19:00:00'
|
||||||
|
assert ev['allDay'] is False
|
||||||
|
|
||||||
|
def test_allday_match_has_date_when_no_time(
|
||||||
|
self, app, client, as_role, make_user, match_factory
|
||||||
|
):
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from app.extensions import db
|
||||||
|
from app.models import Match, MatchParticipant, Tryout, TryoutRegistration
|
||||||
|
|
||||||
|
player_id = make_user('player')
|
||||||
|
admin_id = as_role('admin')
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
tryout = Tryout(
|
||||||
|
title='All-day tryout',
|
||||||
|
game='Valorant',
|
||||||
|
date=date(2030, 5, 1),
|
||||||
|
created_by=admin_id,
|
||||||
|
status='upcoming',
|
||||||
|
)
|
||||||
|
db.session.add(tryout)
|
||||||
|
db.session.flush()
|
||||||
|
db.session.add(TryoutRegistration(tryout_id=tryout.id, player_id=player_id))
|
||||||
|
|
||||||
|
match = Match(
|
||||||
|
tryout_id=tryout.id,
|
||||||
|
title='All-day scrim',
|
||||||
|
description=None,
|
||||||
|
date=date(2030, 5, 2),
|
||||||
|
start_time=None,
|
||||||
|
end_time=None,
|
||||||
|
match_type='player_scrim',
|
||||||
|
created_by=admin_id,
|
||||||
|
)
|
||||||
|
db.session.add(match)
|
||||||
|
db.session.flush()
|
||||||
|
db.session.add(MatchParticipant(match_id=match.id, player_id=player_id))
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
payload = client.get('/matches/api/events').get_json()
|
||||||
|
ev = next(e for e in payload if e['title'] == 'All-day scrim')
|
||||||
|
|
||||||
|
assert ev['date'] == '2030-05-02'
|
||||||
|
assert ev['allDay'] is True
|
||||||
|
assert 'start' not in ev
|
||||||
|
|
||||||
|
|
||||||
|
class TestTeamMatchCalendarEvents:
|
||||||
|
"""Regular-season team matches appear on the main calendar."""
|
||||||
|
|
||||||
|
def test_team_matches_appear_for_admin(self, app, client, as_role, make_user):
|
||||||
|
from datetime import date, time
|
||||||
|
|
||||||
|
from app.extensions import db
|
||||||
|
from app.models import OrgTeam, TeamMatch
|
||||||
|
|
||||||
|
admin_id = as_role('admin')
|
||||||
|
with app.app_context():
|
||||||
|
team = OrgTeam(name='Test Org', created_by=admin_id)
|
||||||
|
db.session.add(team)
|
||||||
|
db.session.flush()
|
||||||
|
tm = TeamMatch(
|
||||||
|
org_team_id=team.id,
|
||||||
|
title='Semifinal',
|
||||||
|
opponent='Enemy Team',
|
||||||
|
date=date(2030, 6, 15),
|
||||||
|
start_time=time(20, 0),
|
||||||
|
end_time=time(21, 30),
|
||||||
|
location='Main arena',
|
||||||
|
status='scheduled',
|
||||||
|
created_by=admin_id,
|
||||||
|
)
|
||||||
|
db.session.add(tm)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
payload = client.get('/matches/api/events').get_json()
|
||||||
|
team_events = [e for e in payload if e['type'] == 'team_match']
|
||||||
|
|
||||||
|
assert len(team_events) == 1
|
||||||
|
ev = team_events[0]
|
||||||
|
assert ev['title'] == 'Semifinal'
|
||||||
|
assert ev['start'] == '2030-06-15T20:00:00'
|
||||||
|
assert ev['allDay'] is False
|
||||||
|
assert ev['extendedProps']['org_team'] == 'Test Org'
|
||||||
|
assert ev['extendedProps']['opponent'] == 'Enemy Team'
|
||||||
|
|||||||
Reference in New Issue
Block a user