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:
+76
-12
@@ -26,11 +26,13 @@ from app.models import (
|
||||
PlayerDisponibility,
|
||||
Scout,
|
||||
Team,
|
||||
TeamMatch,
|
||||
TeamMember,
|
||||
Tryout,
|
||||
TryoutRegistration,
|
||||
User,
|
||||
)
|
||||
from app.permissions import visible_org_teams
|
||||
from app.services.scheduling import notify_participants, zip_participants
|
||||
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()
|
||||
|
||||
|
||||
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):
|
||||
"""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
|
||||
out whether the viewer was in it. The calendar's cost grew with the
|
||||
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 = []
|
||||
tryouts = get_visible_tryouts_for_user()
|
||||
window_start, window_end = calendar_window(request.args)
|
||||
tryouts_by_id = {tryout.id: tryout for tryout in tryouts}
|
||||
|
||||
if tryouts_by_id:
|
||||
window_start, window_end = calendar_window(request.args)
|
||||
query = Match.query.filter(Match.tryout_id.in_(tryouts_by_id))
|
||||
if window_start:
|
||||
query = query.filter(Match.date >= window_start)
|
||||
@@ -225,11 +250,9 @@ def api_events():
|
||||
|
||||
user_participant = mine_by_match.get(match.id)
|
||||
|
||||
events.append(
|
||||
{
|
||||
event = {
|
||||
'id': f'match_{match.id}',
|
||||
'title': match.title,
|
||||
'date': match.date.strftime('%Y-%m-%d'),
|
||||
'type': 'match',
|
||||
'color': match_color,
|
||||
'extendedProps': {
|
||||
@@ -248,7 +271,50 @@ def api_events():
|
||||
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)
|
||||
if isinstance(current_user, Player):
|
||||
@@ -263,11 +329,9 @@ def api_events():
|
||||
one_on_ones = []
|
||||
|
||||
for ooo in one_on_ones:
|
||||
events.append(
|
||||
{
|
||||
event = {
|
||||
'id': f'one_on_one_{ooo.id}',
|
||||
'title': f'1:1 - {ooo.player.full_name} & {ooo.coach.full_name}',
|
||||
'date': ooo.date.strftime('%Y-%m-%d'),
|
||||
'type': 'one_on_one',
|
||||
'color': '#8b5cf6',
|
||||
'extendedProps': {
|
||||
@@ -279,7 +343,8 @@ def api_events():
|
||||
'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)
|
||||
|
||||
@@ -351,11 +416,9 @@ def api_events_for_tryout(tryout_id):
|
||||
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
|
||||
|
||||
events.append(
|
||||
{
|
||||
event = {
|
||||
'id': f'match_{match.id}',
|
||||
'title': match.title,
|
||||
'date': match.date.strftime('%Y-%m-%d'),
|
||||
'type': 'match',
|
||||
'color': match_color,
|
||||
'extendedProps': {
|
||||
@@ -369,7 +432,8 @@ def api_events_for_tryout(tryout_id):
|
||||
'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)
|
||||
|
||||
|
||||
@@ -346,6 +346,14 @@ function showEventModal(event) {
|
||||
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) {
|
||||
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">
|
||||
<i class="fas fa-edit"></i> {{ _('Edit') }}
|
||||
</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 %}
|
||||
<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') }}
|
||||
|
||||
+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'
|
||||
).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(
|
||||
self, app, client, as_role, make_user, match_factory
|
||||
@@ -192,7 +192,7 @@ class TestTheWindow:
|
||||
|
||||
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(
|
||||
self, app, client, as_role, make_user, match_factory
|
||||
@@ -206,7 +206,7 @@ class TestTheWindow:
|
||||
response = client.get('/matches/api/events?start=whenever&end=')
|
||||
|
||||
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(
|
||||
self, app, client, as_role, make_user, match_factory
|
||||
@@ -258,3 +258,103 @@ class TestTheWindowParser:
|
||||
from flask import request
|
||||
|
||||
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