Compare commits
2
Commits
cf551867c9
...
e4a271cfd8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4a271cfd8 | ||
|
|
742995d3cd |
+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)
|
||||||
|
|
||||||
|
|||||||
+192
-15
@@ -16,6 +16,7 @@ from app.models import (
|
|||||||
ESPORT_GAMES,
|
ESPORT_GAMES,
|
||||||
GAME_POSITIONS,
|
GAME_POSITIONS,
|
||||||
Admin,
|
Admin,
|
||||||
|
AppSettings,
|
||||||
Coach,
|
Coach,
|
||||||
Evaluation,
|
Evaluation,
|
||||||
Manager,
|
Manager,
|
||||||
@@ -30,7 +31,6 @@ from app.models import (
|
|||||||
Tryout,
|
Tryout,
|
||||||
TryoutRegistration,
|
TryoutRegistration,
|
||||||
User,
|
User,
|
||||||
AppSettings,
|
|
||||||
)
|
)
|
||||||
from app.time_utils import utc_now_naive
|
from app.time_utils import utc_now_naive
|
||||||
from app.validators import (
|
from app.validators import (
|
||||||
@@ -38,6 +38,7 @@ from app.validators import (
|
|||||||
TryoutRegistrationStatusSchema,
|
TryoutRegistrationStatusSchema,
|
||||||
TryoutSchema,
|
TryoutSchema,
|
||||||
TryoutStatusSchema,
|
TryoutStatusSchema,
|
||||||
|
TryoutTeamMemberPositionSchema,
|
||||||
TryoutTeamMemberSchema,
|
TryoutTeamMemberSchema,
|
||||||
TryoutTeamSchema,
|
TryoutTeamSchema,
|
||||||
)
|
)
|
||||||
@@ -140,7 +141,10 @@ def list_tryouts():
|
|||||||
def create_tryout():
|
def create_tryout():
|
||||||
"""Create a new tryout event. Requires Admin or Manager."""
|
"""Create a new tryout event. Requires Admin or Manager."""
|
||||||
if tryouts_locked():
|
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'))
|
return redirect(url_for('tryouts.list_tryouts'))
|
||||||
|
|
||||||
if not can_manage():
|
if not can_manage():
|
||||||
@@ -204,7 +208,10 @@ def edit_tryout(tryout_id):
|
|||||||
tryout = db.get_or_404(Tryout, tryout_id)
|
tryout = db.get_or_404(Tryout, tryout_id)
|
||||||
|
|
||||||
if tryouts_locked():
|
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))
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id))
|
||||||
|
|
||||||
if not current_user.can_manage_this_tryout(tryout):
|
if not current_user.can_manage_this_tryout(tryout):
|
||||||
@@ -273,9 +280,7 @@ def view_tryout(tryout_id):
|
|||||||
can_view = False
|
can_view = False
|
||||||
if isinstance(current_user, Admin):
|
if isinstance(current_user, Admin):
|
||||||
can_view = True
|
can_view = True
|
||||||
elif isinstance(current_user, Manager):
|
elif isinstance(current_user, (Manager, Coach)):
|
||||||
can_view = current_user.can_manage_this_tryout(tryout)
|
|
||||||
elif isinstance(current_user, Coach):
|
|
||||||
can_view = current_user.can_manage_this_tryout(tryout)
|
can_view = current_user.can_manage_this_tryout(tryout)
|
||||||
elif isinstance(current_user, Player):
|
elif isinstance(current_user, Player):
|
||||||
is_registered = (
|
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])
|
member_players = _users_by_id([m.player_id for m in member_rows if m.player_id])
|
||||||
for row in member_rows:
|
for row in member_rows:
|
||||||
members_by_team.setdefault(row.team_id, []).append(
|
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]
|
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."""
|
"""Update the status of a tryout."""
|
||||||
tryout = db.get_or_404(Tryout, tryout_id)
|
tryout = db.get_or_404(Tryout, tryout_id)
|
||||||
if tryouts_locked():
|
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))
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||||
|
|
||||||
if not current_user.can_manage_this_tryout(tryout):
|
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."""
|
"""Update a registration's attendance status."""
|
||||||
tryout = db.get_or_404(Tryout, tryout_id)
|
tryout = db.get_or_404(Tryout, tryout_id)
|
||||||
if tryouts_locked():
|
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))
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||||
|
|
||||||
if not current_user.can_manage_this_tryout(tryout):
|
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)."""
|
"""Manually register a player for a tryout (by managers/coaches)."""
|
||||||
tryout = locked_tryout_or_404(tryout_id)
|
tryout = locked_tryout_or_404(tryout_id)
|
||||||
if tryouts_locked():
|
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))
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||||
|
|
||||||
if not current_user.can_manage_this_tryout(tryout):
|
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)."""
|
"""Remove a registered player from a tryout (cascades to teams/matches)."""
|
||||||
tryout = db.get_or_404(Tryout, tryout_id)
|
tryout = db.get_or_404(Tryout, tryout_id)
|
||||||
if tryouts_locked():
|
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))
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||||
|
|
||||||
if not current_user.can_manage_this_tryout(tryout):
|
if not current_user.can_manage_this_tryout(tryout):
|
||||||
@@ -643,7 +665,10 @@ def create_team(tryout_id):
|
|||||||
"""Create a tryout-specific team."""
|
"""Create a tryout-specific team."""
|
||||||
tryout = db.get_or_404(Tryout, tryout_id)
|
tryout = db.get_or_404(Tryout, tryout_id)
|
||||||
if tryouts_locked():
|
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))
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||||
|
|
||||||
if not current_user.can_manage_this_tryout(tryout):
|
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)
|
team = db.get_or_404(Team, team_id)
|
||||||
tryout = db.get_or_404(Tryout, tryout_id)
|
tryout = db.get_or_404(Tryout, tryout_id)
|
||||||
if tryouts_locked():
|
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))
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||||
|
|
||||||
if not current_user.can_manage_this_tryout(tryout):
|
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))
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||||
|
|
||||||
|
|
||||||
|
@tryouts_bp.route('/<int:tryout_id>/team/<int:team_id>/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('/<int:tryout_id>/team/<int:team_id>/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('/<int:tryout_id>/team/<int:team_id>/remove/<int:player_id>', 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(
|
||||||
|
'/<int:tryout_id>/team/<int:team_id>/member/<int:member_id>/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('/<int:tryout_id>/delete', methods=['POST'])
|
@tryouts_bp.route('/<int:tryout_id>/delete', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def delete_tryout(tryout_id):
|
def delete_tryout(tryout_id):
|
||||||
"""Delete a tryout and all associated data (matches, teams, registrations, evaluations)."""
|
"""Delete a tryout and all associated data (matches, teams, registrations, evaluations)."""
|
||||||
tryout = db.get_or_404(Tryout, tryout_id)
|
tryout = db.get_or_404(Tryout, tryout_id)
|
||||||
if tryouts_locked():
|
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))
|
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||||
|
|
||||||
if not current_user.can_manage_this_tryout(tryout):
|
if not current_user.can_manage_this_tryout(tryout):
|
||||||
@@ -757,4 +934,4 @@ def delete_tryout(tryout_id):
|
|||||||
db.session.delete(tryout)
|
db.session.delete(tryout)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_('Tryout deleted successfully.'), 'success')
|
flash(_('Tryout deleted successfully.'), 'success')
|
||||||
return redirect(url_for('tryouts.list_tryouts'))
|
return redirect(url_for('tryouts.list_tryouts'))
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -249,7 +249,14 @@
|
|||||||
|
|
||||||
{% for team in team_data %}
|
{% for team in team_data %}
|
||||||
<div class="team-card mb-3">
|
<div class="team-card mb-3">
|
||||||
<h4 class="team-name">{{ team.team.name }}</h4>
|
<div class="team-card-header">
|
||||||
|
<h4 class="team-name">{{ team.team.name }}</h4>
|
||||||
|
{% if can_edit and not tryout.is_ended %}
|
||||||
|
<button class="btn btn-sm btn-outline" data-action="show-modal" data-modal-id="editTeamModal{{ team.team.id }}">
|
||||||
|
<i class="fas fa-edit"></i> {{ _('Edit') }}
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
<ul class="team-members">
|
<ul class="team-members">
|
||||||
{% for member in team.members %}
|
{% for member in team.members %}
|
||||||
<li>
|
<li>
|
||||||
@@ -263,32 +270,96 @@
|
|||||||
<li class="text-muted">{{ _('No players assigned yet.') }}</li>
|
<li class="text-muted">{{ _('No players assigned yet.') }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% if can_edit and not tryout.is_ended and registered_players %}
|
|
||||||
{% set positions = game_positions.get(tryout.game, []) %}
|
|
||||||
<form method="POST" action="{{ url_for('tryouts.add_to_team', tryout_id=tryout.id, team_id=team.team.id) }}" class="form-inline mt-2">
|
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
|
||||||
<select name="player_id" class="form-select" required>
|
|
||||||
<option value="">{{ _('Select player...') }}</option>
|
|
||||||
{% for p in registered_players %}
|
|
||||||
{% if p.id not in team.members | map(attribute='player') | map(attribute='id') | list %}
|
|
||||||
<option value="{{ p.id }}">{{ p.username }}</option>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
{% if positions %}
|
|
||||||
<select name="position" class="form-select mx-2">
|
|
||||||
<option value="">{{ _('-- Select Position --') }}</option>
|
|
||||||
{% for pos in positions %}
|
|
||||||
<option value="{{ pos }}">{{ pos }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
{% else %}
|
|
||||||
<input type="text" name="position" placeholder="{{ _('Position') }}" class="form-input mx-2">
|
|
||||||
{% endif %}
|
|
||||||
<button type="submit" class="btn btn-sm btn-primary">{{ _('Add') }}</button>
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if can_edit and not tryout.is_ended %}
|
||||||
|
{% set positions = game_positions.get(tryout.game, []) %}
|
||||||
|
<!-- Edit Team Modal -->
|
||||||
|
<div id="editTeamModal{{ team.team.id }}" class="modal hidden">
|
||||||
|
<div class="modal-backdrop" data-action="hide-modal" data-modal-id="editTeamModal{{ team.team.id }}"></div>
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3><i class="fas fa-users-cog"></i> {{ _('Edit Team: %(name)s', name=team.team.name) }}</h3>
|
||||||
|
<button class="modal-close" data-action="hide-modal" data-modal-id="editTeamModal{{ team.team.id }}">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<!-- Rename -->
|
||||||
|
<form method="POST" action="{{ url_for('tryouts.edit_team', tryout_id=tryout.id, team_id=team.team.id) }}" class="form-inline mb-3">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
|
<label for="renameInput{{ team.team.id }}" class="mr-2" style="white-space:nowrap;">{{ _('Team name') }}</label>
|
||||||
|
<input type="text" id="renameInput{{ team.team.id }}" name="team_name" value="{{ team.team.name }}" required class="form-input" style="flex:1;">
|
||||||
|
<button type="submit" class="btn btn-sm btn-primary ml-2">{{ _('Rename') }}</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<hr class="section-divider">
|
||||||
|
|
||||||
|
<!-- Roster -->
|
||||||
|
<h5 class="mb-2"><i class="fas fa-users"></i> {{ _('Roster') }}</h5>
|
||||||
|
<div class="roster-rows">
|
||||||
|
{% for member in team.members %}
|
||||||
|
<div class="roster-row">
|
||||||
|
<div class="avatar-sm">{{ member.player.username[:2] | upper }}</div>
|
||||||
|
<span class="roster-player-name">{{ member.player.username }}</span>
|
||||||
|
<form method="POST" action="{{ url_for('tryouts.update_member_position', tryout_id=tryout.id, team_id=team.team.id, member_id=member.member_id) }}" class="inline-form">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
|
<select name="position" class="form-select roster-role-select" data-submit-on-change>
|
||||||
|
<option value="">{{ _('No role') }}</option>
|
||||||
|
{% for pos in positions %}
|
||||||
|
<option value="{{ pos }}" {% if member.position == pos %}selected{% endif %}>{{ pos }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</form>
|
||||||
|
<form method="POST" action="{{ url_for('tryouts.remove_from_team', tryout_id=tryout.id, team_id=team.team.id, player_id=member.player_id) }}" class="inline-form" data-confirm="{{ _('Remove %(player)s from this team?', player=member.player.username) }}">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
|
<button type="submit" class="btn btn-sm btn-danger roster-remove-btn" title="{{ _('Remove from team') }}">
|
||||||
|
<i class="fas fa-user-minus"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted text-center">{{ _('No players assigned yet.') }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if registered_players %}
|
||||||
|
<hr class="section-divider">
|
||||||
|
<h5 class="mb-2"><i class="fas fa-user-plus"></i> {{ _('Add Player') }}</h5>
|
||||||
|
<form method="POST" action="{{ url_for('tryouts.add_to_team', tryout_id=tryout.id, team_id=team.team.id) }}" class="form-inline">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
|
<select name="player_id" class="form-select" required style="flex:1;">
|
||||||
|
<option value="">{{ _('Select player...') }}</option>
|
||||||
|
{% for p in registered_players %}
|
||||||
|
{% if p.id not in team.members | map(attribute='player') | map(attribute='id') | list %}
|
||||||
|
<option value="{{ p.id }}">{{ p.username }}</option>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
{% if positions %}
|
||||||
|
<select name="position" class="form-select mx-2">
|
||||||
|
<option value="">{{ _('-- Select Position --') }}</option>
|
||||||
|
{% for pos in positions %}
|
||||||
|
<option value="{{ pos }}">{{ pos }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
{% else %}
|
||||||
|
<input type="text" name="position" placeholder="{{ _('Position') }}" class="form-input mx-2" style="max-width:140px;">
|
||||||
|
{% endif %}
|
||||||
|
<button type="submit" class="btn btn-sm btn-primary">{{ _('Add') }}</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<hr class="section-divider" style="border-color: var(--danger);">
|
||||||
|
<div class="text-center">
|
||||||
|
<form method="POST" action="{{ url_for('tryouts.delete_team', tryout_id=tryout.id, team_id=team.team.id) }}" class="inline-form" data-confirm="{{ _('Delete this team? Players already in the team will be removed and matches referencing it will be unlinked.') }}">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
|
<button type="submit" class="btn btn-danger">{{ _('Delete Team') }}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="text-muted text-center">{{ _('No teams created yet.') }}</p>
|
<p class="text-muted text-center">{{ _('No teams created yet.') }}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -458,6 +529,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') }}
|
||||||
@@ -568,8 +645,56 @@
|
|||||||
transform: scale(1.08);
|
transform: scale(1.08);
|
||||||
box-shadow: 0 1px 3px rgba(0,0,0,0.12);
|
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);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<script nonce="{{ csp_nonce }}">
|
<script nonce="{{ csp_nonce }}">
|
||||||
|
function showModal(modalId) {
|
||||||
|
document.getElementById(modalId).classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideModal(modalId) {
|
||||||
|
document.getElementById(modalId).classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
function showCreateTeam() { document.getElementById('createTeamForm').classList.remove('hidden'); }
|
function showCreateTeam() { document.getElementById('createTeamForm').classList.remove('hidden'); }
|
||||||
function hideCreateTeam() { document.getElementById('createTeamForm').classList.add('hidden'); }
|
function hideCreateTeam() { document.getElementById('createTeamForm').classList.add('hidden'); }
|
||||||
|
|
||||||
@@ -627,6 +752,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
registerActions({
|
registerActions({
|
||||||
'show-create-team': showCreateTeam,
|
'show-create-team': showCreateTeam,
|
||||||
'hide-create-team': hideCreateTeam,
|
'hide-create-team': hideCreateTeam,
|
||||||
|
'show-modal': function (element) {
|
||||||
|
showModal(element.getAttribute('data-modal-id'));
|
||||||
|
},
|
||||||
|
'hide-modal': function (element) {
|
||||||
|
hideModal(element.getAttribute('data-modal-id'));
|
||||||
|
},
|
||||||
'toggle-tryout-presence': function (element) {
|
'toggle-tryout-presence': function (element) {
|
||||||
toggleTryoutPresence(
|
toggleTryoutPresence(
|
||||||
element.getAttribute('data-match-id'),
|
element.getAttribute('data-match-id'),
|
||||||
|
|||||||
@@ -605,6 +605,18 @@ class TryoutTeamMemberSchema(StripMixin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TryoutTeamMemberPositionSchema(StripMixin):
|
||||||
|
"""Update a team member's in-game position (role)."""
|
||||||
|
|
||||||
|
position = fields.String(
|
||||||
|
load_default='',
|
||||||
|
validate=validate.Length(
|
||||||
|
max=50,
|
||||||
|
error=_l('Position must be 50 characters or less.'),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class NoteContentSchema(StripMixin):
|
class NoteContentSchema(StripMixin):
|
||||||
"""Bounded text stored as a team or personal coaching note."""
|
"""Bounded text stored as a team or personal coaching 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