Changement du refresh handler, soft reset d'ajouter sur beaucoup de formulaire de modification
CI - Security, Lint & Tests / validate (push) Failing after 1m3s

This commit is contained in:
cedrick2711
2026-09-08 11:27:53 -04:00
parent e2e2b1b07e
commit 4a2e2f053f
42 changed files with 722 additions and 333 deletions
+66 -37
View File
@@ -9,19 +9,35 @@ import os
from datetime import datetime
from flask import (
Blueprint, render_template, redirect, url_for, flash, request,
Blueprint,
flash,
redirect,
render_template,
request,
send_file,
url_for,
)
from flask_login import login_required, current_user
from flask_login import current_user, login_required
from app.extensions import db
from app.models import (
Admin, User, Tryout, OrgTeam, TeamPlayer, TeamMatch,
TeamMatchParticipant, AppSettings, BackupRecord, AuditLog,
Admin,
AppSettings,
AuditLog,
BackupRecord,
OrgTeam,
TeamMatch,
TeamMatchParticipant,
TeamPlayer,
Tryout,
User,
)
from app.responders import respond_after_change
from app.supporting_scripts.backup import (
BACKUP_DIR, BackupError, backup_database, backup_documents,
create_backup_dir, parse_database_url, verify_backup,
BackupError,
backup_database,
create_backup_dir,
parse_database_url,
)
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
@@ -56,6 +72,7 @@ def _log(action, details=None):
# Dashboard
# ---------------------------------------------------------------------------
@admin_bp.route('')
@login_required
def dashboard():
@@ -96,6 +113,7 @@ def dashboard():
# Backups
# ---------------------------------------------------------------------------
def _create_backup_record(backup_type='manual', notes=None):
"""Run a database backup and return a BackupRecord, or raise BackupError."""
create_backup_dir()
@@ -130,11 +148,11 @@ def create_backup():
except BackupError as e:
flash(f'Backup failed: {e}', 'danger')
_log('backup_failed', f'Error: {e}')
return redirect(url_for('admin.dashboard'))
return respond_after_change(url_for('admin.dashboard'))
_log('backup_created', f'File: {record.filename} ({record.size_bytes} bytes)')
flash(f'Backup created successfully: {record.filename}', 'success')
return redirect(url_for('admin.dashboard'))
return respond_after_change(url_for('admin.dashboard'))
@admin_bp.route('/backup/<int:backup_id>/download')
@@ -147,7 +165,7 @@ def download_backup(backup_id):
record = BackupRecord.query.get_or_404(backup_id)
if not os.path.exists(record.file_path):
flash('Backup file is missing from disk.', 'danger')
return redirect(url_for('admin.dashboard'))
return respond_after_change(url_for('admin.dashboard'))
_log('backup_downloaded', f'File: {record.filename}')
return send_file(record.file_path, as_attachment=True, download_name=record.filename)
@@ -166,13 +184,13 @@ def delete_backup(backup_id):
os.remove(record.file_path)
except OSError as e:
flash(f'Could not remove backup file: {e}', 'danger')
return redirect(url_for('admin.dashboard'))
return respond_after_change(url_for('admin.dashboard'))
_log('backup_deleted', f'File: {record.filename}')
db.session.delete(record)
db.session.commit()
flash(f'Backup {record.filename} deleted.', 'success')
return redirect(url_for('admin.dashboard'))
return respond_after_change(url_for('admin.dashboard'))
@admin_bp.route('/backup/<int:backup_id>/restore', methods=['POST'])
@@ -189,7 +207,7 @@ def restore_backup(backup_id):
record = BackupRecord.query.get_or_404(backup_id)
if not os.path.exists(record.file_path):
flash('Backup file is missing from disk.', 'danger')
return redirect(url_for('admin.dashboard'))
return respond_after_change(url_for('admin.dashboard'))
# Create a safety backup of the current state before restoring.
try:
@@ -199,22 +217,31 @@ def restore_backup(backup_id):
)
except BackupError as e:
flash(f'Could not create safety backup, restore aborted: {e}', 'danger')
return redirect(url_for('admin.dashboard'))
return respond_after_change(url_for('admin.dashboard'))
# Restore using pg_restore
import subprocess
from app.supporting_scripts.backup import (
PG_RESTORE, dump_environment, parse_database_url,
PG_RESTORE,
dump_environment,
parse_database_url,
)
conn = parse_database_url(os.getenv('DATABASE_URL'))
cmd = [
PG_RESTORE,
'--host', conn['host'],
'--port', conn['port'],
'--username', conn['user'],
'--dbname', conn['dbname'],
'--clean', '--if-exists', '--no-owner',
'--host',
conn['host'],
'--port',
conn['port'],
'--username',
conn['user'],
'--dbname',
conn['dbname'],
'--clean',
'--if-exists',
'--no-owner',
record.file_path,
]
try:
@@ -230,20 +257,21 @@ def restore_backup(backup_id):
except (subprocess.TimeoutExpired, FileNotFoundError, RuntimeError) as e:
flash(f'Restore failed: {e}', 'danger')
_log('backup_restore_failed', f'File: {record.filename}, Error: {e}')
return redirect(url_for('admin.dashboard'))
return respond_after_change(url_for('admin.dashboard'))
_log('backup_restored', f'File: {record.filename}')
flash(
f'Backup {record.filename} restored successfully. The database has been rolled back.',
'success',
)
return redirect(url_for('admin.dashboard'))
return respond_after_change(url_for('admin.dashboard'))
# ---------------------------------------------------------------------------
# Tryouts open/close toggle
# ---------------------------------------------------------------------------
@admin_bp.route('/toggle-tryouts', methods=['POST'])
@login_required
def toggle_tryouts():
@@ -258,13 +286,14 @@ def toggle_tryouts():
state = 'opened' if new_value else 'closed'
_log('tryouts_toggled', f'Tryouts {state}')
flash(f'Tryouts are now {state}.', 'success')
return redirect(url_for('admin.dashboard'))
return respond_after_change(url_for('admin.dashboard'))
# ---------------------------------------------------------------------------
# Season lifecycle
# ---------------------------------------------------------------------------
@admin_bp.route('/season/start', methods=['POST'])
@login_required
def start_season():
@@ -274,19 +303,19 @@ def start_season():
if AppSettings.get_bool('season_active', default=False):
flash('A season is already active. End it before starting a new one.', 'danger')
return redirect(url_for('admin.dashboard'))
return respond_after_change(url_for('admin.dashboard'))
name = request.form.get('season_name', '').strip()
start_date = request.form.get('season_start', '').strip()
if not start_date:
flash('A season start date is required.', 'danger')
return redirect(url_for('admin.dashboard'))
return respond_after_change(url_for('admin.dashboard'))
try:
datetime.strptime(start_date, '%Y-%m-%d')
except ValueError:
flash('Invalid season start date format.', 'danger')
return redirect(url_for('admin.dashboard'))
return respond_after_change(url_for('admin.dashboard'))
AppSettings.set('season_name', name or 'Untitled Season')
AppSettings.set('season_start', start_date)
@@ -295,7 +324,7 @@ def start_season():
_log('season_started', f'Season: {name or "Untitled Season"}, Start: {start_date}')
flash(f'Season "{name or "Untitled Season"}" has begun.', 'success')
return redirect(url_for('admin.dashboard'))
return respond_after_change(url_for('admin.dashboard'))
@admin_bp.route('/season/end', methods=['POST'])
@@ -307,18 +336,18 @@ def end_season():
if not AppSettings.get_bool('season_active', default=False):
flash('No season is currently active.', 'danger')
return redirect(url_for('admin.dashboard'))
return respond_after_change(url_for('admin.dashboard'))
end_date = request.form.get('season_end', '').strip()
if not end_date:
flash('A season end date is required.', 'danger')
return redirect(url_for('admin.dashboard'))
return respond_after_change(url_for('admin.dashboard'))
try:
datetime.strptime(end_date, '%Y-%m-%d')
except ValueError:
flash('Invalid season end date format.', 'danger')
return redirect(url_for('admin.dashboard'))
return respond_after_change(url_for('admin.dashboard'))
name = AppSettings.get('season_name')
AppSettings.set('season_end', end_date)
@@ -326,7 +355,9 @@ def end_season():
_log('season_ended', f'Season: {name}, End: {end_date}')
flash(f'Season "{name}" has ended.', 'success')
return redirect(url_for('admin.dashboard'))
return respond_after_change(url_for('admin.dashboard'))
@admin_bp.route('/toggle-document-uploads', methods=['POST'])
@login_required
def toggle_document_uploads():
@@ -341,16 +372,14 @@ def toggle_document_uploads():
state = 'enabled' if new_value else 'disabled'
_log('document_uploads_toggled', f'Document uploads {state}')
flash(f'Document uploads are now {state}.', 'success')
return redirect(url_for('admin.dashboard'))
return respond_after_change(url_for('admin.dashboard'))
# ---------------------------------------------------------------------------
# Wipe teams for new season
# ---------------------------------------------------------------------------
@admin_bp.route('/teams/wipe', methods=['POST'])
@login_required
def wipe_teams():
@@ -366,7 +395,7 @@ def wipe_teams():
confirm = request.form.get('confirm', '').strip()
if confirm != 'WIPE':
flash("Type 'WIPE' in the confirmation box to proceed.", 'danger')
return redirect(url_for('admin.dashboard'))
return respond_after_change(url_for('admin.dashboard'))
# Safety backup before destructive operation
try:
@@ -376,7 +405,7 @@ def wipe_teams():
)
except BackupError as e:
flash(f'Wipe aborted — could not create safety backup: {e}', 'danger')
return redirect(url_for('admin.dashboard'))
return respond_after_change(url_for('admin.dashboard'))
roster_count = TeamPlayer.query.count()
match_count = TeamMatch.query.count()
@@ -393,4 +422,4 @@ def wipe_teams():
f'and {match_count} regular-season matches.',
'success',
)
return redirect(url_for('admin.dashboard'))
return respond_after_change(url_for('admin.dashboard'))
+32 -26
View File
@@ -7,20 +7,13 @@ from flask import Blueprint, flash, redirect, render_template, request, url_for
from flask_babel import gettext as _
from flask_login import current_user, login_required
from marshmallow import ValidationError
from flask import Blueprint, render_template, redirect, url_for, flash, request
from flask_login import login_required, current_user
from app.extensions import db
from app.models import (
Admin, Coach, Manager, Player,
User, Tryout, Evaluation, TryoutRegistration,
OrgTeam, GAME_POSITIONS, EVALUATION_CRITERIA,
)
from sqlalchemy import func
from sqlalchemy.orm import aliased
from app.extensions import db
from app.forms import flash_validation_errors, form_payload
from app.models import (
EVALUATION_CRITERIA,
GAME_POSITIONS,
Admin,
Evaluation,
@@ -30,6 +23,7 @@ from app.models import (
User,
)
from app.pagination import paginate
from app.responders import respond_after_change
from app.validators import EvaluationSchema
evaluations_bp = Blueprint('evaluations', __name__, url_prefix='/evaluations')
@@ -157,12 +151,12 @@ def evaluate_player(tryout_id, player_id):
)
if not is_registered:
flash(_('Player is not registered for this tryout.'), 'danger')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout_id))
player = db.get_or_404(User, player_id)
if not isinstance(player, Player):
flash(_('Can only evaluate players.'), 'danger')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout_id))
existing_eval = Evaluation.query.filter_by(
tryout_id=tryout_id,
@@ -216,7 +210,7 @@ def evaluate_player(tryout_id, player_id):
evaluation.position_recommendation = data['position_recommendation']
db.session.commit()
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout_id))
return render_evaluation_form()
@@ -291,23 +285,31 @@ def batch_evaluate(tryout_id):
player = User.query.get(pid)
if not player or not isinstance(player, Player):
continue
is_registered = TryoutRegistration.query.filter_by(
tryout_id=tryout_id, player_id=pid,
).first() is not None
is_registered = (
TryoutRegistration.query.filter_by(
tryout_id=tryout_id,
player_id=pid,
).first()
is not None
)
if not is_registered:
continue
existing = Evaluation.query.filter_by(
tryout_id=tryout_id, player_id=pid, evaluator_id=current_user.id,
tryout_id=tryout_id,
player_id=pid,
evaluator_id=current_user.id,
).first()
existing_scores = {
field_name: getattr(existing, field_name) if existing else None
for field_name, _ in EVALUATION_CRITERIA
}
players.append({
'player': player,
'existing': existing,
'existing_scores': existing_scores,
})
players.append(
{
'player': player,
'existing': existing,
'existing_scores': existing_scores,
}
)
if not players:
flash('No valid players selected for evaluation.', 'danger')
@@ -329,7 +331,8 @@ def batch_evaluate(tryout_id):
_apply_evaluation(existing, scores, comments, position)
else:
evaluation = Evaluation(
tryout_id=tryout_id, player_id=pid,
tryout_id=tryout_id,
player_id=pid,
evaluator_id=current_user.id,
)
_apply_evaluation(evaluation, scores, comments, position)
@@ -338,9 +341,12 @@ def batch_evaluate(tryout_id):
db.session.commit()
flash(f'Saved evaluations for {saved} player(s).', 'success')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout_id))
return render_template('pages/batch_evaluate.html',
tryout=tryout, players=players,
evaluation_criteria=EVALUATION_CRITERIA,
game_positions=GAME_POSITIONS)
return render_template(
'pages/batch_evaluate.html',
tryout=tryout,
players=players,
evaluation_criteria=EVALUATION_CRITERIA,
game_positions=GAME_POSITIONS,
)
+11 -10
View File
@@ -5,7 +5,7 @@ Uses polymorphic isinstance checks instead of role-string comparisons.
from datetime import datetime, timedelta
from flask import Blueprint, flash, jsonify, redirect, render_template, request, url_for
from flask import Blueprint, flash, jsonify, render_template, request, url_for
from flask_babel import gettext as _
from flask_login import current_user, login_required
from marshmallow import ValidationError
@@ -33,6 +33,7 @@ from app.models import (
User,
)
from app.permissions import visible_org_teams
from app.responders import respond_after_change
from app.services.scheduling import notify_participants, zip_participants
from app.validators import MatchEditSchema, MatchSchema
@@ -445,11 +446,11 @@ def create_match(tryout_id):
tryout = db.get_or_404(Tryout, tryout_id)
if not current_user.can_manage_this_tryout(tryout):
flash(_('You do not have permission to schedule matches for this tryout.'), 'danger')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
return respond_after_change(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))
return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout_id))
teams = Team.query.filter_by(tryout_id=tryout_id).all()
all_players = registered_players(tryout_id)
@@ -507,7 +508,7 @@ def create_match(tryout_id):
)
flash(_('Match scheduled successfully!'), 'success')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout_id))
return rerender()
@@ -521,11 +522,11 @@ def edit_match(match_id):
if not current_user.can_manage_this_tryout(tryout):
flash(_('You do not have permission to edit this match.'), 'danger')
return redirect(url_for('matches.calendar'))
return respond_after_change(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))
return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout.id))
teams = Team.query.filter_by(tryout_id=tryout.id).all()
all_players = registered_players(tryout.id)
@@ -636,7 +637,7 @@ def edit_match(match_id):
)
flash(_('Match updated successfully!'), 'success')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id))
return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout.id))
return rerender()
@@ -672,10 +673,10 @@ def delete_match(match_id):
tryout = match.tryout
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'))
return respond_after_change(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))
return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout.id))
# Notes outlive the match they were taken during: a coach's observation
# keeps its value, and deleting it here would destroy unrelated content.
@@ -688,7 +689,7 @@ def delete_match(match_id):
db.session.delete(match)
db.session.commit()
flash(_('Match deleted successfully.'), 'success')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id))
return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout.id))
def get_players_available_at_time(date_str, time_str):
+17 -12
View File
@@ -3,7 +3,7 @@
Uses polymorphic isinstance checks instead of role-string comparisons.
"""
from flask import Blueprint, flash, jsonify, redirect, render_template, request, url_for
from flask import Blueprint, flash, jsonify, render_template, request, url_for
from flask_babel import gettext as _
from flask_login import current_user, login_required
from marshmallow import ValidationError
@@ -13,6 +13,7 @@ from app.extensions import db
from app.forms import flash_validation_errors, form_payload
from app.models import (
Admin,
AppSettings,
Coach,
Manager,
OrgTeam,
@@ -20,10 +21,10 @@ from app.models import (
TeamMatch,
TeamMatchParticipant,
TeamPlayer,
AppSettings,
)
from app.pagination import paginate
from app.permissions import can_manage_org_team, coach_org_teams, visible_org_teams
from app.responders import respond_after_change
from app.routes.matches import default_end_time
from app.services.scheduling import notify_participants, zip_participants
from app.time_utils import utc_now_naive
@@ -121,12 +122,14 @@ def create_match(team_id):
"""Create a new regular-season team match."""
team = db.get_or_404(OrgTeam, team_id)
if season_locked():
flash('The regular season is not active. Begin a season before scheduling matches.', 'warning')
return redirect(url_for('team_matches.list_matches'))
flash(
'The regular season is not active. Begin a season before scheduling matches.', 'warning'
)
return respond_after_change(url_for('team_matches.list_matches'))
if not can_manage_team_match(team):
flash(_('You do not have permission to schedule matches for this team.'), 'danger')
return redirect(url_for('team_matches.list_matches'))
return respond_after_change(url_for('team_matches.list_matches'))
team_players = TeamPlayer.query.filter_by(org_team_id=team_id).all()
prefill_date = request.args.get('date', '')
@@ -219,7 +222,7 @@ def create_match(team_id):
flash(
_('Team match "%(title)s" scheduled successfully!', title=team_match.title), 'success'
)
return redirect(url_for('team_matches.list_matches'))
return respond_after_change(url_for('team_matches.list_matches'))
return render_template('pages/team_match_form.html', team=team, team_players=team_players)
@@ -233,7 +236,7 @@ def edit_match(match_id):
if not can_manage_team_match(team):
flash(_('You do not have permission to edit this match.'), 'danger')
return redirect(url_for('team_matches.list_matches'))
return respond_after_change(url_for('team_matches.list_matches'))
if request.method == 'POST':
# The three date and time fields used to be checked one at a time,
@@ -263,7 +266,7 @@ def edit_match(match_id):
db.session.commit()
flash(_('Match updated successfully!'), 'success')
return redirect(url_for('team_matches.list_matches'))
return respond_after_change(url_for('team_matches.list_matches'))
return render_template(
'pages/team_match_form.html', match=team_match, team=team, team_players=[]
@@ -277,17 +280,19 @@ def delete_match(match_id):
team_match = db.get_or_404(TeamMatch, match_id)
team = team_match.org_team
if season_locked():
flash('The regular season is not active. Begin a season before deleting matches.', 'warning')
return redirect(url_for('team_matches.list_matches'))
flash(
'The regular season is not active. Begin a season before deleting matches.', 'warning'
)
return respond_after_change(url_for('team_matches.list_matches'))
if not can_manage_team_match(team):
flash(_('You do not have permission to delete this match.'), 'danger')
return redirect(url_for('team_matches.list_matches'))
return respond_after_change(url_for('team_matches.list_matches'))
db.session.delete(team_match)
db.session.commit()
flash(_('Match deleted successfully.'), 'success')
return redirect(url_for('team_matches.list_matches'))
return respond_after_change(url_for('team_matches.list_matches'))
@team_matches_bp.route('/api/manageable-teams')
+47 -46
View File
@@ -27,6 +27,7 @@ from app.models import (
User,
)
from app.permissions import visible_org_teams
from app.responders import respond_after_change
from app.time_utils import utc_now_naive
from app.validators import NoteContentSchema, OrgTeamSchema, TeamPlayerSchema, TeamStaffSchema
@@ -76,7 +77,7 @@ def my_teams():
"""View the player's own teams with upcoming matches."""
if not isinstance(current_user, Player):
flash(_('This page is for players.'), 'info')
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
from app.models import TeamMatch, TeamMatchParticipant
@@ -183,18 +184,18 @@ def create_team():
"""Create a new organization team."""
if not current_user.can_manage_teams():
flash(_('You do not have permission to create teams.'), 'danger')
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
try:
data = OrgTeamSchema().load(form_payload(list_fields=('coach_ids', 'manager_ids')))
except ValidationError as err:
flash_validation_errors(err)
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
name = data['name']
if OrgTeam.query.filter_by(name=name).first():
flash(_('Team "%(name)s" already exists.', name=name), 'danger')
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
coach = _staff_member(data['coach_id'], Coach)
manager = _staff_member(data['manager_id'], Manager)
@@ -215,7 +216,7 @@ def create_team():
db.session.commit()
flash(_('Team "%(name)s" created successfully!', name=name), 'success')
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
@teams_bp.route('/<int:team_id>/edit', methods=['POST'])
@@ -225,18 +226,18 @@ def edit_team(team_id):
team = db.get_or_404(OrgTeam, team_id)
if not current_user.can_manage_this_org_team(team):
flash(_('You do not have permission to edit this team.'), 'danger')
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
try:
data = OrgTeamSchema().load(form_payload(list_fields=('coach_ids', 'manager_ids')))
except ValidationError as err:
flash_validation_errors(err)
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
name = data['name']
if OrgTeam.query.filter(OrgTeam.name == name, OrgTeam.id != team_id).first():
flash(_('Team "%(name)s" already exists.', name=name), 'danger')
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
team.name = name
@@ -269,7 +270,7 @@ def edit_team(team_id):
db.session.commit()
flash(_('Team "%(name)s" updated successfully!', name=name), 'success')
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
@teams_bp.route('/<int:team_id>/delete', methods=['POST'])
@@ -297,7 +298,7 @@ def delete_team(team_id):
if not (current_user.can_manage_teams() and current_user.can_manage_this_org_team(team)):
flash(_('You do not have permission to delete teams.'), 'danger')
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
name = team.name
@@ -328,7 +329,7 @@ def delete_team(team_id):
db.session.delete(team)
db.session.commit()
flash(_('Team "%(name)s" deleted successfully.', name=name), 'success')
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
@teams_bp.route('/<int:team_id>/add_coach', methods=['POST'])
@@ -338,19 +339,19 @@ def add_coach(team_id):
team = db.get_or_404(OrgTeam, team_id)
if not current_user.can_manage_this_org_team(team):
flash(_('Permission denied.'), 'danger')
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
data = _posted(TeamStaffSchema())
if data is None:
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
if not data['coach_id']:
flash(_('Please select a coach.'), 'danger')
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
coach = db.session.get(User, data['coach_id'])
if not coach or not _assignable(coach, Coach):
flash(_('Only coaches can be assigned as coach.'), 'danger')
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
if team.coaches.filter_by(id=coach.id).first():
flash(
@@ -361,7 +362,7 @@ def add_coach(team_id):
),
'info',
)
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
team.coaches.append(coach)
if not team.coach_id:
@@ -371,7 +372,7 @@ def add_coach(team_id):
_('%(username)s added as coach of %(name)s.', username=coach.username, name=team.name),
'success',
)
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
@teams_bp.route('/<int:team_id>/add_manager', methods=['POST'])
@@ -381,19 +382,19 @@ def add_manager(team_id):
team = db.get_or_404(OrgTeam, team_id)
if not current_user.can_manage_this_org_team(team):
flash(_('Permission denied.'), 'danger')
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
data = _posted(TeamStaffSchema())
if data is None:
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
if not data['manager_id']:
flash(_('Please select a manager.'), 'danger')
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
manager = db.session.get(User, data['manager_id'])
if not manager or not _assignable(manager, Manager):
flash(_('Only managers can be assigned as manager.'), 'danger')
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
if team.managers.filter_by(id=manager.id).first():
flash(
@@ -404,7 +405,7 @@ def add_manager(team_id):
),
'info',
)
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
team.managers.append(manager)
if not team.manager_id:
@@ -414,7 +415,7 @@ def add_manager(team_id):
_('%(username)s added as manager of %(name)s.', username=manager.username, name=team.name),
'success',
)
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
@teams_bp.route('/<int:team_id>/remove_coach', methods=['POST'])
@@ -424,11 +425,11 @@ def remove_coach(team_id):
team = db.get_or_404(OrgTeam, team_id)
if not current_user.can_manage_this_org_team(team):
flash(_('Permission denied.'), 'danger')
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
data = _posted(TeamStaffSchema())
if data is None:
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
if data['coach_id']:
coach = db.session.get(User, data['coach_id'])
@@ -442,7 +443,7 @@ def remove_coach(team_id):
db.session.commit()
flash(_('Coach removed from %(name)s.', name=team.name), 'success')
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
@teams_bp.route('/<int:team_id>/remove_manager', methods=['POST'])
@@ -452,11 +453,11 @@ def remove_manager(team_id):
team = db.get_or_404(OrgTeam, team_id)
if not current_user.can_manage_this_org_team(team):
flash(_('Permission denied.'), 'danger')
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
data = _posted(TeamStaffSchema())
if data is None:
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
if data['manager_id']:
manager = db.session.get(User, data['manager_id'])
@@ -470,7 +471,7 @@ def remove_manager(team_id):
db.session.commit()
flash(_('Manager removed from %(name)s.', name=team.name), 'success')
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
@teams_bp.route('/<int:team_id>/add_player', methods=['POST'])
@@ -480,20 +481,20 @@ def add_player(team_id):
team = db.get_or_404(OrgTeam, team_id)
if not current_user.can_manage_this_org_team(team):
flash(_('Permission denied.'), 'danger')
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
data = _posted(TeamPlayerSchema())
if data is None:
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
if not data['player_id']:
flash(_('Please select a player.'), 'danger')
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
status = data['status']
player = db.session.get(User, data['player_id'])
if not player or not _assignable(player, Player):
flash(_('Can only assign players to teams.'), 'danger')
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
existing = TeamPlayer.query.filter_by(player_id=player.id, org_team_id=team.id).first()
if existing:
@@ -501,13 +502,13 @@ def add_player(team_id):
_('%(username)s is already on %(name)s.', username=player.username, name=team.name),
'info',
)
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
tp = TeamPlayer(player_id=player.id, org_team_id=team.id, status=status)
db.session.add(tp)
db.session.commit()
flash(_('%(username)s added to %(name)s!', username=player.username, name=team.name), 'success')
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
@teams_bp.route('/<int:team_id>/remove_player/<int:player_id>', methods=['POST'])
@@ -517,7 +518,7 @@ def remove_player(team_id, player_id):
team = db.get_or_404(OrgTeam, team_id)
if not current_user.can_manage_this_org_team(team):
flash(_('Permission denied.'), 'danger')
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
player = db.get_or_404(User, player_id)
tp = TeamPlayer.query.filter_by(player_id=player_id, org_team_id=team_id).first()
@@ -526,7 +527,7 @@ def remove_player(team_id, player_id):
_('%(username)s is not on %(name)s.', username=player.username, name=team.name),
'danger',
)
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
db.session.delete(tp)
db.session.commit()
@@ -534,7 +535,7 @@ def remove_player(team_id, player_id):
_('%(username)s removed from %(name)s.', username=player.username, name=team.name),
'success',
)
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
@teams_bp.route('/<int:team_id>/toggle_status/<int:player_id>', methods=['POST'])
@@ -569,19 +570,19 @@ def add_team_note(team_id):
team = db.get_or_404(OrgTeam, team_id)
if not current_user.can_manage_this_org_team(team):
flash(_('You do not have permission to add notes to this team.'), 'danger')
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
try:
data = NoteContentSchema().load(form_payload(list_fields=()))
except ValidationError as err:
flash_validation_errors(err)
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
note = TeamNote(org_team_id=team_id, coach_id=current_user.id, content=data['content'])
db.session.add(note)
db.session.commit()
flash(_('Team notes added successfully!'), 'success')
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
@teams_bp.route('/<int:team_id>/add-player-note/<int:player_id>', methods=['POST'])
@@ -591,12 +592,12 @@ def add_player_note(team_id, player_id):
team = db.get_or_404(OrgTeam, team_id)
if not current_user.can_manage_this_org_team(team):
flash(_('You do not have permission to add notes to this team.'), 'danger')
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
player = db.get_or_404(User, player_id)
if not isinstance(player, Player):
flash(_('Can only add notes for players.'), 'danger')
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
tp = TeamPlayer.query.filter_by(player_id=player_id, org_team_id=team_id).first()
if not tp:
@@ -604,16 +605,16 @@ def add_player_note(team_id, player_id):
_('%(username)s is not on %(name)s.', username=player.username, name=team.name),
'danger',
)
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
try:
data = NoteContentSchema().load(form_payload(list_fields=()))
except ValidationError as err:
flash_validation_errors(err)
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
note = PersonalNote(player_id=player_id, coach_id=current_user.id, content=data['content'])
db.session.add(note)
db.session.commit()
flash(_('Note added for %(username)s!', username=player.username), 'success')
return redirect(url_for('teams.list_teams'))
return respond_after_change(url_for('teams.list_teams'))
+46 -45
View File
@@ -32,6 +32,7 @@ from app.models import (
TryoutRegistration,
User,
)
from app.responders import respond_after_change
from app.time_utils import utc_now_naive
from app.validators import (
PlayerSelectionSchema,
@@ -477,31 +478,31 @@ def register_for_tryout(tryout_id):
"""Register a player for a tryout. Only Players can self-register."""
if not isinstance(current_user, Player):
flash(_('Only players can register for tryouts.'), 'danger')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout_id))
tryout = locked_tryout_or_404(tryout_id)
if tryout.status not in ['upcoming', 'in_progress']:
flash(_('This tryout is not accepting registrations.'), 'danger')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout_id))
existing = TryoutRegistration.query.filter_by(
tryout_id=tryout_id, player_id=current_user.id
).first()
if existing:
flash(_('You are already registered for this tryout.'), 'info')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout_id))
if tryout.max_players:
count = TryoutRegistration.query.filter_by(tryout_id=tryout_id).count()
if count >= tryout.max_players:
flash(_('This tryout is full.'), 'danger')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout_id))
registration = TryoutRegistration(tryout_id=tryout_id, player_id=current_user.id)
db.session.add(registration)
db.session.commit()
flash(_('Successfully registered for tryout!'), 'success')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout_id))
@tryouts_bp.route('/<int:tryout_id>/status', methods=['POST'])
@@ -514,7 +515,7 @@ def update_status(tryout_id):
'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 respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout_id))
if not current_user.can_manage_this_tryout(tryout):
flash(_('Permission denied.'), 'danger')
@@ -523,12 +524,12 @@ def update_status(tryout_id):
data = TryoutStatusSchema().load(form_payload(list_fields=()))
except ValidationError as err:
flash_validation_errors(err)
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout_id))
tryout.status = data['status']
db.session.commit()
flash(_('Tryout status updated to %(new_status)s.', new_status=data['status']), 'success')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout_id))
@tryouts_bp.route('/<int:tryout_id>/registration/<int:player_id>/status', methods=['POST'])
@@ -541,7 +542,7 @@ def update_registration_status(tryout_id, player_id):
'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 respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout_id))
if not current_user.can_manage_this_tryout(tryout):
flash(_('Permission denied.'), 'danger')
@@ -554,12 +555,12 @@ def update_registration_status(tryout_id, player_id):
data = TryoutRegistrationStatusSchema().load(form_payload(list_fields=()))
except ValidationError as err:
flash_validation_errors(err)
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout_id))
registration.status = data['status']
db.session.commit()
flash(_('Registration status updated.'), 'success')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout_id))
@tryouts_bp.route('/<int:tryout_id>/register_player', methods=['POST'])
@@ -572,20 +573,20 @@ def register_player(tryout_id):
'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 respond_after_change(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))
return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout_id))
try:
data = PlayerSelectionSchema().load(form_payload())
except ValidationError as err:
flash_validation_errors(err)
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout_id))
if not data['player_id']:
flash(_('Please select a player.'), 'danger')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout_id))
# Same two checks as the team roster (SEC-16): the right role, and an
# account that has not been deactivated. The select this comes from now
@@ -593,7 +594,7 @@ def register_player(tryout_id):
player = db.session.get(User, data['player_id'])
if not player or not isinstance(player, Player) or not player.is_active_account:
flash(_('Can only register players.'), 'danger')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout_id))
existing = TryoutRegistration.query.filter_by(tryout_id=tryout_id, player_id=player.id).first()
if existing:
@@ -601,19 +602,19 @@ def register_player(tryout_id):
_('%(username)s is already registered for this tryout.', username=player.username),
'info',
)
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout_id))
if tryout.max_players:
count = TryoutRegistration.query.filter_by(tryout_id=tryout_id).count()
if count >= tryout.max_players:
flash(_('This tryout is full.'), 'danger')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout_id))
registration = TryoutRegistration(tryout_id=tryout_id, player_id=player.id)
db.session.add(registration)
db.session.commit()
flash(_('%(username)s registered for tryout!', username=player.username), 'success')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout_id))
@tryouts_bp.route('/<int:tryout_id>/remove_player/<int:player_id>', methods=['POST'])
@@ -626,7 +627,7 @@ def remove_player(tryout_id, player_id):
'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 respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout_id))
if not current_user.can_manage_this_tryout(tryout):
flash(_('Permission denied.'), 'danger')
@@ -656,7 +657,7 @@ def remove_player(tryout_id, player_id):
db.session.commit()
flash(_('%(username)s removed from tryout.', username=player.username), 'success')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout_id))
@tryouts_bp.route('/<int:tryout_id>/team/create', methods=['POST'])
@@ -669,23 +670,23 @@ def create_team(tryout_id):
'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 respond_after_change(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))
return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout_id))
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))
return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout_id))
team = Team(tryout_id=tryout_id, name=data['team_name'], created_by=current_user.id)
db.session.add(team)
db.session.commit()
flash(_('Team "%(team_name)s" created!', team_name=data['team_name']), 'success')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout_id))
@tryouts_bp.route('/<int:tryout_id>/team/<int:team_id>/add', methods=['POST'])
@@ -699,11 +700,11 @@ def add_to_team(tryout_id, team_id):
'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 respond_after_change(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))
return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout_id))
# The two ids arrive independently in the URL. Without this check, being
# allowed to manage tryout A was enough to modify a team belonging to
@@ -715,7 +716,7 @@ def add_to_team(tryout_id, team_id):
data = TryoutTeamMemberSchema().load(form_payload(list_fields=()))
except ValidationError as err:
flash_validation_errors(err)
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout_id))
player_id = data['player_id']
# Only players registered for this tryout may be placed on its teams.
@@ -725,7 +726,7 @@ def add_to_team(tryout_id, team_id):
)
if not is_registered:
flash(_('That player is not registered for this tryout.'), 'danger')
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout_id))
existing = TeamMember.query.filter_by(team_id=team_id, player_id=player_id).first()
if existing:
@@ -735,7 +736,7 @@ 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))
return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout_id))
@tryouts_bp.route('/<int:tryout_id>/team/<int:team_id>/edit', methods=['POST'])
@@ -749,11 +750,11 @@ def edit_team(tryout_id, team_id):
'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 respond_after_change(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))
return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout_id))
if team.tryout_id != tryout_id:
abort(404)
@@ -762,12 +763,12 @@ def edit_team(tryout_id, team_id):
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))
return respond_after_change(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))
return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout_id))
@tryouts_bp.route('/<int:tryout_id>/team/<int:team_id>/delete', methods=['POST'])
@@ -781,11 +782,11 @@ def delete_team(tryout_id, team_id):
'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 respond_after_change(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))
return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout_id))
if team.tryout_id != tryout_id:
abort(404)
@@ -811,7 +812,7 @@ def delete_team(tryout_id, team_id):
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))
return respond_after_change(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'])
@@ -825,11 +826,11 @@ def remove_from_team(tryout_id, team_id, player_id):
'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 respond_after_change(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))
return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout_id))
if team.tryout_id != tryout_id:
abort(404)
@@ -846,7 +847,7 @@ def remove_from_team(tryout_id, team_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))
return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout_id))
@tryouts_bp.route(
@@ -863,11 +864,11 @@ def update_member_position(tryout_id, team_id, member_id):
'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 respond_after_change(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))
return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout_id))
if team.tryout_id != tryout_id or member.team_id != team_id:
abort(404)
@@ -876,12 +877,12 @@ def update_member_position(tryout_id, team_id, member_id):
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))
return respond_after_change(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))
return respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout_id))
@tryouts_bp.route('/<int:tryout_id>/delete', methods=['POST'])
@@ -894,7 +895,7 @@ def delete_tryout(tryout_id):
'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 respond_after_change(url_for('tryouts.view_tryout', tryout_id=tryout_id))
if not current_user.can_manage_this_tryout(tryout):
flash(_('You do not have permission to delete this tryout.'), 'danger')
+17 -16
View File
@@ -10,6 +10,7 @@ from marshmallow import ValidationError
from werkzeug.utils import secure_filename
from app.extensions import db
from app.responders import respond_after_change
from app.models import Admin, Coach, Contract, Manager, Player, User
from app.models.admin_settings import AppSettings
from app.permissions import can_manage_player_contract, coach_player_ids
@@ -102,11 +103,11 @@ def upload_contract():
"""Upload a contract for a player."""
if not isinstance(current_user, (Admin, Manager, Coach)):
flash(_('Only presidents, managers, and coaches can upload contracts.'), 'danger')
return redirect(url_for('users.list_contracts'))
return respond_after_change(url_for('users.list_contracts'))
if document_uploads_locked():
flash(_('Document uploads are currently disabled.'), 'danger')
return redirect(url_for('users.list_contracts'))
return respond_after_change(url_for('users.list_contracts'))
players = manageable_players()
@@ -125,13 +126,13 @@ def upload_contract():
if not can_manage_player_contract(current_user, player_id):
flash(_('You do not have permission to upload a contract for this player.'), 'danger')
return redirect(url_for('users.upload_contract'))
return respond_after_change(url_for('users.upload_contract'))
file = request.files.get('contract_file')
error = pdf_upload_error(file, ALLOWED_CONTRACT_EXTENSIONS)
if error:
flash(error, 'danger')
return redirect(url_for('users.upload_contract'))
return respond_after_change(url_for('users.upload_contract'))
player = db.get_or_404(User, player_id)
player_teams = player.get_org_teams()
@@ -154,7 +155,7 @@ def upload_contract():
flash(
_('Contract storage is temporarily unavailable. Please try again later.'), 'danger'
)
return redirect(url_for('users.upload_contract'))
return respond_after_change(url_for('users.upload_contract'))
contract = Contract(
player_id=player_id,
@@ -171,7 +172,7 @@ def upload_contract():
_('Contract uploaded successfully for %(username)s!', username=player.username),
'success',
)
return redirect(url_for('users.list_contracts'))
return respond_after_change(url_for('users.list_contracts'))
return render_template('pages/upload_contract.html', players=players)
@@ -183,17 +184,17 @@ def upload_signed_contract(contract_id):
contract = db.get_or_404(Contract, contract_id)
if not contract.can_upload_signed(current_user):
flash(_('Only the player can upload their signed contract.'), 'danger')
return redirect(url_for('users.list_contracts'))
return respond_after_change(url_for('users.list_contracts'))
if document_uploads_locked():
flash(_('Document uploads are currently disabled.'), 'danger')
return redirect(url_for('users.list_contracts'))
return respond_after_change(url_for('users.list_contracts'))
file = request.files.get('signed_file')
error = pdf_upload_error(file, ALLOWED_SIGNED_EXTENSIONS)
if error:
flash(error, 'danger')
return redirect(url_for('users.list_contracts'))
return respond_after_change(url_for('users.list_contracts'))
signed_filename = f"signed_{contract.stored_filename}"
# Legacy local records keep their signed copy beside the original. Drive
@@ -208,7 +209,7 @@ def upload_signed_contract(contract_id):
stored_signed_path = store_uploaded_document(file, signed_path)
except GoogleDriveStorageError:
flash(_('Contract storage is temporarily unavailable. Please try again later.'), 'danger')
return redirect(url_for('users.list_contracts'))
return respond_after_change(url_for('users.list_contracts'))
contract.signed_filename = signed_filename
contract.signed_file_path = stored_signed_path
@@ -216,7 +217,7 @@ def upload_signed_contract(contract_id):
contract.signed_at = utc_now_naive()
db.session.commit()
flash(_('Signed contract uploaded successfully!'), 'success')
return redirect(url_for('users.list_contracts'))
return respond_after_change(url_for('users.list_contracts'))
@users_bp.route('/contracts/<int:contract_id>/download')
@@ -226,12 +227,12 @@ def download_contract(contract_id):
contract = db.get_or_404(Contract, contract_id)
if not contract.can_view(current_user):
flash(_('You do not have permission to download this contract.'), 'danger')
return redirect(url_for('users.list_contracts'))
return respond_after_change(url_for('users.list_contracts'))
try:
document = open_document(contract.file_path)
except GoogleDriveStorageError:
flash(_('Contract storage is temporarily unavailable. Please try again later.'), 'danger')
return redirect(url_for('users.list_contracts'))
return respond_after_change(url_for('users.list_contracts'))
return send_file(document, as_attachment=True, download_name=contract.original_filename)
@@ -242,13 +243,13 @@ def download_signed_contract(contract_id):
contract = db.get_or_404(Contract, contract_id)
if not contract.can_view(current_user):
flash(_('You do not have permission to download this contract.'), 'danger')
return redirect(url_for('users.list_contracts'))
return respond_after_change(url_for('users.list_contracts'))
if not contract.signed_file_path:
flash(_('No signed contract available.'), 'danger')
return redirect(url_for('users.list_contracts'))
return respond_after_change(url_for('users.list_contracts'))
try:
document = open_document(contract.signed_file_path)
except GoogleDriveStorageError:
flash(_('Contract storage is temporarily unavailable. Please try again later.'), 'danger')
return redirect(url_for('users.list_contracts'))
return respond_after_change(url_for('users.list_contracts'))
return send_file(document, as_attachment=True, download_name=contract.signed_filename)
+20 -19
View File
@@ -10,6 +10,7 @@ from flask_login import current_user, login_required
from marshmallow import ValidationError
from app.extensions import db
from app.responders import respond_after_change
from app.forms import flash_validation_errors, form_payload
from app.models import (
Coach,
@@ -176,14 +177,14 @@ def manage_team_notes():
org_teams = coach_org_teams(current_user)
if not org_teams:
flash(_('You are not assigned to a team.'), 'danger')
return redirect(url_for('users.notes_dashboard'))
return respond_after_change(url_for('users.notes_dashboard'))
org_team = org_teams[0]
try:
data = NoteContentSchema().load(form_payload(list_fields=()))
except ValidationError as err:
flash_validation_errors(err)
return redirect(url_for('users.notes_dashboard'))
return respond_after_change(url_for('users.notes_dashboard'))
note = TeamNote(
org_team_id=org_team.id,
@@ -194,7 +195,7 @@ def manage_team_notes():
db.session.commit()
flash(_('Team notes saved successfully!'), 'success')
return redirect(url_for('users.notes_dashboard'))
return respond_after_change(url_for('users.notes_dashboard'))
# ---------------------------------------------------------------------------
@@ -214,17 +215,17 @@ def manage_personal_notes():
data = PersonalNoteSchema().load(form_payload(list_fields=()))
except ValidationError as err:
flash_validation_errors(err)
return redirect(url_for('users.notes_dashboard'))
return respond_after_change(url_for('users.notes_dashboard'))
player_id = data['player_id']
player = db.get_or_404(User, player_id)
if not isinstance(player, Player):
flash(_('Can only add notes for players.'), 'danger')
return redirect(url_for('users.notes_dashboard'))
return respond_after_change(url_for('users.notes_dashboard'))
if not coach_can_access_player(current_user, player_id):
flash(_('You can only write notes about players you work with.'), 'danger')
return redirect(url_for('users.notes_dashboard'))
return respond_after_change(url_for('users.notes_dashboard'))
note = PersonalNote(
player_id=player_id,
@@ -234,7 +235,7 @@ def manage_personal_notes():
db.session.add(note)
db.session.commit()
flash(_('Note added for %(username)s.', username=player.username), 'success')
return redirect(url_for('users.notes_dashboard'))
return respond_after_change(url_for('users.notes_dashboard'))
# ---------------------------------------------------------------------------
@@ -254,44 +255,44 @@ def add_personal_note():
data = PersonalNoteSchema().load(form_payload(list_fields=()))
except ValidationError as err:
flash_validation_errors(err)
return redirect(url_for('users.notes_dashboard'))
return respond_after_change(url_for('users.notes_dashboard'))
player_id = data['player_id']
player = db.get_or_404(User, player_id)
if not isinstance(player, Player):
flash(_('Can only add notes for players.'), 'danger')
return redirect(url_for('users.notes_dashboard'))
return respond_after_change(url_for('users.notes_dashboard'))
if not coach_can_access_player(current_user, player_id):
flash(_('You can only write notes about players you work with.'), 'danger')
return redirect(url_for('users.notes_dashboard'))
return respond_after_change(url_for('users.notes_dashboard'))
if data['match_id']:
match = db.get_or_404(Match, data['match_id'])
if not current_user.can_manage_this_tryout(match.tryout):
flash(_('You cannot use that match as note context.'), 'danger')
return redirect(url_for('users.notes_dashboard'))
return respond_after_change(url_for('users.notes_dashboard'))
if not MatchParticipant.query.filter_by(match_id=match.id, player_id=player_id).first():
flash(_('That player did not participate in the selected match.'), 'danger')
return redirect(url_for('users.notes_dashboard'))
return respond_after_change(url_for('users.notes_dashboard'))
if data['tryout_id']:
tryout = db.get_or_404(Tryout, data['tryout_id'])
if not current_user.can_manage_this_tryout(tryout):
flash(_('You cannot use that tryout as note context.'), 'danger')
return redirect(url_for('users.notes_dashboard'))
return respond_after_change(url_for('users.notes_dashboard'))
if not TryoutRegistration.query.filter_by(tryout_id=tryout.id, player_id=player_id).first():
flash(_('That player is not registered for the selected tryout.'), 'danger')
return redirect(url_for('users.notes_dashboard'))
return respond_after_change(url_for('users.notes_dashboard'))
if data['team_id']:
team = db.get_or_404(Team, data['team_id'])
if not current_user.can_manage_this_tryout(team.tryout):
flash(_('You cannot use that team as note context.'), 'danger')
return redirect(url_for('users.notes_dashboard'))
return respond_after_change(url_for('users.notes_dashboard'))
if not TeamMember.query.filter_by(team_id=team.id, player_id=player_id).first():
flash(_('That player is not on the selected team.'), 'danger')
return redirect(url_for('users.notes_dashboard'))
return respond_after_change(url_for('users.notes_dashboard'))
note = PersonalNote(
player_id=player_id,
@@ -304,7 +305,7 @@ def add_personal_note():
db.session.add(note)
db.session.commit()
flash(_('Note added for %(username)s.', username=player.username), 'success')
return redirect(url_for('users.notes_dashboard'))
return respond_after_change(url_for('users.notes_dashboard'))
# ---------------------------------------------------------------------------
@@ -323,7 +324,7 @@ def add_note_from_tryout(tryout_id):
tryout = db.get_or_404(Tryout, tryout_id)
if not current_user.can_manage_this_tryout(tryout):
flash(_('You do not have permission to add notes for this tryout.'), 'danger')
return redirect(url_for('users.notes_dashboard'))
return respond_after_change(url_for('users.notes_dashboard'))
preselected_player_id = request.args.get('player_id', type=int)
# Get registrations as players for the select list
@@ -387,7 +388,7 @@ def add_note_from_match(match_id):
match_obj = db.get_or_404(Match, match_id)
if not current_user.can_manage_this_tryout(match_obj.tryout):
flash(_('You do not have permission to add notes for this match.'), 'danger')
return redirect(url_for('users.notes_dashboard'))
return respond_after_change(url_for('users.notes_dashboard'))
# Get participants as players for the select list
participants = MatchParticipant.query.filter_by(match_id=match_id).all()
+12 -11
View File
@@ -6,6 +6,7 @@ from flask_login import current_user, login_required
from marshmallow import ValidationError
from app.extensions import db
from app.responders import respond_after_change
from app.forms import flash_validation_errors, form_payload
from app.models import Coach, CoachAvailability, OneOnOneRequest, PersonalNote, Player, TeamNote
from app.routes.users.blueprint import users_bp
@@ -64,13 +65,13 @@ def one_on_one():
if request.method == 'POST':
if not coach:
flash(_('Cannot request One on One - no coach assigned.'), 'danger')
return redirect(url_for('users.one_on_one'))
return respond_after_change(url_for('users.one_on_one'))
try:
data = OneOnOneRequestSchema().load(form_payload())
except ValidationError as err:
flash_validation_errors(err)
return redirect(url_for('users.one_on_one'))
return respond_after_change(url_for('users.one_on_one'))
date_obj = data['date']
start_time = data['start_time']
@@ -90,7 +91,7 @@ def one_on_one():
if not is_available:
flash(_("The requested time is not within the coach's availability."), 'danger')
return redirect(url_for('users.one_on_one'))
return respond_after_change(url_for('users.one_on_one'))
request_obj = OneOnOneRequest(
player_id=current_user.id,
@@ -118,7 +119,7 @@ def one_on_one():
)
flash(_('Your One on One request has been submitted!'), 'success')
return redirect(url_for('users.one_on_one'))
return respond_after_change(url_for('users.one_on_one'))
# Build list of upcoming dates that have coach availability
from datetime import date as date_cls
@@ -169,11 +170,11 @@ def accept_one_on_one(request_id):
if request_obj.coach_id != current_user.id:
flash(_('This request is not for you.'), 'danger')
return redirect(url_for('users.notes_dashboard'))
return respond_after_change(url_for('users.notes_dashboard'))
if request_obj.status != 'pending':
flash(_('This request has already been processed.'), 'info')
return redirect(url_for('users.notes_dashboard'))
return respond_after_change(url_for('users.notes_dashboard'))
player = request_obj.player
request_obj.status = 'approved'
@@ -204,7 +205,7 @@ def accept_one_on_one(request_id):
),
'success',
)
return redirect(url_for('users.notes_dashboard'))
return respond_after_change(url_for('users.notes_dashboard'))
@users_bp.route('/one-on-one/<int:request_id>/reject', methods=['POST'])
@@ -219,17 +220,17 @@ def reject_one_on_one(request_id):
if request_obj.coach_id != current_user.id:
flash(_('This request is not for you.'), 'danger')
return redirect(url_for('users.notes_dashboard'))
return respond_after_change(url_for('users.notes_dashboard'))
if request_obj.status != 'pending':
flash(_('This request has already been processed.'), 'info')
return redirect(url_for('users.notes_dashboard'))
return respond_after_change(url_for('users.notes_dashboard'))
try:
data = OneOnOneRejectionSchema().load(form_payload(list_fields=()))
except ValidationError as err:
flash_validation_errors(err)
return redirect(url_for('users.notes_dashboard'))
return respond_after_change(url_for('users.notes_dashboard'))
rejection_reason = data['rejection_reason']
player = request_obj.player
@@ -264,4 +265,4 @@ def reject_one_on_one(request_id):
),
'info',
)
return redirect(url_for('users.notes_dashboard'))
return respond_after_change(url_for('users.notes_dashboard'))
+2 -1
View File
@@ -6,6 +6,7 @@ from flask_login import current_user, login_required
from marshmallow import ValidationError
from app.extensions import db, hash_password
from app.responders import respond_after_change
from app.logging_config import log_auth_event
from app.models import (
ESPORT_GAMES,
@@ -129,7 +130,7 @@ def edit_profile():
db.session.commit()
flash(_('Profile updated successfully!'), 'success')
return redirect(url_for('users.profile'))
return respond_after_change(url_for('users.profile'))
return render_template(
'pages/edit_profile.html',