Changement du refresh handler, soft reset d'ajouter sur beaucoup de formulaire de modification
CI - Security, Lint & Tests / validate (push) Failing after 1m3s
CI - Security, Lint & Tests / validate (push) Failing after 1m3s
This commit is contained in:
+52
-2
@@ -26,8 +26,10 @@ from sqlalchemy import text
|
|||||||
from werkzeug.exceptions import HTTPException
|
from werkzeug.exceptions import HTTPException
|
||||||
|
|
||||||
from app import i18n
|
from app import i18n
|
||||||
|
from app.api import json_endpoint
|
||||||
from app.extensions import babel, csrf, db, limiter, login_manager
|
from app.extensions import babel, csrf, db, limiter, login_manager
|
||||||
from app.pagination import page_url
|
from app.pagination import page_url
|
||||||
|
from app.responders import current_revision, is_ajax_submit
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
@@ -347,6 +349,42 @@ def create_app(config=None):
|
|||||||
response.headers['X-Request-Id'] = g.get('request_id', '-')
|
response.headers['X-Request-Id'] = g.get('request_id', '-')
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@app.before_request
|
||||||
|
def reject_stale_ajax_write():
|
||||||
|
"""Reject an AJAX edit whose view is stale, instead of overwriting.
|
||||||
|
|
||||||
|
The AJAX form handler in main.js sends the revision it last saw in the
|
||||||
|
X-Revision header. If another client has committed since then the
|
||||||
|
server's revision has moved on, and clobbering their change would be
|
||||||
|
silent last-write-wins. Answer 409 so the client asks the user to
|
||||||
|
reload the page first.
|
||||||
|
"""
|
||||||
|
if request.method != 'POST':
|
||||||
|
return None
|
||||||
|
if not is_ajax_submit():
|
||||||
|
return None
|
||||||
|
sent = request.headers.get('X-Revision')
|
||||||
|
if sent is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
sent = int(sent)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
if sent != current_revision():
|
||||||
|
return (
|
||||||
|
jsonify(
|
||||||
|
{
|
||||||
|
'error': 'conflict',
|
||||||
|
'message': _(
|
||||||
|
'This page was changed by someone else. Reload to '
|
||||||
|
'see the latest changes before saving.'
|
||||||
|
),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
409,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
@app.url_defaults
|
@app.url_defaults
|
||||||
def version_static_urls(endpoint, values):
|
def version_static_urls(endpoint, values):
|
||||||
"""Stamp every static URL with the file's modification time.
|
"""Stamp every static URL with the file's modification time.
|
||||||
@@ -403,6 +441,7 @@ def create_app(config=None):
|
|||||||
|
|
||||||
configure_logging(app)
|
configure_logging(app)
|
||||||
|
|
||||||
|
from app.routes.admin import admin_bp
|
||||||
from app.routes.auth import auth_bp
|
from app.routes.auth import auth_bp
|
||||||
from app.routes.evaluations import evaluations_bp
|
from app.routes.evaluations import evaluations_bp
|
||||||
from app.routes.main import main_bp
|
from app.routes.main import main_bp
|
||||||
@@ -411,7 +450,6 @@ def create_app(config=None):
|
|||||||
from app.routes.teams import teams_bp
|
from app.routes.teams import teams_bp
|
||||||
from app.routes.tryouts import tryouts_bp
|
from app.routes.tryouts import tryouts_bp
|
||||||
from app.routes.users import users_bp
|
from app.routes.users import users_bp
|
||||||
from app.routes.admin import admin_bp
|
|
||||||
|
|
||||||
app.register_blueprint(auth_bp)
|
app.register_blueprint(auth_bp)
|
||||||
app.register_blueprint(tryouts_bp)
|
app.register_blueprint(tryouts_bp)
|
||||||
@@ -531,6 +569,18 @@ def create_app(config=None):
|
|||||||
|
|
||||||
return jsonify(health_data), 200
|
return jsonify(health_data), 200
|
||||||
|
|
||||||
|
@app.route('/api/revision')
|
||||||
|
@json_endpoint
|
||||||
|
def data_revision():
|
||||||
|
"""The current dataset revision, for the AJAX form handler in main.js.
|
||||||
|
|
||||||
|
One primary-key lookup in app_settings. The client compares this with
|
||||||
|
the revision it last saw to decide whether a focus refresh is needed,
|
||||||
|
and sends it back with every AJAX mutation so a stale edit is rejected
|
||||||
|
with a 409 instead of overwriting someone else's change.
|
||||||
|
"""
|
||||||
|
return jsonify({'revision': current_revision()})
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# Custom Error Handlers
|
# Custom Error Handlers
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
@@ -725,4 +775,4 @@ def create_app(config=None):
|
|||||||
# own host, port and debug default — `python app/app.py` bound 0.0.0.0:10000
|
# own host, port and debug default — `python app/app.py` bound 0.0.0.0:10000
|
||||||
# while `python run.py` bound 127.0.0.2:5000 with the debugger on. This
|
# while `python run.py` bound 127.0.0.2:5000 with the debugger on. This
|
||||||
# module defines the factory; run.py starts it for development, wsgi.py for
|
# module defines the factory; run.py starts it for development, wsgi.py for
|
||||||
# production (ARCH-007).
|
# production (ARCH-007).
|
||||||
|
|||||||
@@ -96,4 +96,4 @@ from app.models.personal_note import PersonalNote
|
|||||||
from app.models.one_on_one_request import OneOnOneRequest
|
from app.models.one_on_one_request import OneOnOneRequest
|
||||||
from app.models.admin_settings import AppSettings
|
from app.models.admin_settings import AppSettings
|
||||||
from app.models.backup_record import BackupRecord
|
from app.models.backup_record import BackupRecord
|
||||||
from app.models.audit_log import AuditLog
|
from app.models.audit_log import AuditLog
|
||||||
|
|||||||
@@ -53,4 +53,4 @@ class AppSettings(db.Model):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def set_bool(key, value):
|
def set_bool(key, value):
|
||||||
"""Store a boolean setting as 'true' / 'false'."""
|
"""Store a boolean setting as 'true' / 'false'."""
|
||||||
AppSettings.set(key, 'true' if value else 'false')
|
AppSettings.set(key, 'true' if value else 'false')
|
||||||
|
|||||||
@@ -20,14 +20,18 @@ class Tryout(db.Model):
|
|||||||
max_players = db.Column(db.Integer, nullable=True)
|
max_players = db.Column(db.Integer, nullable=True)
|
||||||
created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
||||||
target_org_team_id = db.Column(db.Integer, db.ForeignKey('org_teams.id'), nullable=True)
|
target_org_team_id = db.Column(db.Integer, db.ForeignKey('org_teams.id'), nullable=True)
|
||||||
manager_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True) # deprecated, kept for migration
|
manager_id = db.Column(
|
||||||
|
db.Integer, db.ForeignKey('users.id'), nullable=True
|
||||||
|
) # deprecated, kept for migration
|
||||||
coach_id = db.Column(
|
coach_id = db.Column(
|
||||||
db.Integer, db.ForeignKey('users.id'), nullable=True
|
db.Integer, db.ForeignKey('users.id'), nullable=True
|
||||||
) # deprecated, kept for migration
|
) # deprecated, kept for migration
|
||||||
created_at = db.Column(db.DateTime, default=utc_now_naive)
|
created_at = db.Column(db.DateTime, default=utc_now_naive)
|
||||||
|
|
||||||
creator = db.relationship('User', foreign_keys=[created_by], backref='created_tryouts')
|
creator = db.relationship('User', foreign_keys=[created_by], backref='created_tryouts')
|
||||||
manager = db.relationship('User', foreign_keys=[manager_id], backref='managed_tryouts') # deprecated
|
manager = db.relationship(
|
||||||
|
'User', foreign_keys=[manager_id], backref='managed_tryouts'
|
||||||
|
) # deprecated
|
||||||
coach = db.relationship('User', foreign_keys=[coach_id], backref='_deprecated_coached_tryouts')
|
coach = db.relationship('User', foreign_keys=[coach_id], backref='_deprecated_coached_tryouts')
|
||||||
coaches = db.relationship('User', secondary=tryout_coaches, backref='coached_tryouts')
|
coaches = db.relationship('User', secondary=tryout_coaches, backref='coached_tryouts')
|
||||||
managers = db.relationship('User', secondary=tryout_managers, backref='managed_tryouts_m2m')
|
managers = db.relationship('User', secondary=tryout_managers, backref='managed_tryouts_m2m')
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
"""Helpers for mutating routes that should not force a full page reload.
|
||||||
|
|
||||||
|
The site is server-rendered, and every mutating route used to end with
|
||||||
|
``return redirect(url_for(...))``, so each change reloaded the whole page:
|
||||||
|
scroll reset, open dialogs closed, and chaining several edits meant waiting
|
||||||
|
for a full render between each one.
|
||||||
|
|
||||||
|
These helpers let a route answer an AJAX submit (the ``data-ajax`` forms in
|
||||||
|
main.js) with a small JSON payload instead of a redirect, while the plain
|
||||||
|
HTML form path keeps behaving exactly as before. The client then fetches the
|
||||||
|
page again and swaps only the content region, so the page never reloads.
|
||||||
|
|
||||||
|
The counterpart that performs the swap lives in app/static/js/main.js.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import get_flashed_messages, jsonify, redirect, request, session
|
||||||
|
|
||||||
|
from app.models import AppSettings
|
||||||
|
|
||||||
|
#: Key holding a coarse, monotonically increasing stamp for the whole dataset.
|
||||||
|
#: It advances after every committed mutation so a stale client can be told
|
||||||
|
#: (with a 409) that someone else has changed what it is looking at.
|
||||||
|
REVISION_KEY = 'data_revision'
|
||||||
|
|
||||||
|
|
||||||
|
def is_ajax_submit():
|
||||||
|
"""Whether this POST came from the fetch-based form handler in main.js."""
|
||||||
|
return request.headers.get('X-Requested-With') == 'XMLHttpRequest'
|
||||||
|
|
||||||
|
|
||||||
|
def current_revision():
|
||||||
|
"""The current revision stamp, as an int (0 when nothing has been bumped)."""
|
||||||
|
try:
|
||||||
|
return int(AppSettings.get(REVISION_KEY, '0') or '0')
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def bump_revision():
|
||||||
|
"""Advance the revision stamp after a committed change.
|
||||||
|
|
||||||
|
Stored through AppSettings so it survives restarts and is shared by every
|
||||||
|
worker, unlike a module-level counter.
|
||||||
|
"""
|
||||||
|
AppSettings.set(REVISION_KEY, str(current_revision() + 1))
|
||||||
|
|
||||||
|
|
||||||
|
def _flashed_success():
|
||||||
|
"""Whether a success flash is pending, without consuming it.
|
||||||
|
|
||||||
|
The HTML path still redirects and must leave the flash for the next page
|
||||||
|
to render; peeking at the session is the only way to know without clearing
|
||||||
|
it. Only a successful commit advances the revision -- a rejected edit must
|
||||||
|
not make other users reload for nothing.
|
||||||
|
"""
|
||||||
|
flashes = session.get('_flashes', [])
|
||||||
|
return any(category == 'success' for category, _message in flashes)
|
||||||
|
|
||||||
|
|
||||||
|
def respond_after_change(redirect_url):
|
||||||
|
"""Answer a committed (or rejected) mutation.
|
||||||
|
|
||||||
|
For a normal HTML form submit this is exactly the ``redirect(...)`` the
|
||||||
|
routes used to return. For an AJAX submit it returns JSON telling the
|
||||||
|
client where to fetch the refreshed fragment and which flash messages to
|
||||||
|
show, so the page can update in place instead of reloading.
|
||||||
|
"""
|
||||||
|
if _flashed_success():
|
||||||
|
bump_revision()
|
||||||
|
|
||||||
|
if is_ajax_submit():
|
||||||
|
messages = [
|
||||||
|
{'category': category, 'text': message}
|
||||||
|
for category, message in get_flashed_messages(with_categories=True)
|
||||||
|
]
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
'ok': True,
|
||||||
|
'redirect': redirect_url,
|
||||||
|
'messages': messages,
|
||||||
|
'revision': current_revision(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return redirect(redirect_url)
|
||||||
+66
-37
@@ -9,19 +9,35 @@ import os
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from flask import (
|
from flask import (
|
||||||
Blueprint, render_template, redirect, url_for, flash, request,
|
Blueprint,
|
||||||
|
flash,
|
||||||
|
redirect,
|
||||||
|
render_template,
|
||||||
|
request,
|
||||||
send_file,
|
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.extensions import db
|
||||||
from app.models import (
|
from app.models import (
|
||||||
Admin, User, Tryout, OrgTeam, TeamPlayer, TeamMatch,
|
Admin,
|
||||||
TeamMatchParticipant, AppSettings, BackupRecord, AuditLog,
|
AppSettings,
|
||||||
|
AuditLog,
|
||||||
|
BackupRecord,
|
||||||
|
OrgTeam,
|
||||||
|
TeamMatch,
|
||||||
|
TeamMatchParticipant,
|
||||||
|
TeamPlayer,
|
||||||
|
Tryout,
|
||||||
|
User,
|
||||||
)
|
)
|
||||||
|
from app.responders import respond_after_change
|
||||||
from app.supporting_scripts.backup import (
|
from app.supporting_scripts.backup import (
|
||||||
BACKUP_DIR, BackupError, backup_database, backup_documents,
|
BackupError,
|
||||||
create_backup_dir, parse_database_url, verify_backup,
|
backup_database,
|
||||||
|
create_backup_dir,
|
||||||
|
parse_database_url,
|
||||||
)
|
)
|
||||||
|
|
||||||
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
|
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
|
||||||
@@ -56,6 +72,7 @@ def _log(action, details=None):
|
|||||||
# Dashboard
|
# Dashboard
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
@admin_bp.route('')
|
@admin_bp.route('')
|
||||||
@login_required
|
@login_required
|
||||||
def dashboard():
|
def dashboard():
|
||||||
@@ -96,6 +113,7 @@ def dashboard():
|
|||||||
# Backups
|
# Backups
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def _create_backup_record(backup_type='manual', notes=None):
|
def _create_backup_record(backup_type='manual', notes=None):
|
||||||
"""Run a database backup and return a BackupRecord, or raise BackupError."""
|
"""Run a database backup and return a BackupRecord, or raise BackupError."""
|
||||||
create_backup_dir()
|
create_backup_dir()
|
||||||
@@ -130,11 +148,11 @@ def create_backup():
|
|||||||
except BackupError as e:
|
except BackupError as e:
|
||||||
flash(f'Backup failed: {e}', 'danger')
|
flash(f'Backup failed: {e}', 'danger')
|
||||||
_log('backup_failed', f'Error: {e}')
|
_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)')
|
_log('backup_created', f'File: {record.filename} ({record.size_bytes} bytes)')
|
||||||
flash(f'Backup created successfully: {record.filename}', 'success')
|
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')
|
@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)
|
record = BackupRecord.query.get_or_404(backup_id)
|
||||||
if not os.path.exists(record.file_path):
|
if not os.path.exists(record.file_path):
|
||||||
flash('Backup file is missing from disk.', 'danger')
|
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}')
|
_log('backup_downloaded', f'File: {record.filename}')
|
||||||
return send_file(record.file_path, as_attachment=True, download_name=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)
|
os.remove(record.file_path)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
flash(f'Could not remove backup file: {e}', 'danger')
|
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}')
|
_log('backup_deleted', f'File: {record.filename}')
|
||||||
db.session.delete(record)
|
db.session.delete(record)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(f'Backup {record.filename} deleted.', 'success')
|
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'])
|
@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)
|
record = BackupRecord.query.get_or_404(backup_id)
|
||||||
if not os.path.exists(record.file_path):
|
if not os.path.exists(record.file_path):
|
||||||
flash('Backup file is missing from disk.', 'danger')
|
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.
|
# Create a safety backup of the current state before restoring.
|
||||||
try:
|
try:
|
||||||
@@ -199,22 +217,31 @@ def restore_backup(backup_id):
|
|||||||
)
|
)
|
||||||
except BackupError as e:
|
except BackupError as e:
|
||||||
flash(f'Could not create safety backup, restore aborted: {e}', 'danger')
|
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
|
# Restore using pg_restore
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
from app.supporting_scripts.backup import (
|
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'))
|
conn = parse_database_url(os.getenv('DATABASE_URL'))
|
||||||
cmd = [
|
cmd = [
|
||||||
PG_RESTORE,
|
PG_RESTORE,
|
||||||
'--host', conn['host'],
|
'--host',
|
||||||
'--port', conn['port'],
|
conn['host'],
|
||||||
'--username', conn['user'],
|
'--port',
|
||||||
'--dbname', conn['dbname'],
|
conn['port'],
|
||||||
'--clean', '--if-exists', '--no-owner',
|
'--username',
|
||||||
|
conn['user'],
|
||||||
|
'--dbname',
|
||||||
|
conn['dbname'],
|
||||||
|
'--clean',
|
||||||
|
'--if-exists',
|
||||||
|
'--no-owner',
|
||||||
record.file_path,
|
record.file_path,
|
||||||
]
|
]
|
||||||
try:
|
try:
|
||||||
@@ -230,20 +257,21 @@ def restore_backup(backup_id):
|
|||||||
except (subprocess.TimeoutExpired, FileNotFoundError, RuntimeError) as e:
|
except (subprocess.TimeoutExpired, FileNotFoundError, RuntimeError) as e:
|
||||||
flash(f'Restore failed: {e}', 'danger')
|
flash(f'Restore failed: {e}', 'danger')
|
||||||
_log('backup_restore_failed', f'File: {record.filename}, Error: {e}')
|
_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}')
|
_log('backup_restored', f'File: {record.filename}')
|
||||||
flash(
|
flash(
|
||||||
f'Backup {record.filename} restored successfully. The database has been rolled back.',
|
f'Backup {record.filename} restored successfully. The database has been rolled back.',
|
||||||
'success',
|
'success',
|
||||||
)
|
)
|
||||||
return redirect(url_for('admin.dashboard'))
|
return respond_after_change(url_for('admin.dashboard'))
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Tryouts open/close toggle
|
# Tryouts open/close toggle
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
@admin_bp.route('/toggle-tryouts', methods=['POST'])
|
@admin_bp.route('/toggle-tryouts', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def toggle_tryouts():
|
def toggle_tryouts():
|
||||||
@@ -258,13 +286,14 @@ def toggle_tryouts():
|
|||||||
state = 'opened' if new_value else 'closed'
|
state = 'opened' if new_value else 'closed'
|
||||||
_log('tryouts_toggled', f'Tryouts {state}')
|
_log('tryouts_toggled', f'Tryouts {state}')
|
||||||
flash(f'Tryouts are now {state}.', 'success')
|
flash(f'Tryouts are now {state}.', 'success')
|
||||||
return redirect(url_for('admin.dashboard'))
|
return respond_after_change(url_for('admin.dashboard'))
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Season lifecycle
|
# Season lifecycle
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
@admin_bp.route('/season/start', methods=['POST'])
|
@admin_bp.route('/season/start', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def start_season():
|
def start_season():
|
||||||
@@ -274,19 +303,19 @@ def start_season():
|
|||||||
|
|
||||||
if AppSettings.get_bool('season_active', default=False):
|
if AppSettings.get_bool('season_active', default=False):
|
||||||
flash('A season is already active. End it before starting a new one.', 'danger')
|
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()
|
name = request.form.get('season_name', '').strip()
|
||||||
start_date = request.form.get('season_start', '').strip()
|
start_date = request.form.get('season_start', '').strip()
|
||||||
if not start_date:
|
if not start_date:
|
||||||
flash('A season start date is required.', 'danger')
|
flash('A season start date is required.', 'danger')
|
||||||
return redirect(url_for('admin.dashboard'))
|
return respond_after_change(url_for('admin.dashboard'))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
datetime.strptime(start_date, '%Y-%m-%d')
|
datetime.strptime(start_date, '%Y-%m-%d')
|
||||||
except ValueError:
|
except ValueError:
|
||||||
flash('Invalid season start date format.', 'danger')
|
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_name', name or 'Untitled Season')
|
||||||
AppSettings.set('season_start', start_date)
|
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}')
|
_log('season_started', f'Season: {name or "Untitled Season"}, Start: {start_date}')
|
||||||
flash(f'Season "{name or "Untitled Season"}" has begun.', 'success')
|
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'])
|
@admin_bp.route('/season/end', methods=['POST'])
|
||||||
@@ -307,18 +336,18 @@ def end_season():
|
|||||||
|
|
||||||
if not AppSettings.get_bool('season_active', default=False):
|
if not AppSettings.get_bool('season_active', default=False):
|
||||||
flash('No season is currently active.', 'danger')
|
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()
|
end_date = request.form.get('season_end', '').strip()
|
||||||
if not end_date:
|
if not end_date:
|
||||||
flash('A season end date is required.', 'danger')
|
flash('A season end date is required.', 'danger')
|
||||||
return redirect(url_for('admin.dashboard'))
|
return respond_after_change(url_for('admin.dashboard'))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
datetime.strptime(end_date, '%Y-%m-%d')
|
datetime.strptime(end_date, '%Y-%m-%d')
|
||||||
except ValueError:
|
except ValueError:
|
||||||
flash('Invalid season end date format.', 'danger')
|
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')
|
name = AppSettings.get('season_name')
|
||||||
AppSettings.set('season_end', end_date)
|
AppSettings.set('season_end', end_date)
|
||||||
@@ -326,7 +355,9 @@ def end_season():
|
|||||||
|
|
||||||
_log('season_ended', f'Season: {name}, End: {end_date}')
|
_log('season_ended', f'Season: {name}, End: {end_date}')
|
||||||
flash(f'Season "{name}" has ended.', 'success')
|
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'])
|
@admin_bp.route('/toggle-document-uploads', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def toggle_document_uploads():
|
def toggle_document_uploads():
|
||||||
@@ -341,16 +372,14 @@ def toggle_document_uploads():
|
|||||||
state = 'enabled' if new_value else 'disabled'
|
state = 'enabled' if new_value else 'disabled'
|
||||||
_log('document_uploads_toggled', f'Document uploads {state}')
|
_log('document_uploads_toggled', f'Document uploads {state}')
|
||||||
flash(f'Document uploads are now {state}.', 'success')
|
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
|
# Wipe teams for new season
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
@admin_bp.route('/teams/wipe', methods=['POST'])
|
@admin_bp.route('/teams/wipe', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def wipe_teams():
|
def wipe_teams():
|
||||||
@@ -366,7 +395,7 @@ def wipe_teams():
|
|||||||
confirm = request.form.get('confirm', '').strip()
|
confirm = request.form.get('confirm', '').strip()
|
||||||
if confirm != 'WIPE':
|
if confirm != 'WIPE':
|
||||||
flash("Type 'WIPE' in the confirmation box to proceed.", 'danger')
|
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
|
# Safety backup before destructive operation
|
||||||
try:
|
try:
|
||||||
@@ -376,7 +405,7 @@ def wipe_teams():
|
|||||||
)
|
)
|
||||||
except BackupError as e:
|
except BackupError as e:
|
||||||
flash(f'Wipe aborted — could not create safety backup: {e}', 'danger')
|
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()
|
roster_count = TeamPlayer.query.count()
|
||||||
match_count = TeamMatch.query.count()
|
match_count = TeamMatch.query.count()
|
||||||
@@ -393,4 +422,4 @@ def wipe_teams():
|
|||||||
f'and {match_count} regular-season matches.',
|
f'and {match_count} regular-season matches.',
|
||||||
'success',
|
'success',
|
||||||
)
|
)
|
||||||
return redirect(url_for('admin.dashboard'))
|
return respond_after_change(url_for('admin.dashboard'))
|
||||||
|
|||||||
+32
-26
@@ -7,20 +7,13 @@ from flask import Blueprint, flash, redirect, render_template, request, url_for
|
|||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
from flask_login import current_user, login_required
|
from flask_login import current_user, login_required
|
||||||
from marshmallow import ValidationError
|
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 import func
|
||||||
from sqlalchemy.orm import aliased
|
from sqlalchemy.orm import aliased
|
||||||
|
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.forms import flash_validation_errors, form_payload
|
from app.forms import flash_validation_errors, form_payload
|
||||||
from app.models import (
|
from app.models import (
|
||||||
|
EVALUATION_CRITERIA,
|
||||||
GAME_POSITIONS,
|
GAME_POSITIONS,
|
||||||
Admin,
|
Admin,
|
||||||
Evaluation,
|
Evaluation,
|
||||||
@@ -30,6 +23,7 @@ from app.models import (
|
|||||||
User,
|
User,
|
||||||
)
|
)
|
||||||
from app.pagination import paginate
|
from app.pagination import paginate
|
||||||
|
from app.responders import respond_after_change
|
||||||
from app.validators import EvaluationSchema
|
from app.validators import EvaluationSchema
|
||||||
|
|
||||||
evaluations_bp = Blueprint('evaluations', __name__, url_prefix='/evaluations')
|
evaluations_bp = Blueprint('evaluations', __name__, url_prefix='/evaluations')
|
||||||
@@ -157,12 +151,12 @@ def evaluate_player(tryout_id, player_id):
|
|||||||
)
|
)
|
||||||
if not is_registered:
|
if not is_registered:
|
||||||
flash(_('Player is not registered for this tryout.'), 'danger')
|
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)
|
player = db.get_or_404(User, player_id)
|
||||||
if not isinstance(player, Player):
|
if not isinstance(player, Player):
|
||||||
flash(_('Can only evaluate players.'), 'danger')
|
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(
|
existing_eval = Evaluation.query.filter_by(
|
||||||
tryout_id=tryout_id,
|
tryout_id=tryout_id,
|
||||||
@@ -216,7 +210,7 @@ def evaluate_player(tryout_id, player_id):
|
|||||||
evaluation.position_recommendation = data['position_recommendation']
|
evaluation.position_recommendation = data['position_recommendation']
|
||||||
|
|
||||||
db.session.commit()
|
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()
|
return render_evaluation_form()
|
||||||
|
|
||||||
@@ -291,23 +285,31 @@ def batch_evaluate(tryout_id):
|
|||||||
player = User.query.get(pid)
|
player = User.query.get(pid)
|
||||||
if not player or not isinstance(player, Player):
|
if not player or not isinstance(player, Player):
|
||||||
continue
|
continue
|
||||||
is_registered = TryoutRegistration.query.filter_by(
|
is_registered = (
|
||||||
tryout_id=tryout_id, player_id=pid,
|
TryoutRegistration.query.filter_by(
|
||||||
).first() is not None
|
tryout_id=tryout_id,
|
||||||
|
player_id=pid,
|
||||||
|
).first()
|
||||||
|
is not None
|
||||||
|
)
|
||||||
if not is_registered:
|
if not is_registered:
|
||||||
continue
|
continue
|
||||||
existing = Evaluation.query.filter_by(
|
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()
|
).first()
|
||||||
existing_scores = {
|
existing_scores = {
|
||||||
field_name: getattr(existing, field_name) if existing else None
|
field_name: getattr(existing, field_name) if existing else None
|
||||||
for field_name, _ in EVALUATION_CRITERIA
|
for field_name, _ in EVALUATION_CRITERIA
|
||||||
}
|
}
|
||||||
players.append({
|
players.append(
|
||||||
'player': player,
|
{
|
||||||
'existing': existing,
|
'player': player,
|
||||||
'existing_scores': existing_scores,
|
'existing': existing,
|
||||||
})
|
'existing_scores': existing_scores,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if not players:
|
if not players:
|
||||||
flash('No valid players selected for evaluation.', 'danger')
|
flash('No valid players selected for evaluation.', 'danger')
|
||||||
@@ -329,7 +331,8 @@ def batch_evaluate(tryout_id):
|
|||||||
_apply_evaluation(existing, scores, comments, position)
|
_apply_evaluation(existing, scores, comments, position)
|
||||||
else:
|
else:
|
||||||
evaluation = Evaluation(
|
evaluation = Evaluation(
|
||||||
tryout_id=tryout_id, player_id=pid,
|
tryout_id=tryout_id,
|
||||||
|
player_id=pid,
|
||||||
evaluator_id=current_user.id,
|
evaluator_id=current_user.id,
|
||||||
)
|
)
|
||||||
_apply_evaluation(evaluation, scores, comments, position)
|
_apply_evaluation(evaluation, scores, comments, position)
|
||||||
@@ -338,9 +341,12 @@ def batch_evaluate(tryout_id):
|
|||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(f'Saved evaluations for {saved} player(s).', 'success')
|
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',
|
return render_template(
|
||||||
tryout=tryout, players=players,
|
'pages/batch_evaluate.html',
|
||||||
evaluation_criteria=EVALUATION_CRITERIA,
|
tryout=tryout,
|
||||||
game_positions=GAME_POSITIONS)
|
players=players,
|
||||||
|
evaluation_criteria=EVALUATION_CRITERIA,
|
||||||
|
game_positions=GAME_POSITIONS,
|
||||||
|
)
|
||||||
|
|||||||
+11
-10
@@ -5,7 +5,7 @@ Uses polymorphic isinstance checks instead of role-string comparisons.
|
|||||||
|
|
||||||
from datetime import datetime, timedelta
|
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_babel import gettext as _
|
||||||
from flask_login import current_user, login_required
|
from flask_login import current_user, login_required
|
||||||
from marshmallow import ValidationError
|
from marshmallow import ValidationError
|
||||||
@@ -33,6 +33,7 @@ from app.models import (
|
|||||||
User,
|
User,
|
||||||
)
|
)
|
||||||
from app.permissions import visible_org_teams
|
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.services.scheduling import notify_participants, zip_participants
|
||||||
from app.validators import MatchEditSchema, MatchSchema
|
from app.validators import MatchEditSchema, MatchSchema
|
||||||
|
|
||||||
@@ -445,11 +446,11 @@ def create_match(tryout_id):
|
|||||||
tryout = db.get_or_404(Tryout, tryout_id)
|
tryout = db.get_or_404(Tryout, tryout_id)
|
||||||
if not current_user.can_manage_this_tryout(tryout):
|
if not current_user.can_manage_this_tryout(tryout):
|
||||||
flash(_('You do not have permission to schedule matches for this tryout.'), 'danger')
|
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:
|
if tryout.is_ended:
|
||||||
flash(_('This tryout has ended. Matches can no longer be created or modified.'), 'danger')
|
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()
|
teams = Team.query.filter_by(tryout_id=tryout_id).all()
|
||||||
all_players = registered_players(tryout_id)
|
all_players = registered_players(tryout_id)
|
||||||
@@ -507,7 +508,7 @@ def create_match(tryout_id):
|
|||||||
)
|
)
|
||||||
|
|
||||||
flash(_('Match scheduled successfully!'), 'success')
|
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()
|
return rerender()
|
||||||
|
|
||||||
@@ -521,11 +522,11 @@ def edit_match(match_id):
|
|||||||
|
|
||||||
if not current_user.can_manage_this_tryout(tryout):
|
if not current_user.can_manage_this_tryout(tryout):
|
||||||
flash(_('You do not have permission to edit this match.'), 'danger')
|
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:
|
if tryout.is_ended:
|
||||||
flash(_('This tryout has ended. Matches can no longer be created or modified.'), 'danger')
|
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()
|
teams = Team.query.filter_by(tryout_id=tryout.id).all()
|
||||||
all_players = registered_players(tryout.id)
|
all_players = registered_players(tryout.id)
|
||||||
@@ -636,7 +637,7 @@ def edit_match(match_id):
|
|||||||
)
|
)
|
||||||
|
|
||||||
flash(_('Match updated successfully!'), 'success')
|
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()
|
return rerender()
|
||||||
|
|
||||||
@@ -672,10 +673,10 @@ def delete_match(match_id):
|
|||||||
tryout = match.tryout
|
tryout = match.tryout
|
||||||
if not current_user.can_manage_this_tryout(tryout):
|
if not current_user.can_manage_this_tryout(tryout):
|
||||||
flash(_('You do not have permission to delete this match.'), 'danger')
|
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:
|
if tryout.is_ended:
|
||||||
flash(_('This tryout has ended. Matches can no longer be deleted.'), 'danger')
|
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
|
# Notes outlive the match they were taken during: a coach's observation
|
||||||
# keeps its value, and deleting it here would destroy unrelated content.
|
# 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.delete(match)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_('Match deleted successfully.'), 'success')
|
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):
|
def get_players_available_at_time(date_str, time_str):
|
||||||
|
|||||||
+17
-12
@@ -3,7 +3,7 @@
|
|||||||
Uses polymorphic isinstance checks instead of role-string comparisons.
|
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_babel import gettext as _
|
||||||
from flask_login import current_user, login_required
|
from flask_login import current_user, login_required
|
||||||
from marshmallow import ValidationError
|
from marshmallow import ValidationError
|
||||||
@@ -13,6 +13,7 @@ from app.extensions import db
|
|||||||
from app.forms import flash_validation_errors, form_payload
|
from app.forms import flash_validation_errors, form_payload
|
||||||
from app.models import (
|
from app.models import (
|
||||||
Admin,
|
Admin,
|
||||||
|
AppSettings,
|
||||||
Coach,
|
Coach,
|
||||||
Manager,
|
Manager,
|
||||||
OrgTeam,
|
OrgTeam,
|
||||||
@@ -20,10 +21,10 @@ from app.models import (
|
|||||||
TeamMatch,
|
TeamMatch,
|
||||||
TeamMatchParticipant,
|
TeamMatchParticipant,
|
||||||
TeamPlayer,
|
TeamPlayer,
|
||||||
AppSettings,
|
|
||||||
)
|
)
|
||||||
from app.pagination import paginate
|
from app.pagination import paginate
|
||||||
from app.permissions import can_manage_org_team, coach_org_teams, visible_org_teams
|
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.routes.matches import default_end_time
|
||||||
from app.services.scheduling import notify_participants, zip_participants
|
from app.services.scheduling import notify_participants, zip_participants
|
||||||
from app.time_utils import utc_now_naive
|
from app.time_utils import utc_now_naive
|
||||||
@@ -121,12 +122,14 @@ def create_match(team_id):
|
|||||||
"""Create a new regular-season team match."""
|
"""Create a new regular-season team match."""
|
||||||
team = db.get_or_404(OrgTeam, team_id)
|
team = db.get_or_404(OrgTeam, team_id)
|
||||||
if season_locked():
|
if season_locked():
|
||||||
flash('The regular season is not active. Begin a season before scheduling matches.', 'warning')
|
flash(
|
||||||
return redirect(url_for('team_matches.list_matches'))
|
'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):
|
if not can_manage_team_match(team):
|
||||||
flash(_('You do not have permission to schedule matches for this team.'), 'danger')
|
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()
|
team_players = TeamPlayer.query.filter_by(org_team_id=team_id).all()
|
||||||
prefill_date = request.args.get('date', '')
|
prefill_date = request.args.get('date', '')
|
||||||
@@ -219,7 +222,7 @@ def create_match(team_id):
|
|||||||
flash(
|
flash(
|
||||||
_('Team match "%(title)s" scheduled successfully!', title=team_match.title), 'success'
|
_('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)
|
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):
|
if not can_manage_team_match(team):
|
||||||
flash(_('You do not have permission to edit this match.'), 'danger')
|
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':
|
if request.method == 'POST':
|
||||||
# The three date and time fields used to be checked one at a time,
|
# 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()
|
db.session.commit()
|
||||||
flash(_('Match updated successfully!'), 'success')
|
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(
|
return render_template(
|
||||||
'pages/team_match_form.html', match=team_match, team=team, team_players=[]
|
'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_match = db.get_or_404(TeamMatch, match_id)
|
||||||
team = team_match.org_team
|
team = team_match.org_team
|
||||||
if season_locked():
|
if season_locked():
|
||||||
flash('The regular season is not active. Begin a season before deleting matches.', 'warning')
|
flash(
|
||||||
return redirect(url_for('team_matches.list_matches'))
|
'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):
|
if not can_manage_team_match(team):
|
||||||
flash(_('You do not have permission to delete this match.'), 'danger')
|
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.delete(team_match)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_('Match deleted successfully.'), 'success')
|
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')
|
@team_matches_bp.route('/api/manageable-teams')
|
||||||
|
|||||||
+47
-46
@@ -27,6 +27,7 @@ from app.models import (
|
|||||||
User,
|
User,
|
||||||
)
|
)
|
||||||
from app.permissions import visible_org_teams
|
from app.permissions import visible_org_teams
|
||||||
|
from app.responders import respond_after_change
|
||||||
from app.time_utils import utc_now_naive
|
from app.time_utils import utc_now_naive
|
||||||
from app.validators import NoteContentSchema, OrgTeamSchema, TeamPlayerSchema, TeamStaffSchema
|
from app.validators import NoteContentSchema, OrgTeamSchema, TeamPlayerSchema, TeamStaffSchema
|
||||||
|
|
||||||
@@ -76,7 +77,7 @@ def my_teams():
|
|||||||
"""View the player's own teams with upcoming matches."""
|
"""View the player's own teams with upcoming matches."""
|
||||||
if not isinstance(current_user, Player):
|
if not isinstance(current_user, Player):
|
||||||
flash(_('This page is for players.'), 'info')
|
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
|
from app.models import TeamMatch, TeamMatchParticipant
|
||||||
|
|
||||||
@@ -183,18 +184,18 @@ def create_team():
|
|||||||
"""Create a new organization team."""
|
"""Create a new organization team."""
|
||||||
if not current_user.can_manage_teams():
|
if not current_user.can_manage_teams():
|
||||||
flash(_('You do not have permission to create teams.'), 'danger')
|
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:
|
try:
|
||||||
data = OrgTeamSchema().load(form_payload(list_fields=('coach_ids', 'manager_ids')))
|
data = OrgTeamSchema().load(form_payload(list_fields=('coach_ids', 'manager_ids')))
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
flash_validation_errors(err)
|
flash_validation_errors(err)
|
||||||
return redirect(url_for('teams.list_teams'))
|
return respond_after_change(url_for('teams.list_teams'))
|
||||||
|
|
||||||
name = data['name']
|
name = data['name']
|
||||||
if OrgTeam.query.filter_by(name=name).first():
|
if OrgTeam.query.filter_by(name=name).first():
|
||||||
flash(_('Team "%(name)s" already exists.', name=name), 'danger')
|
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)
|
coach = _staff_member(data['coach_id'], Coach)
|
||||||
manager = _staff_member(data['manager_id'], Manager)
|
manager = _staff_member(data['manager_id'], Manager)
|
||||||
@@ -215,7 +216,7 @@ def create_team():
|
|||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_('Team "%(name)s" created successfully!', name=name), 'success')
|
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'])
|
@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)
|
team = db.get_or_404(OrgTeam, team_id)
|
||||||
if not current_user.can_manage_this_org_team(team):
|
if not current_user.can_manage_this_org_team(team):
|
||||||
flash(_('You do not have permission to edit this team.'), 'danger')
|
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:
|
try:
|
||||||
data = OrgTeamSchema().load(form_payload(list_fields=('coach_ids', 'manager_ids')))
|
data = OrgTeamSchema().load(form_payload(list_fields=('coach_ids', 'manager_ids')))
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
flash_validation_errors(err)
|
flash_validation_errors(err)
|
||||||
return redirect(url_for('teams.list_teams'))
|
return respond_after_change(url_for('teams.list_teams'))
|
||||||
|
|
||||||
name = data['name']
|
name = data['name']
|
||||||
if OrgTeam.query.filter(OrgTeam.name == name, OrgTeam.id != team_id).first():
|
if OrgTeam.query.filter(OrgTeam.name == name, OrgTeam.id != team_id).first():
|
||||||
flash(_('Team "%(name)s" already exists.', name=name), 'danger')
|
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
|
team.name = name
|
||||||
|
|
||||||
@@ -269,7 +270,7 @@ def edit_team(team_id):
|
|||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_('Team "%(name)s" updated successfully!', name=name), 'success')
|
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'])
|
@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)):
|
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')
|
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
|
name = team.name
|
||||||
|
|
||||||
@@ -328,7 +329,7 @@ def delete_team(team_id):
|
|||||||
db.session.delete(team)
|
db.session.delete(team)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_('Team "%(name)s" deleted successfully.', name=name), 'success')
|
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'])
|
@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)
|
team = db.get_or_404(OrgTeam, team_id)
|
||||||
if not current_user.can_manage_this_org_team(team):
|
if not current_user.can_manage_this_org_team(team):
|
||||||
flash(_('Permission denied.'), 'danger')
|
flash(_('Permission denied.'), 'danger')
|
||||||
return redirect(url_for('teams.list_teams'))
|
return respond_after_change(url_for('teams.list_teams'))
|
||||||
|
|
||||||
data = _posted(TeamStaffSchema())
|
data = _posted(TeamStaffSchema())
|
||||||
if data is None:
|
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']:
|
if not data['coach_id']:
|
||||||
flash(_('Please select a coach.'), 'danger')
|
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'])
|
coach = db.session.get(User, data['coach_id'])
|
||||||
if not coach or not _assignable(coach, Coach):
|
if not coach or not _assignable(coach, Coach):
|
||||||
flash(_('Only coaches can be assigned as coach.'), 'danger')
|
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():
|
if team.coaches.filter_by(id=coach.id).first():
|
||||||
flash(
|
flash(
|
||||||
@@ -361,7 +362,7 @@ def add_coach(team_id):
|
|||||||
),
|
),
|
||||||
'info',
|
'info',
|
||||||
)
|
)
|
||||||
return redirect(url_for('teams.list_teams'))
|
return respond_after_change(url_for('teams.list_teams'))
|
||||||
|
|
||||||
team.coaches.append(coach)
|
team.coaches.append(coach)
|
||||||
if not team.coach_id:
|
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),
|
_('%(username)s added as coach of %(name)s.', username=coach.username, name=team.name),
|
||||||
'success',
|
'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'])
|
@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)
|
team = db.get_or_404(OrgTeam, team_id)
|
||||||
if not current_user.can_manage_this_org_team(team):
|
if not current_user.can_manage_this_org_team(team):
|
||||||
flash(_('Permission denied.'), 'danger')
|
flash(_('Permission denied.'), 'danger')
|
||||||
return redirect(url_for('teams.list_teams'))
|
return respond_after_change(url_for('teams.list_teams'))
|
||||||
|
|
||||||
data = _posted(TeamStaffSchema())
|
data = _posted(TeamStaffSchema())
|
||||||
if data is None:
|
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']:
|
if not data['manager_id']:
|
||||||
flash(_('Please select a manager.'), 'danger')
|
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'])
|
manager = db.session.get(User, data['manager_id'])
|
||||||
if not manager or not _assignable(manager, Manager):
|
if not manager or not _assignable(manager, Manager):
|
||||||
flash(_('Only managers can be assigned as manager.'), 'danger')
|
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():
|
if team.managers.filter_by(id=manager.id).first():
|
||||||
flash(
|
flash(
|
||||||
@@ -404,7 +405,7 @@ def add_manager(team_id):
|
|||||||
),
|
),
|
||||||
'info',
|
'info',
|
||||||
)
|
)
|
||||||
return redirect(url_for('teams.list_teams'))
|
return respond_after_change(url_for('teams.list_teams'))
|
||||||
|
|
||||||
team.managers.append(manager)
|
team.managers.append(manager)
|
||||||
if not team.manager_id:
|
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),
|
_('%(username)s added as manager of %(name)s.', username=manager.username, name=team.name),
|
||||||
'success',
|
'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'])
|
@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)
|
team = db.get_or_404(OrgTeam, team_id)
|
||||||
if not current_user.can_manage_this_org_team(team):
|
if not current_user.can_manage_this_org_team(team):
|
||||||
flash(_('Permission denied.'), 'danger')
|
flash(_('Permission denied.'), 'danger')
|
||||||
return redirect(url_for('teams.list_teams'))
|
return respond_after_change(url_for('teams.list_teams'))
|
||||||
|
|
||||||
data = _posted(TeamStaffSchema())
|
data = _posted(TeamStaffSchema())
|
||||||
if data is None:
|
if data is None:
|
||||||
return redirect(url_for('teams.list_teams'))
|
return respond_after_change(url_for('teams.list_teams'))
|
||||||
|
|
||||||
if data['coach_id']:
|
if data['coach_id']:
|
||||||
coach = db.session.get(User, data['coach_id'])
|
coach = db.session.get(User, data['coach_id'])
|
||||||
@@ -442,7 +443,7 @@ def remove_coach(team_id):
|
|||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_('Coach removed from %(name)s.', name=team.name), 'success')
|
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'])
|
@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)
|
team = db.get_or_404(OrgTeam, team_id)
|
||||||
if not current_user.can_manage_this_org_team(team):
|
if not current_user.can_manage_this_org_team(team):
|
||||||
flash(_('Permission denied.'), 'danger')
|
flash(_('Permission denied.'), 'danger')
|
||||||
return redirect(url_for('teams.list_teams'))
|
return respond_after_change(url_for('teams.list_teams'))
|
||||||
|
|
||||||
data = _posted(TeamStaffSchema())
|
data = _posted(TeamStaffSchema())
|
||||||
if data is None:
|
if data is None:
|
||||||
return redirect(url_for('teams.list_teams'))
|
return respond_after_change(url_for('teams.list_teams'))
|
||||||
|
|
||||||
if data['manager_id']:
|
if data['manager_id']:
|
||||||
manager = db.session.get(User, data['manager_id'])
|
manager = db.session.get(User, data['manager_id'])
|
||||||
@@ -470,7 +471,7 @@ def remove_manager(team_id):
|
|||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_('Manager removed from %(name)s.', name=team.name), 'success')
|
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'])
|
@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)
|
team = db.get_or_404(OrgTeam, team_id)
|
||||||
if not current_user.can_manage_this_org_team(team):
|
if not current_user.can_manage_this_org_team(team):
|
||||||
flash(_('Permission denied.'), 'danger')
|
flash(_('Permission denied.'), 'danger')
|
||||||
return redirect(url_for('teams.list_teams'))
|
return respond_after_change(url_for('teams.list_teams'))
|
||||||
|
|
||||||
data = _posted(TeamPlayerSchema())
|
data = _posted(TeamPlayerSchema())
|
||||||
if data is None:
|
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']:
|
if not data['player_id']:
|
||||||
flash(_('Please select a player.'), 'danger')
|
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']
|
status = data['status']
|
||||||
player = db.session.get(User, data['player_id'])
|
player = db.session.get(User, data['player_id'])
|
||||||
if not player or not _assignable(player, Player):
|
if not player or not _assignable(player, Player):
|
||||||
flash(_('Can only assign players to teams.'), 'danger')
|
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()
|
existing = TeamPlayer.query.filter_by(player_id=player.id, org_team_id=team.id).first()
|
||||||
if existing:
|
if existing:
|
||||||
@@ -501,13 +502,13 @@ def add_player(team_id):
|
|||||||
_('%(username)s is already on %(name)s.', username=player.username, name=team.name),
|
_('%(username)s is already on %(name)s.', username=player.username, name=team.name),
|
||||||
'info',
|
'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)
|
tp = TeamPlayer(player_id=player.id, org_team_id=team.id, status=status)
|
||||||
db.session.add(tp)
|
db.session.add(tp)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_('%(username)s added to %(name)s!', username=player.username, name=team.name), 'success')
|
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'])
|
@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)
|
team = db.get_or_404(OrgTeam, team_id)
|
||||||
if not current_user.can_manage_this_org_team(team):
|
if not current_user.can_manage_this_org_team(team):
|
||||||
flash(_('Permission denied.'), 'danger')
|
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)
|
player = db.get_or_404(User, player_id)
|
||||||
tp = TeamPlayer.query.filter_by(player_id=player_id, org_team_id=team_id).first()
|
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),
|
_('%(username)s is not on %(name)s.', username=player.username, name=team.name),
|
||||||
'danger',
|
'danger',
|
||||||
)
|
)
|
||||||
return redirect(url_for('teams.list_teams'))
|
return respond_after_change(url_for('teams.list_teams'))
|
||||||
|
|
||||||
db.session.delete(tp)
|
db.session.delete(tp)
|
||||||
db.session.commit()
|
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),
|
_('%(username)s removed from %(name)s.', username=player.username, name=team.name),
|
||||||
'success',
|
'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'])
|
@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)
|
team = db.get_or_404(OrgTeam, team_id)
|
||||||
if not current_user.can_manage_this_org_team(team):
|
if not current_user.can_manage_this_org_team(team):
|
||||||
flash(_('You do not have permission to add notes to this team.'), 'danger')
|
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:
|
try:
|
||||||
data = NoteContentSchema().load(form_payload(list_fields=()))
|
data = NoteContentSchema().load(form_payload(list_fields=()))
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
flash_validation_errors(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'])
|
note = TeamNote(org_team_id=team_id, coach_id=current_user.id, content=data['content'])
|
||||||
db.session.add(note)
|
db.session.add(note)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_('Team notes added successfully!'), 'success')
|
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'])
|
@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)
|
team = db.get_or_404(OrgTeam, team_id)
|
||||||
if not current_user.can_manage_this_org_team(team):
|
if not current_user.can_manage_this_org_team(team):
|
||||||
flash(_('You do not have permission to add notes to this team.'), 'danger')
|
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)
|
player = db.get_or_404(User, player_id)
|
||||||
if not isinstance(player, Player):
|
if not isinstance(player, Player):
|
||||||
flash(_('Can only add notes for players.'), 'danger')
|
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()
|
tp = TeamPlayer.query.filter_by(player_id=player_id, org_team_id=team_id).first()
|
||||||
if not tp:
|
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),
|
_('%(username)s is not on %(name)s.', username=player.username, name=team.name),
|
||||||
'danger',
|
'danger',
|
||||||
)
|
)
|
||||||
return redirect(url_for('teams.list_teams'))
|
return respond_after_change(url_for('teams.list_teams'))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = NoteContentSchema().load(form_payload(list_fields=()))
|
data = NoteContentSchema().load(form_payload(list_fields=()))
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
flash_validation_errors(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'])
|
note = PersonalNote(player_id=player_id, coach_id=current_user.id, content=data['content'])
|
||||||
db.session.add(note)
|
db.session.add(note)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_('Note added for %(username)s!', username=player.username), 'success')
|
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
@@ -32,6 +32,7 @@ from app.models import (
|
|||||||
TryoutRegistration,
|
TryoutRegistration,
|
||||||
User,
|
User,
|
||||||
)
|
)
|
||||||
|
from app.responders import respond_after_change
|
||||||
from app.time_utils import utc_now_naive
|
from app.time_utils import utc_now_naive
|
||||||
from app.validators import (
|
from app.validators import (
|
||||||
PlayerSelectionSchema,
|
PlayerSelectionSchema,
|
||||||
@@ -477,31 +478,31 @@ def register_for_tryout(tryout_id):
|
|||||||
"""Register a player for a tryout. Only Players can self-register."""
|
"""Register a player for a tryout. Only Players can self-register."""
|
||||||
if not isinstance(current_user, Player):
|
if not isinstance(current_user, Player):
|
||||||
flash(_('Only players can register for tryouts.'), 'danger')
|
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)
|
tryout = locked_tryout_or_404(tryout_id)
|
||||||
|
|
||||||
if tryout.status not in ['upcoming', 'in_progress']:
|
if tryout.status not in ['upcoming', 'in_progress']:
|
||||||
flash(_('This tryout is not accepting registrations.'), 'danger')
|
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(
|
existing = TryoutRegistration.query.filter_by(
|
||||||
tryout_id=tryout_id, player_id=current_user.id
|
tryout_id=tryout_id, player_id=current_user.id
|
||||||
).first()
|
).first()
|
||||||
if existing:
|
if existing:
|
||||||
flash(_('You are already registered for this tryout.'), 'info')
|
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:
|
if tryout.max_players:
|
||||||
count = TryoutRegistration.query.filter_by(tryout_id=tryout_id).count()
|
count = TryoutRegistration.query.filter_by(tryout_id=tryout_id).count()
|
||||||
if count >= tryout.max_players:
|
if count >= tryout.max_players:
|
||||||
flash(_('This tryout is full.'), 'danger')
|
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)
|
registration = TryoutRegistration(tryout_id=tryout_id, player_id=current_user.id)
|
||||||
db.session.add(registration)
|
db.session.add(registration)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_('Successfully registered for tryout!'), 'success')
|
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'])
|
@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.',
|
'Tryouts are currently closed. An admin must open tryouts before changes can be made.',
|
||||||
'danger',
|
'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):
|
if not current_user.can_manage_this_tryout(tryout):
|
||||||
flash(_('Permission denied.'), 'danger')
|
flash(_('Permission denied.'), 'danger')
|
||||||
@@ -523,12 +524,12 @@ def update_status(tryout_id):
|
|||||||
data = TryoutStatusSchema().load(form_payload(list_fields=()))
|
data = TryoutStatusSchema().load(form_payload(list_fields=()))
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
flash_validation_errors(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']
|
tryout.status = data['status']
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_('Tryout status updated to %(new_status)s.', new_status=data['status']), 'success')
|
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'])
|
@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.',
|
'Tryouts are currently closed. An admin must open tryouts before changes can be made.',
|
||||||
'danger',
|
'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):
|
if not current_user.can_manage_this_tryout(tryout):
|
||||||
flash(_('Permission denied.'), 'danger')
|
flash(_('Permission denied.'), 'danger')
|
||||||
@@ -554,12 +555,12 @@ def update_registration_status(tryout_id, player_id):
|
|||||||
data = TryoutRegistrationStatusSchema().load(form_payload(list_fields=()))
|
data = TryoutRegistrationStatusSchema().load(form_payload(list_fields=()))
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
flash_validation_errors(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']
|
registration.status = data['status']
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_('Registration status updated.'), 'success')
|
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'])
|
@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.',
|
'Tryouts are currently closed. An admin must open tryouts before changes can be made.',
|
||||||
'danger',
|
'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):
|
if not current_user.can_manage_this_tryout(tryout):
|
||||||
flash(_('Permission denied.'), 'danger')
|
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:
|
try:
|
||||||
data = PlayerSelectionSchema().load(form_payload())
|
data = PlayerSelectionSchema().load(form_payload())
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
flash_validation_errors(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']:
|
if not data['player_id']:
|
||||||
flash(_('Please select a player.'), 'danger')
|
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
|
# 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
|
# 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'])
|
player = db.session.get(User, data['player_id'])
|
||||||
if not player or not isinstance(player, Player) or not player.is_active_account:
|
if not player or not isinstance(player, Player) or not player.is_active_account:
|
||||||
flash(_('Can only register players.'), 'danger')
|
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()
|
existing = TryoutRegistration.query.filter_by(tryout_id=tryout_id, player_id=player.id).first()
|
||||||
if existing:
|
if existing:
|
||||||
@@ -601,19 +602,19 @@ def register_player(tryout_id):
|
|||||||
_('%(username)s is already registered for this tryout.', username=player.username),
|
_('%(username)s is already registered for this tryout.', username=player.username),
|
||||||
'info',
|
'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:
|
if tryout.max_players:
|
||||||
count = TryoutRegistration.query.filter_by(tryout_id=tryout_id).count()
|
count = TryoutRegistration.query.filter_by(tryout_id=tryout_id).count()
|
||||||
if count >= tryout.max_players:
|
if count >= tryout.max_players:
|
||||||
flash(_('This tryout is full.'), 'danger')
|
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)
|
registration = TryoutRegistration(tryout_id=tryout_id, player_id=player.id)
|
||||||
db.session.add(registration)
|
db.session.add(registration)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_('%(username)s registered for tryout!', username=player.username), 'success')
|
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'])
|
@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.',
|
'Tryouts are currently closed. An admin must open tryouts before changes can be made.',
|
||||||
'danger',
|
'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):
|
if not current_user.can_manage_this_tryout(tryout):
|
||||||
flash(_('Permission denied.'), 'danger')
|
flash(_('Permission denied.'), 'danger')
|
||||||
@@ -656,7 +657,7 @@ def remove_player(tryout_id, player_id):
|
|||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_('%(username)s removed from tryout.', username=player.username), 'success')
|
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'])
|
@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.',
|
'Tryouts are currently closed. An admin must open tryouts before changes can be made.',
|
||||||
'danger',
|
'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):
|
if not current_user.can_manage_this_tryout(tryout):
|
||||||
flash(_('Permission denied.'), 'danger')
|
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:
|
try:
|
||||||
data = TryoutTeamSchema().load(form_payload(list_fields=()))
|
data = TryoutTeamSchema().load(form_payload(list_fields=()))
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
flash_validation_errors(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)
|
team = Team(tryout_id=tryout_id, name=data['team_name'], created_by=current_user.id)
|
||||||
db.session.add(team)
|
db.session.add(team)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_('Team "%(team_name)s" created!', team_name=data['team_name']), 'success')
|
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'])
|
@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.',
|
'Tryouts are currently closed. An admin must open tryouts before changes can be made.',
|
||||||
'danger',
|
'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):
|
if not current_user.can_manage_this_tryout(tryout):
|
||||||
flash(_('Permission denied.'), 'danger')
|
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
|
# 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
|
# 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=()))
|
data = TryoutTeamMemberSchema().load(form_payload(list_fields=()))
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
flash_validation_errors(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']
|
player_id = data['player_id']
|
||||||
|
|
||||||
# Only players registered for this tryout may be placed on its teams.
|
# 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:
|
if not is_registered:
|
||||||
flash(_('That player is not registered for this tryout.'), 'danger')
|
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()
|
existing = TeamMember.query.filter_by(team_id=team_id, player_id=player_id).first()
|
||||||
if existing:
|
if existing:
|
||||||
@@ -735,7 +736,7 @@ def add_to_team(tryout_id, team_id):
|
|||||||
db.session.add(member)
|
db.session.add(member)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_('Player added to team!'), 'success')
|
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'])
|
@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.',
|
'Tryouts are currently closed. An admin must open tryouts before changes can be made.',
|
||||||
'danger',
|
'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):
|
if not current_user.can_manage_this_tryout(tryout):
|
||||||
flash(_('Permission denied.'), 'danger')
|
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:
|
if team.tryout_id != tryout_id:
|
||||||
abort(404)
|
abort(404)
|
||||||
@@ -762,12 +763,12 @@ def edit_team(tryout_id, team_id):
|
|||||||
data = TryoutTeamSchema().load(form_payload(list_fields=()))
|
data = TryoutTeamSchema().load(form_payload(list_fields=()))
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
flash_validation_errors(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']
|
team.name = data['team_name']
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_('Team renamed to "%(team_name)s".', team_name=data['team_name']), 'success')
|
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'])
|
@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.',
|
'Tryouts are currently closed. An admin must open tryouts before changes can be made.',
|
||||||
'danger',
|
'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):
|
if not current_user.can_manage_this_tryout(tryout):
|
||||||
flash(_('Permission denied.'), 'danger')
|
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:
|
if team.tryout_id != tryout_id:
|
||||||
abort(404)
|
abort(404)
|
||||||
@@ -811,7 +812,7 @@ def delete_team(tryout_id, team_id):
|
|||||||
db.session.delete(team)
|
db.session.delete(team)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_('Team "%(name)s" deleted.', name=name), 'success')
|
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'])
|
@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.',
|
'Tryouts are currently closed. An admin must open tryouts before changes can be made.',
|
||||||
'danger',
|
'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):
|
if not current_user.can_manage_this_tryout(tryout):
|
||||||
flash(_('Permission denied.'), 'danger')
|
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:
|
if team.tryout_id != tryout_id:
|
||||||
abort(404)
|
abort(404)
|
||||||
@@ -846,7 +847,7 @@ def remove_from_team(tryout_id, team_id, player_id):
|
|||||||
).delete(synchronize_session=False)
|
).delete(synchronize_session=False)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_('Player removed from team.'), 'success')
|
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(
|
@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.',
|
'Tryouts are currently closed. An admin must open tryouts before changes can be made.',
|
||||||
'danger',
|
'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):
|
if not current_user.can_manage_this_tryout(tryout):
|
||||||
flash(_('Permission denied.'), 'danger')
|
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:
|
if team.tryout_id != tryout_id or member.team_id != team_id:
|
||||||
abort(404)
|
abort(404)
|
||||||
@@ -876,12 +877,12 @@ def update_member_position(tryout_id, team_id, member_id):
|
|||||||
data = TryoutTeamMemberPositionSchema().load(form_payload(list_fields=()))
|
data = TryoutTeamMemberPositionSchema().load(form_payload(list_fields=()))
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
flash_validation_errors(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
|
member.position = data['position'] or None
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_('Position updated.'), 'success')
|
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'])
|
@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.',
|
'Tryouts are currently closed. An admin must open tryouts before changes can be made.',
|
||||||
'danger',
|
'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):
|
if not current_user.can_manage_this_tryout(tryout):
|
||||||
flash(_('You do not have permission to delete this tryout.'), 'danger')
|
flash(_('You do not have permission to delete this tryout.'), 'danger')
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from marshmallow import ValidationError
|
|||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
|
|
||||||
from app.extensions import db
|
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 import Admin, Coach, Contract, Manager, Player, User
|
||||||
from app.models.admin_settings import AppSettings
|
from app.models.admin_settings import AppSettings
|
||||||
from app.permissions import can_manage_player_contract, coach_player_ids
|
from app.permissions import can_manage_player_contract, coach_player_ids
|
||||||
@@ -102,11 +103,11 @@ def upload_contract():
|
|||||||
"""Upload a contract for a player."""
|
"""Upload a contract for a player."""
|
||||||
if not isinstance(current_user, (Admin, Manager, Coach)):
|
if not isinstance(current_user, (Admin, Manager, Coach)):
|
||||||
flash(_('Only presidents, managers, and coaches can upload contracts.'), 'danger')
|
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():
|
if document_uploads_locked():
|
||||||
flash(_('Document uploads are currently disabled.'), 'danger')
|
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()
|
players = manageable_players()
|
||||||
|
|
||||||
@@ -125,13 +126,13 @@ def upload_contract():
|
|||||||
|
|
||||||
if not can_manage_player_contract(current_user, player_id):
|
if not can_manage_player_contract(current_user, player_id):
|
||||||
flash(_('You do not have permission to upload a contract for this player.'), 'danger')
|
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')
|
file = request.files.get('contract_file')
|
||||||
error = pdf_upload_error(file, ALLOWED_CONTRACT_EXTENSIONS)
|
error = pdf_upload_error(file, ALLOWED_CONTRACT_EXTENSIONS)
|
||||||
if error:
|
if error:
|
||||||
flash(error, 'danger')
|
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 = db.get_or_404(User, player_id)
|
||||||
player_teams = player.get_org_teams()
|
player_teams = player.get_org_teams()
|
||||||
@@ -154,7 +155,7 @@ def upload_contract():
|
|||||||
flash(
|
flash(
|
||||||
_('Contract storage is temporarily unavailable. Please try again later.'), 'danger'
|
_('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(
|
contract = Contract(
|
||||||
player_id=player_id,
|
player_id=player_id,
|
||||||
@@ -171,7 +172,7 @@ def upload_contract():
|
|||||||
_('Contract uploaded successfully for %(username)s!', username=player.username),
|
_('Contract uploaded successfully for %(username)s!', username=player.username),
|
||||||
'success',
|
'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)
|
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)
|
contract = db.get_or_404(Contract, contract_id)
|
||||||
if not contract.can_upload_signed(current_user):
|
if not contract.can_upload_signed(current_user):
|
||||||
flash(_('Only the player can upload their signed contract.'), 'danger')
|
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():
|
if document_uploads_locked():
|
||||||
flash(_('Document uploads are currently disabled.'), 'danger')
|
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')
|
file = request.files.get('signed_file')
|
||||||
error = pdf_upload_error(file, ALLOWED_SIGNED_EXTENSIONS)
|
error = pdf_upload_error(file, ALLOWED_SIGNED_EXTENSIONS)
|
||||||
if error:
|
if error:
|
||||||
flash(error, 'danger')
|
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}"
|
signed_filename = f"signed_{contract.stored_filename}"
|
||||||
# Legacy local records keep their signed copy beside the original. Drive
|
# 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)
|
stored_signed_path = store_uploaded_document(file, signed_path)
|
||||||
except GoogleDriveStorageError:
|
except GoogleDriveStorageError:
|
||||||
flash(_('Contract storage is temporarily unavailable. Please try again later.'), 'danger')
|
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_filename = signed_filename
|
||||||
contract.signed_file_path = stored_signed_path
|
contract.signed_file_path = stored_signed_path
|
||||||
@@ -216,7 +217,7 @@ def upload_signed_contract(contract_id):
|
|||||||
contract.signed_at = utc_now_naive()
|
contract.signed_at = utc_now_naive()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_('Signed contract uploaded successfully!'), 'success')
|
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')
|
@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)
|
contract = db.get_or_404(Contract, contract_id)
|
||||||
if not contract.can_view(current_user):
|
if not contract.can_view(current_user):
|
||||||
flash(_('You do not have permission to download this contract.'), 'danger')
|
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:
|
try:
|
||||||
document = open_document(contract.file_path)
|
document = open_document(contract.file_path)
|
||||||
except GoogleDriveStorageError:
|
except GoogleDriveStorageError:
|
||||||
flash(_('Contract storage is temporarily unavailable. Please try again later.'), 'danger')
|
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)
|
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)
|
contract = db.get_or_404(Contract, contract_id)
|
||||||
if not contract.can_view(current_user):
|
if not contract.can_view(current_user):
|
||||||
flash(_('You do not have permission to download this contract.'), 'danger')
|
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:
|
if not contract.signed_file_path:
|
||||||
flash(_('No signed contract available.'), 'danger')
|
flash(_('No signed contract available.'), 'danger')
|
||||||
return redirect(url_for('users.list_contracts'))
|
return respond_after_change(url_for('users.list_contracts'))
|
||||||
try:
|
try:
|
||||||
document = open_document(contract.signed_file_path)
|
document = open_document(contract.signed_file_path)
|
||||||
except GoogleDriveStorageError:
|
except GoogleDriveStorageError:
|
||||||
flash(_('Contract storage is temporarily unavailable. Please try again later.'), 'danger')
|
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)
|
return send_file(document, as_attachment=True, download_name=contract.signed_filename)
|
||||||
|
|||||||
+20
-19
@@ -10,6 +10,7 @@ from flask_login import current_user, login_required
|
|||||||
from marshmallow import ValidationError
|
from marshmallow import ValidationError
|
||||||
|
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
|
from app.responders import respond_after_change
|
||||||
from app.forms import flash_validation_errors, form_payload
|
from app.forms import flash_validation_errors, form_payload
|
||||||
from app.models import (
|
from app.models import (
|
||||||
Coach,
|
Coach,
|
||||||
@@ -176,14 +177,14 @@ def manage_team_notes():
|
|||||||
org_teams = coach_org_teams(current_user)
|
org_teams = coach_org_teams(current_user)
|
||||||
if not org_teams:
|
if not org_teams:
|
||||||
flash(_('You are not assigned to a team.'), 'danger')
|
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]
|
org_team = org_teams[0]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = NoteContentSchema().load(form_payload(list_fields=()))
|
data = NoteContentSchema().load(form_payload(list_fields=()))
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
flash_validation_errors(err)
|
flash_validation_errors(err)
|
||||||
return redirect(url_for('users.notes_dashboard'))
|
return respond_after_change(url_for('users.notes_dashboard'))
|
||||||
|
|
||||||
note = TeamNote(
|
note = TeamNote(
|
||||||
org_team_id=org_team.id,
|
org_team_id=org_team.id,
|
||||||
@@ -194,7 +195,7 @@ def manage_team_notes():
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_('Team notes saved successfully!'), 'success')
|
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=()))
|
data = PersonalNoteSchema().load(form_payload(list_fields=()))
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
flash_validation_errors(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_id = data['player_id']
|
||||||
|
|
||||||
player = db.get_or_404(User, player_id)
|
player = db.get_or_404(User, player_id)
|
||||||
if not isinstance(player, Player):
|
if not isinstance(player, Player):
|
||||||
flash(_('Can only add notes for players.'), 'danger')
|
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):
|
if not coach_can_access_player(current_user, player_id):
|
||||||
flash(_('You can only write notes about players you work with.'), 'danger')
|
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(
|
note = PersonalNote(
|
||||||
player_id=player_id,
|
player_id=player_id,
|
||||||
@@ -234,7 +235,7 @@ def manage_personal_notes():
|
|||||||
db.session.add(note)
|
db.session.add(note)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_('Note added for %(username)s.', username=player.username), 'success')
|
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=()))
|
data = PersonalNoteSchema().load(form_payload(list_fields=()))
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
flash_validation_errors(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_id = data['player_id']
|
||||||
|
|
||||||
player = db.get_or_404(User, player_id)
|
player = db.get_or_404(User, player_id)
|
||||||
if not isinstance(player, Player):
|
if not isinstance(player, Player):
|
||||||
flash(_('Can only add notes for players.'), 'danger')
|
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):
|
if not coach_can_access_player(current_user, player_id):
|
||||||
flash(_('You can only write notes about players you work with.'), 'danger')
|
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']:
|
if data['match_id']:
|
||||||
match = db.get_or_404(Match, data['match_id'])
|
match = db.get_or_404(Match, data['match_id'])
|
||||||
if not current_user.can_manage_this_tryout(match.tryout):
|
if not current_user.can_manage_this_tryout(match.tryout):
|
||||||
flash(_('You cannot use that match as note context.'), 'danger')
|
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():
|
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')
|
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']:
|
if data['tryout_id']:
|
||||||
tryout = db.get_or_404(Tryout, data['tryout_id'])
|
tryout = db.get_or_404(Tryout, data['tryout_id'])
|
||||||
if not current_user.can_manage_this_tryout(tryout):
|
if not current_user.can_manage_this_tryout(tryout):
|
||||||
flash(_('You cannot use that tryout as note context.'), 'danger')
|
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():
|
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')
|
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']:
|
if data['team_id']:
|
||||||
team = db.get_or_404(Team, data['team_id'])
|
team = db.get_or_404(Team, data['team_id'])
|
||||||
if not current_user.can_manage_this_tryout(team.tryout):
|
if not current_user.can_manage_this_tryout(team.tryout):
|
||||||
flash(_('You cannot use that team as note context.'), 'danger')
|
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():
|
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')
|
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(
|
note = PersonalNote(
|
||||||
player_id=player_id,
|
player_id=player_id,
|
||||||
@@ -304,7 +305,7 @@ def add_personal_note():
|
|||||||
db.session.add(note)
|
db.session.add(note)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_('Note added for %(username)s.', username=player.username), 'success')
|
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)
|
tryout = db.get_or_404(Tryout, tryout_id)
|
||||||
if not current_user.can_manage_this_tryout(tryout):
|
if not current_user.can_manage_this_tryout(tryout):
|
||||||
flash(_('You do not have permission to add notes for this tryout.'), 'danger')
|
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)
|
preselected_player_id = request.args.get('player_id', type=int)
|
||||||
|
|
||||||
# Get registrations as players for the select list
|
# 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)
|
match_obj = db.get_or_404(Match, match_id)
|
||||||
if not current_user.can_manage_this_tryout(match_obj.tryout):
|
if not current_user.can_manage_this_tryout(match_obj.tryout):
|
||||||
flash(_('You do not have permission to add notes for this match.'), 'danger')
|
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
|
# Get participants as players for the select list
|
||||||
participants = MatchParticipant.query.filter_by(match_id=match_id).all()
|
participants = MatchParticipant.query.filter_by(match_id=match_id).all()
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from flask_login import current_user, login_required
|
|||||||
from marshmallow import ValidationError
|
from marshmallow import ValidationError
|
||||||
|
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
|
from app.responders import respond_after_change
|
||||||
from app.forms import flash_validation_errors, form_payload
|
from app.forms import flash_validation_errors, form_payload
|
||||||
from app.models import Coach, CoachAvailability, OneOnOneRequest, PersonalNote, Player, TeamNote
|
from app.models import Coach, CoachAvailability, OneOnOneRequest, PersonalNote, Player, TeamNote
|
||||||
from app.routes.users.blueprint import users_bp
|
from app.routes.users.blueprint import users_bp
|
||||||
@@ -64,13 +65,13 @@ def one_on_one():
|
|||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
if not coach:
|
if not coach:
|
||||||
flash(_('Cannot request One on One - no coach assigned.'), 'danger')
|
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:
|
try:
|
||||||
data = OneOnOneRequestSchema().load(form_payload())
|
data = OneOnOneRequestSchema().load(form_payload())
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
flash_validation_errors(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']
|
date_obj = data['date']
|
||||||
start_time = data['start_time']
|
start_time = data['start_time']
|
||||||
@@ -90,7 +91,7 @@ def one_on_one():
|
|||||||
|
|
||||||
if not is_available:
|
if not is_available:
|
||||||
flash(_("The requested time is not within the coach's availability."), 'danger')
|
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(
|
request_obj = OneOnOneRequest(
|
||||||
player_id=current_user.id,
|
player_id=current_user.id,
|
||||||
@@ -118,7 +119,7 @@ def one_on_one():
|
|||||||
)
|
)
|
||||||
|
|
||||||
flash(_('Your One on One request has been submitted!'), 'success')
|
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
|
# Build list of upcoming dates that have coach availability
|
||||||
from datetime import date as date_cls
|
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:
|
if request_obj.coach_id != current_user.id:
|
||||||
flash(_('This request is not for you.'), 'danger')
|
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':
|
if request_obj.status != 'pending':
|
||||||
flash(_('This request has already been processed.'), 'info')
|
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
|
player = request_obj.player
|
||||||
request_obj.status = 'approved'
|
request_obj.status = 'approved'
|
||||||
@@ -204,7 +205,7 @@ def accept_one_on_one(request_id):
|
|||||||
),
|
),
|
||||||
'success',
|
'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'])
|
@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:
|
if request_obj.coach_id != current_user.id:
|
||||||
flash(_('This request is not for you.'), 'danger')
|
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':
|
if request_obj.status != 'pending':
|
||||||
flash(_('This request has already been processed.'), 'info')
|
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:
|
try:
|
||||||
data = OneOnOneRejectionSchema().load(form_payload(list_fields=()))
|
data = OneOnOneRejectionSchema().load(form_payload(list_fields=()))
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
flash_validation_errors(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']
|
rejection_reason = data['rejection_reason']
|
||||||
player = request_obj.player
|
player = request_obj.player
|
||||||
|
|
||||||
@@ -264,4 +265,4 @@ def reject_one_on_one(request_id):
|
|||||||
),
|
),
|
||||||
'info',
|
'info',
|
||||||
)
|
)
|
||||||
return redirect(url_for('users.notes_dashboard'))
|
return respond_after_change(url_for('users.notes_dashboard'))
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from flask_login import current_user, login_required
|
|||||||
from marshmallow import ValidationError
|
from marshmallow import ValidationError
|
||||||
|
|
||||||
from app.extensions import db, hash_password
|
from app.extensions import db, hash_password
|
||||||
|
from app.responders import respond_after_change
|
||||||
from app.logging_config import log_auth_event
|
from app.logging_config import log_auth_event
|
||||||
from app.models import (
|
from app.models import (
|
||||||
ESPORT_GAMES,
|
ESPORT_GAMES,
|
||||||
@@ -129,7 +130,7 @@ def edit_profile():
|
|||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash(_('Profile updated successfully!'), 'success')
|
flash(_('Profile updated successfully!'), 'success')
|
||||||
return redirect(url_for('users.profile'))
|
return respond_after_change(url_for('users.profile'))
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'pages/edit_profile.html',
|
'pages/edit_profile.html',
|
||||||
|
|||||||
+193
-16
@@ -420,22 +420,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
initDraggableBlocks();
|
initDraggableBlocks();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Auto-dismiss flash alerts after 5 seconds.
|
* Auto-dismiss flash alerts after 5 seconds (see armAlertAutoDismiss).
|
||||||
*/
|
*/
|
||||||
const alerts = document.querySelectorAll('.alert-dismissible');
|
armAlertAutoDismiss();
|
||||||
alerts.forEach(function(alert) {
|
|
||||||
setTimeout(function() {
|
|
||||||
if (alert.parentElement) {
|
|
||||||
alert.style.opacity = '0';
|
|
||||||
alert.style.transition = 'opacity 0.3s ease';
|
|
||||||
setTimeout(function() {
|
|
||||||
if (alert.parentElement) {
|
|
||||||
alert.remove();
|
|
||||||
}
|
|
||||||
}, 300);
|
|
||||||
}
|
|
||||||
}, 5000);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
/* =========================================================================
|
/* =========================================================================
|
||||||
Declarative behaviours — replacing inline event handlers
|
Declarative behaviours — replacing inline event handlers
|
||||||
@@ -593,7 +580,7 @@ document.addEventListener('input', function (event) {
|
|||||||
document.addEventListener('change', function (event) {
|
document.addEventListener('change', function (event) {
|
||||||
const control = event.target.closest('[data-submit-on-change]');
|
const control = event.target.closest('[data-submit-on-change]');
|
||||||
if (control && control.form) {
|
if (control && control.form) {
|
||||||
control.form.submit();
|
submitForm(control.form);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -613,3 +600,193 @@ document.addEventListener('change', function (event) {
|
|||||||
window.location.href = select.getAttribute('data-navigate')
|
window.location.href = select.getAttribute('data-navigate')
|
||||||
.replace('{value}', encodeURIComponent(select.value));
|
.replace('{value}', encodeURIComponent(select.value));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* =========================================================================
|
||||||
|
AJAX forms — server-rendered changes without a full page reload
|
||||||
|
=========================================================================
|
||||||
|
|
||||||
|
A <form method="post" data-ajax> is submitted with fetch() instead of a
|
||||||
|
navigation. The route answers a JSON payload (see app/responders.py) that
|
||||||
|
says where to fetch the refreshed content and which flash messages to
|
||||||
|
show; the content region is then swapped in place, so scroll position and
|
||||||
|
open state survive between edits.
|
||||||
|
|
||||||
|
A stale edit — the page changed since this copy was loaded — is answered
|
||||||
|
409, and the user is asked to reload rather than silently overwrite the
|
||||||
|
other person's change.
|
||||||
|
========================================================================= */
|
||||||
|
|
||||||
|
let TT_REVISION = null;
|
||||||
|
|
||||||
|
function armAlertAutoDismiss() {
|
||||||
|
const alerts = document.querySelectorAll('.alert-dismissible');
|
||||||
|
alerts.forEach(function (alert) {
|
||||||
|
if (alert.getAttribute('data-armed')) return;
|
||||||
|
alert.setAttribute('data-armed', '1');
|
||||||
|
setTimeout(function () {
|
||||||
|
if (alert.parentElement) {
|
||||||
|
alert.style.opacity = '0';
|
||||||
|
alert.style.transition = 'opacity 0.3s ease';
|
||||||
|
setTimeout(function () {
|
||||||
|
if (alert.parentElement) {
|
||||||
|
alert.remove();
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchRevision() {
|
||||||
|
return fetch('/api/revision', {
|
||||||
|
headers: { 'Accept': 'application/json' },
|
||||||
|
credentials: 'same-origin'
|
||||||
|
}).then(function (r) { return r.json(); }).then(function (data) {
|
||||||
|
TT_REVISION = data.revision;
|
||||||
|
return data.revision;
|
||||||
|
}).catch(function () {
|
||||||
|
// Revision tracking is best-effort; a failed lookup just disables it.
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showFlashMessages(messages) {
|
||||||
|
const container = document.querySelector('.flash-messages');
|
||||||
|
if (!container || !messages || !messages.length) return;
|
||||||
|
messages.forEach(function (m) {
|
||||||
|
const alert = document.createElement('div');
|
||||||
|
alert.className = 'alert alert-' + (m.category || 'info') + ' alert-dismissible';
|
||||||
|
const span = document.createElement('span');
|
||||||
|
span.textContent = m.text;
|
||||||
|
const close = document.createElement('button');
|
||||||
|
close.type = 'button';
|
||||||
|
close.className = 'alert-close';
|
||||||
|
close.setAttribute('data-action', 'dismiss-alert');
|
||||||
|
close.setAttribute('aria-label', 'Dismiss');
|
||||||
|
close.innerHTML = '×';
|
||||||
|
alert.appendChild(span);
|
||||||
|
alert.appendChild(close);
|
||||||
|
container.appendChild(alert);
|
||||||
|
});
|
||||||
|
armAlertAutoDismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceContent(html, url) {
|
||||||
|
const holder = document.createElement('div');
|
||||||
|
holder.innerHTML = html;
|
||||||
|
const next = holder.querySelector('#page-content');
|
||||||
|
const target = document.getElementById('page-content');
|
||||||
|
if (next && target) {
|
||||||
|
target.innerHTML = next.innerHTML;
|
||||||
|
}
|
||||||
|
if (url && window.history && window.history.replaceState) {
|
||||||
|
window.history.replaceState(null, '', url);
|
||||||
|
}
|
||||||
|
initDraggableBlocks();
|
||||||
|
document.dispatchEvent(new Event('fragment:updated'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshPage(url) {
|
||||||
|
return fetch(url, { credentials: 'same-origin' })
|
||||||
|
.then(function (r) { return r.text(); })
|
||||||
|
.then(function (html) { replaceContent(html, url); });
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitAjaxForm(form) {
|
||||||
|
const headers = {
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
};
|
||||||
|
const token = form.querySelector('input[name="csrf_token"]');
|
||||||
|
if (token) headers['X-CSRFToken'] = token.value;
|
||||||
|
if (TT_REVISION !== null && TT_REVISION !== undefined) {
|
||||||
|
headers['X-Revision'] = String(TT_REVISION);
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(form.action, {
|
||||||
|
method: (form.method || 'POST').toUpperCase(),
|
||||||
|
body: new FormData(form),
|
||||||
|
headers: headers,
|
||||||
|
credentials: 'same-origin'
|
||||||
|
}).then(function (response) {
|
||||||
|
return response.json().then(function (data) {
|
||||||
|
return { status: response.status, data: data };
|
||||||
|
}).catch(function () {
|
||||||
|
return { status: response.status, data: null };
|
||||||
|
});
|
||||||
|
}).then(function (result) {
|
||||||
|
if (result.status === 409) {
|
||||||
|
const message = (result.data && result.data.message) ||
|
||||||
|
'This page was changed by someone else. Reload to see the latest changes.';
|
||||||
|
if (window.confirm(message)) {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (result.status >= 400) {
|
||||||
|
const message = (result.data && (result.data.message || result.data.error)) ||
|
||||||
|
'Something went wrong. Please try again.';
|
||||||
|
window.alert(message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return result.data;
|
||||||
|
}).then(function (data) {
|
||||||
|
if (!data) return;
|
||||||
|
if (data.revision !== undefined && data.revision !== null) {
|
||||||
|
TT_REVISION = data.revision;
|
||||||
|
}
|
||||||
|
showFlashMessages(data.messages);
|
||||||
|
if (data.redirect) {
|
||||||
|
return refreshPage(data.redirect);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}).catch(function (err) {
|
||||||
|
console.error('AJAX form submission failed:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitForm(form) {
|
||||||
|
if (form.hasAttribute('data-ajax')) {
|
||||||
|
submitAjaxForm(form);
|
||||||
|
} else {
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkForRemoteChanges() {
|
||||||
|
return fetchRevision().then(function (rev) {
|
||||||
|
if (TT_REVISION !== null && rev !== TT_REVISION) {
|
||||||
|
TT_REVISION = rev;
|
||||||
|
return refreshPage(window.location.href);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}).catch(function () { /* best-effort */ });
|
||||||
|
}
|
||||||
|
|
||||||
|
// data-submit-on-change and data-ajax both funnel through submitForm so a
|
||||||
|
// <select> change uses fetch() when the form is marked, and a plain submit
|
||||||
|
// otherwise. (form.submit() does not fire the submit event, so intercepting
|
||||||
|
// only 'submit' would miss the selects.)
|
||||||
|
document.addEventListener('submit', function (event) {
|
||||||
|
const form = event.target.closest('form[data-ajax]');
|
||||||
|
if (!form || event.defaultPrevented) return;
|
||||||
|
event.preventDefault();
|
||||||
|
submitAjaxForm(form);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Refresh when the user returns to the tab: if someone else changed the data,
|
||||||
|
// this brings the view up to date without a constant polling loop.
|
||||||
|
document.addEventListener('visibilitychange', function () {
|
||||||
|
if (document.visibilityState === 'visible') {
|
||||||
|
checkForRemoteChanges();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
window.addEventListener('focus', function () {
|
||||||
|
checkForRemoteChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
fetchRevision();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.TT = window.TT || {};
|
||||||
|
window.TT.refreshPage = refreshPage;
|
||||||
@@ -118,4 +118,4 @@ def main():
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
sys.exit(main())
|
sys.exit(main())
|
||||||
|
|||||||
@@ -173,7 +173,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content" id="page-content">
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="POST" action="
|
<form method="POST" data-ajax action="
|
||||||
{% if context_type == 'tryout' %}
|
{% if context_type == 'tryout' %}
|
||||||
{{ url_for('users.add_note_from_tryout', tryout_id=tryout.id) }}
|
{{ url_for('users.add_note_from_tryout', tryout_id=tryout.id) }}
|
||||||
{% elif context_type == 'match' %}
|
{% elif context_type == 'match' %}
|
||||||
|
|||||||
@@ -73,7 +73,7 @@
|
|||||||
Only admins can create or modify tryouts. Coaches and managers are locked out.
|
Only admins can create or modify tryouts. Coaches and managers are locked out.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
<form method="POST" action="{{ url_for('admin.toggle_tryouts') }}">
|
<form method="POST" data-ajax action="{{ url_for('admin.toggle_tryouts') }}">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<button type="submit" class="btn btn-{% if settings.tryouts_open %}warning{% else %}success{% endif %}">
|
<button type="submit" class="btn btn-{% if settings.tryouts_open %}warning{% else %}success{% endif %}">
|
||||||
{% if settings.tryouts_open %}Close Tryouts{% else %}Open Tryouts{% endif %}
|
{% if settings.tryouts_open %}Close Tryouts{% else %}Open Tryouts{% endif %}
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
Only admins can upload or sign contracts. Other roles are blocked.
|
Only admins can upload or sign contracts. Other roles are blocked.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
<form method="POST" action="{{ url_for('admin.toggle_document_uploads') }}">
|
<form method="POST" data-ajax action="{{ url_for('admin.toggle_document_uploads') }}">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<button type="submit" class="btn btn-{% if settings.document_uploads_enabled %}warning{% else %}success{% endif %}">
|
<button type="submit" class="btn btn-{% if settings.document_uploads_enabled %}warning{% else %}success{% endif %}">
|
||||||
{% if settings.document_uploads_enabled %}Disable Uploads{% else %}Enable Uploads{% endif %}
|
{% if settings.document_uploads_enabled %}Disable Uploads{% else %}Enable Uploads{% endif %}
|
||||||
@@ -126,7 +126,7 @@
|
|||||||
|
|
||||||
{% if not settings.season_active %}
|
{% if not settings.season_active %}
|
||||||
<hr style="margin: 12px 0;">
|
<hr style="margin: 12px 0;">
|
||||||
<form method="POST" action="{{ url_for('admin.start_season') }}">
|
<form method="POST" data-ajax action="{{ url_for('admin.start_season') }}">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Season Name</label>
|
<label>Season Name</label>
|
||||||
@@ -140,7 +140,7 @@
|
|||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
<hr style="margin: 12px 0;">
|
<hr style="margin: 12px 0;">
|
||||||
<form method="POST" action="{{ url_for('admin.end_season') }}">
|
<form method="POST" data-ajax action="{{ url_for('admin.end_season') }}">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>End Date</label>
|
<label>End Date</label>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
{% block breadcrumb %}<span class="breadcrumb">Home / <a href="{{ url_for('tryouts.view_tryout', tryout_id=tryout.id) }}">{{ tryout.title }}</a> / Evaluate</span>{% endblock %}
|
{% block breadcrumb %}<span class="breadcrumb">Home / <a href="{{ url_for('tryouts.view_tryout', tryout_id=tryout.id) }}">{{ tryout.title }}</a> / Evaluate</span>{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<form method="POST" action="{{ url_for('evaluations.batch_evaluate', tryout_id=tryout.id) }}" class="form batch-eval-form">
|
<form method="POST" data-ajax action="{{ url_for('evaluations.batch_evaluate', tryout_id=tryout.id) }}" class="form batch-eval-form">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
{% for entry in players %}
|
{% for entry in players %}
|
||||||
<input type="hidden" name="player_ids" value="{{ entry.player.id }}"/>
|
<input type="hidden" name="player_ids" value="{{ entry.player.id }}"/>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<form method="POST" action="{{ url_for('evaluations.evaluate_player', tryout_id=tryout.id, player_id=player.id) }}" class="form">
|
<form method="POST" data-ajax action="{{ url_for('evaluations.evaluate_player', tryout_id=tryout.id, player_id=player.id) }}" class="form">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="POST" action="{% if match %}{{ url_for('matches.edit_match', match_id=match.id) }}{% elif is_practice %}{{ url_for('team_matches.create_match', team_id=team_id) }}{% else %}{{ url_for('matches.create_match', tryout_id=tryout.id) }}{% endif %}" class="form" id="matchForm">
|
<form method="POST" data-ajax action="{% if match %}{{ url_for('matches.edit_match', match_id=match.id) }}{% elif is_practice %}{{ url_for('team_matches.create_match', team_id=team_id) }}{% else %}{{ url_for('matches.create_match', tryout_id=tryout.id) }}{% endif %}" class="form" id="matchForm">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
{% if is_practice %}
|
{% if is_practice %}
|
||||||
<input type="hidden" name="title" value="Practice">
|
<input type="hidden" name="title" value="Practice">
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ function togglePresence(btn) {
|
|||||||
btn.classList.add('btn-outline');
|
btn.classList.add('btn-outline');
|
||||||
btn.innerHTML = 'Confirm';
|
btn.innerHTML = 'Confirm';
|
||||||
}
|
}
|
||||||
location.reload();
|
window.TT.refreshPage(window.location.pathname);
|
||||||
})
|
})
|
||||||
.catch(function(error) {
|
.catch(function(error) {
|
||||||
console.error('Error:', error);
|
console.error('Error:', error);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<span class="badge badge-esport">{{ org_team.name }}</span>
|
<span class="badge badge-esport">{{ org_team.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="POST" action="{{ url_for('users.manage_team_notes') }}" class="form" id="teamNotesForm">
|
<form method="POST" data-ajax action="{{ url_for('users.manage_team_notes') }}" class="form" id="teamNotesForm">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="POST" action="{{ url_for('users.add_personal_note') }}" class="form" id="personalNoteForm">
|
<form method="POST" data-ajax action="{{ url_for('users.add_personal_note') }}" class="form" id="personalNoteForm">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -145,7 +145,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if req.status == 'pending' %}
|
{% if req.status == 'pending' %}
|
||||||
<form method="POST" action="{{ url_for('users.accept_one_on_one', request_id=req.id) }}" style="display:inline;">
|
<form method="POST" data-ajax action="{{ url_for('users.accept_one_on_one', request_id=req.id) }}" style="display:inline;">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<button type="submit" class="btn btn-sm btn-success" title="{{ _('Accept') }}">
|
<button type="submit" class="btn btn-sm btn-success" title="{{ _('Accept') }}">
|
||||||
<i class="fas fa-check"></i> {{ _('Accept') }}
|
<i class="fas fa-check"></i> {{ _('Accept') }}
|
||||||
@@ -177,7 +177,7 @@
|
|||||||
<h4><i class="fas fa-times-circle"></i> {{ _('Reject One on One Request') }}</h4>
|
<h4><i class="fas fa-times-circle"></i> {{ _('Reject One on One Request') }}</h4>
|
||||||
<button type="button" class="modal-close" data-action="hide-reject-modal">×</button>
|
<button type="button" class="modal-close" data-action="hide-reject-modal">×</button>
|
||||||
</div>
|
</div>
|
||||||
<form id="rejectForm" method="POST" action="">
|
<form id="rejectForm" method="POST" data-ajax action="">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|||||||
@@ -124,7 +124,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% if coach %}
|
{% if coach %}
|
||||||
<form method="POST" action="{{ url_for('users.one_on_one') }}" class="form" id="oneOnOneForm">
|
<form method="POST" data-ajax action="{{ url_for('users.one_on_one') }}" class="form" id="oneOnOneForm">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="POST" action="{{ url_for('users.manage_personal_notes') }}" class="form">
|
<form method="POST" data-ajax action="{{ url_for('users.manage_personal_notes') }}" class="form">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<h3><i class="fas fa-futbol"></i> {% if match %}Edit Match{% else %}Schedule New Match{% endif %} — {{ team.name }}</h3>
|
<h3><i class="fas fa-futbol"></i> {% if match %}Edit Match{% else %}Schedule New Match{% endif %} — {{ team.name }}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="POST" action="{% if match %}{{ url_for('team_matches.edit_match', match_id=match.id) }}{% else %}{{ url_for('team_matches.create_match', team_id=team.id) }}{% endif %}" class="form">
|
<form method="POST" data-ajax action="{% if match %}{{ url_for('team_matches.edit_match', match_id=match.id) }}{% else %}{{ url_for('team_matches.create_match', team_id=team.id) }}{% endif %}" class="form">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
|
|||||||
@@ -98,7 +98,7 @@
|
|||||||
<a href="{{ url_for('team_matches.edit_match', match_id=m.id) }}" class="btn btn-sm btn-outline" title="{{ _('Edit Match') }}">
|
<a href="{{ url_for('team_matches.edit_match', match_id=m.id) }}" class="btn btn-sm btn-outline" title="{{ _('Edit Match') }}">
|
||||||
<i class="fas fa-edit"></i>
|
<i class="fas fa-edit"></i>
|
||||||
</a>
|
</a>
|
||||||
<form method="POST" action="{{ url_for('team_matches.delete_match', match_id=m.id) }}" class="inline-form" data-confirm="{{ _('Delete this match?') }}">
|
<form method="POST" data-ajax action="{{ url_for('team_matches.delete_match', match_id=m.id) }}" class="inline-form" data-confirm="{{ _('Delete this match?') }}">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<button type="submit" class="btn btn-sm btn-danger" title="{{ _('Delete Match') }}">
|
<button type="submit" class="btn btn-sm btn-danger" title="{{ _('Delete Match') }}">
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
@@ -170,8 +170,8 @@ function togglePresence(btn) {
|
|||||||
btn.classList.add('btn-outline');
|
btn.classList.add('btn-outline');
|
||||||
btn.innerHTML = 'Confirm';
|
btn.innerHTML = 'Confirm';
|
||||||
}
|
}
|
||||||
// Reload to update presence bar
|
// Update in place to preserve state
|
||||||
location.reload();
|
window.TT.refreshPage(window.location.pathname);
|
||||||
})
|
})
|
||||||
.catch(function(error) {
|
.catch(function(error) {
|
||||||
console.error('Error:', error);
|
console.error('Error:', error);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="POST" action="{{ url_for('users.manage_team_notes') }}" class="form">
|
<form method="POST" data-ajax action="{{ url_for('users.manage_team_notes') }}" class="form">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<h3>{{ _('Create New Team') }}</h3>
|
<h3>{{ _('Create New Team') }}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="POST" action="{{ url_for('teams.create_team') }}" class="form">
|
<form method="POST" data-ajax action="{{ url_for('teams.create_team') }}" class="form">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group col-4">
|
<div class="form-group col-4">
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
<button class="btn btn-sm btn-outline edit-team-btn" data-team-id="{{ team.id }}" data-team-name="{{ team.name }}" data-coach-id="{{ team.coach_id or '' }}" data-manager-id="{{ team.manager_id or '' }}">
|
<button class="btn btn-sm btn-outline edit-team-btn" data-team-id="{{ team.id }}" data-team-name="{{ team.name }}" data-coach-id="{{ team.coach_id or '' }}" data-manager-id="{{ team.manager_id or '' }}">
|
||||||
<i class="fas fa-edit"></i> {{ _('Edit') }}
|
<i class="fas fa-edit"></i> {{ _('Edit') }}
|
||||||
</button>
|
</button>
|
||||||
<form method="POST" action="{{ url_for('teams.delete_team', team_id=team.id) }}" class="inline-form" data-confirm="{{ _('Delete team %(name)s? It will be unassigned from any linked tryouts.', name=team.name) }}">
|
<form method="POST" data-ajax action="{{ url_for('teams.delete_team', team_id=team.id) }}" class="inline-form" data-confirm="{{ _('Delete team %(name)s? It will be unassigned from any linked tryouts.', name=team.name) }}">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<button type="submit" class="btn btn-sm btn-danger">
|
<button type="submit" class="btn btn-sm btn-danger">
|
||||||
<i class="fas fa-trash"></i> {{ _('Delete') }}
|
<i class="fas fa-trash"></i> {{ _('Delete') }}
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
<span class="staff-tag">
|
<span class="staff-tag">
|
||||||
{{ c.username }}
|
{{ c.username }}
|
||||||
{% if can_manage %}
|
{% if can_manage %}
|
||||||
<form method="POST" action="{{ url_for('teams.remove_coach', team_id=team.id) }}" class="inline-form" data-confirm="{{ _('Remove coach %(coach)s from %(team)s?', coach=c.username, team=team.name) }}">
|
<form method="POST" data-ajax action="{{ url_for('teams.remove_coach', team_id=team.id) }}" class="inline-form" data-confirm="{{ _('Remove coach %(coach)s from %(team)s?', coach=c.username, team=team.name) }}">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<input type="hidden" name="coach_id" value="{{ c.id }}"/>
|
<input type="hidden" name="coach_id" value="{{ c.id }}"/>
|
||||||
<button type="submit" class="btn-icon-sm" title="{{ _('Remove Coach') }}">×</button>
|
<button type="submit" class="btn-icon-sm" title="{{ _('Remove Coach') }}">×</button>
|
||||||
@@ -111,7 +111,7 @@
|
|||||||
<span class="staff-tag manager-tag">
|
<span class="staff-tag manager-tag">
|
||||||
{{ m.username }}
|
{{ m.username }}
|
||||||
{% if can_manage %}
|
{% if can_manage %}
|
||||||
<form method="POST" action="{{ url_for('teams.remove_manager', team_id=team.id) }}" class="inline-form" data-confirm="{{ _('Remove manager %(manager)s from %(team)s?', manager=m.username, team=team.name) }}">
|
<form method="POST" data-ajax action="{{ url_for('teams.remove_manager', team_id=team.id) }}" class="inline-form" data-confirm="{{ _('Remove manager %(manager)s from %(team)s?', manager=m.username, team=team.name) }}">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<input type="hidden" name="manager_id" value="{{ m.id }}"/>
|
<input type="hidden" name="manager_id" value="{{ m.id }}"/>
|
||||||
<button type="submit" class="btn-icon-sm" title="{{ _('Remove Manager') }}">×</button>
|
<button type="submit" class="btn-icon-sm" title="{{ _('Remove Manager') }}">×</button>
|
||||||
@@ -180,7 +180,7 @@
|
|||||||
<td>{{ entry.player.phone or '-' }}</td>
|
<td>{{ entry.player.phone or '-' }}</td>
|
||||||
{% if can_manage_team %}
|
{% if can_manage_team %}
|
||||||
<td>
|
<td>
|
||||||
<form method="POST" action="{{ url_for('teams.remove_player', team_id=team.id, player_id=entry.player.id) }}" class="inline-form" data-confirm="{{ _('Remove %(player)s from %(team)s?', player=entry.player.username, team=team.name) }}">
|
<form method="POST" data-ajax action="{{ url_for('teams.remove_player', team_id=team.id, player_id=entry.player.id) }}" class="inline-form" data-confirm="{{ _('Remove %(player)s from %(team)s?', player=entry.player.username, team=team.name) }}">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<button type="submit" class="btn btn-sm btn-danger">
|
<button type="submit" class="btn btn-sm btn-danger">
|
||||||
<i class="fas fa-user-minus"></i> {{ _('Remove') }}
|
<i class="fas fa-user-minus"></i> {{ _('Remove') }}
|
||||||
@@ -210,7 +210,7 @@
|
|||||||
<hr class="my-3">
|
<hr class="my-3">
|
||||||
<div class="add-player-section">
|
<div class="add-player-section">
|
||||||
<h5 class="mb-2"><i class="fas fa-user-plus"></i> Add Player to {{ team.name }}</h5>
|
<h5 class="mb-2"><i class="fas fa-user-plus"></i> Add Player to {{ team.name }}</h5>
|
||||||
<form method="POST" action="{{ url_for('teams.add_player', team_id=team.id) }}" class="form-inline">
|
<form method="POST" data-ajax action="{{ url_for('teams.add_player', team_id=team.id) }}" class="form-inline">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<select name="player_id" class="form-select" required>
|
<select name="player_id" class="form-select" required>
|
||||||
<option value="">{{ _('-- Select a player --') }}</option>
|
<option value="">{{ _('-- Select a player --') }}</option>
|
||||||
@@ -258,7 +258,7 @@
|
|||||||
<button class="modal-close" data-action="hide-edit-form">×</button>
|
<button class="modal-close" data-action="hide-edit-form">×</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form id="editTeamForm" method="POST" action="" class="form">
|
<form id="editTeamForm" method="POST" data-ajax action="" class="form">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<input type="hidden" name="sync_staff" value="1"/>
|
<input type="hidden" name="sync_staff" value="1"/>
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if can_edit %}
|
{% if can_edit %}
|
||||||
<form method="POST" action="{{ url_for('tryouts.update_status', tryout_id=tryout.id) }}" class="inline-form">
|
<form method="POST" data-ajax action="{{ url_for('tryouts.update_status', tryout_id=tryout.id) }}" class="inline-form">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<select name="status" data-submit-on-change class="form-select">
|
<select name="status" data-submit-on-change class="form-select">
|
||||||
<option value="upcoming" {% if tryout.status == 'upcoming' %}selected{% endif %}>{{ _('Upcoming') }}</option>
|
<option value="upcoming" {% if tryout.status == 'upcoming' %}selected{% endif %}>{{ _('Upcoming') }}</option>
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
{% if current_user.role == 'player' and not is_registered and tryout.status in ['upcoming', 'in_progress'] %}
|
{% if current_user.role == 'player' and not is_registered and tryout.status in ['upcoming', 'in_progress'] %}
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-body text-center">
|
<div class="card-body text-center">
|
||||||
<form method="POST" action="{{ url_for('tryouts.register_for_tryout', tryout_id=tryout.id) }}">
|
<form method="POST" data-ajax action="{{ url_for('tryouts.register_for_tryout', tryout_id=tryout.id) }}">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<button type="submit" class="btn btn-primary btn-lg">
|
<button type="submit" class="btn btn-primary btn-lg">
|
||||||
<i class="fas fa-user-plus"></i> {{ _('Register for this Tryout') }}
|
<i class="fas fa-user-plus"></i> {{ _('Register for this Tryout') }}
|
||||||
@@ -124,7 +124,7 @@
|
|||||||
<h3><i class="fas fa-user-plus"></i> {{ _('Add Player to Tryout') }}</h3>
|
<h3><i class="fas fa-user-plus"></i> {{ _('Add Player to Tryout') }}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="POST" action="{{ url_for('tryouts.register_player', tryout_id=tryout.id) }}" class="form-inline">
|
<form method="POST" data-ajax action="{{ url_for('tryouts.register_player', tryout_id=tryout.id) }}" class="form-inline">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<select name="player_id" class="form-select" required>
|
<select name="player_id" class="form-select" required>
|
||||||
<option value="">{{ _('-- Select a player --') }}</option>
|
<option value="">{{ _('-- Select a player --') }}</option>
|
||||||
@@ -173,7 +173,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if can_edit %}
|
{% if can_edit %}
|
||||||
<form method="POST" action="{{ url_for('tryouts.update_registration_status', tryout_id=tryout.id, player_id=p.id) }}" class="inline-form">
|
<form method="POST" data-ajax action="{{ url_for('tryouts.update_registration_status', tryout_id=tryout.id, player_id=p.id) }}" class="inline-form">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<select name="status" data-submit-on-change class="form-select form-select-sm">
|
<select name="status" data-submit-on-change class="form-select form-select-sm">
|
||||||
<option value="registered" {% if reg.status == 'registered' %}selected{% endif %}>{{ _('Registered') }}</option>
|
<option value="registered" {% if reg.status == 'registered' %}selected{% endif %}>{{ _('Registered') }}</option>
|
||||||
@@ -205,7 +205,7 @@
|
|||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if not tryout.is_ended %}
|
{% if not tryout.is_ended %}
|
||||||
<form method="POST" action="{{ url_for('tryouts.remove_player', tryout_id=tryout.id, player_id=p.id) }}" class="inline-form" data-confirm="{{ _('Remove %(name)s from this tryout? They will also be removed from every team and match within it.', name=p.username) }}">
|
<form method="POST" data-ajax action="{{ url_for('tryouts.remove_player', tryout_id=tryout.id, player_id=p.id) }}" class="inline-form" data-confirm="{{ _('Remove %(name)s from this tryout? They will also be removed from every team and match within it.', name=p.username) }}">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<button type="submit" class="btn btn-sm btn-danger" title="{{ _('Remove player from tryout') }}">
|
<button type="submit" class="btn btn-sm btn-danger" title="{{ _('Remove player from tryout') }}">
|
||||||
<i class="fas fa-user-minus"></i> {{ _('Remove') }}
|
<i class="fas fa-user-minus"></i> {{ _('Remove') }}
|
||||||
@@ -238,7 +238,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% if can_edit and not tryout.is_ended %}
|
{% if can_edit and not tryout.is_ended %}
|
||||||
<div id="createTeamForm" class="hidden mb-3">
|
<div id="createTeamForm" class="hidden mb-3">
|
||||||
<form method="POST" action="{{ url_for('tryouts.create_team', tryout_id=tryout.id) }}" class="form-inline">
|
<form method="POST" data-ajax action="{{ url_for('tryouts.create_team', tryout_id=tryout.id) }}" class="form-inline">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<input type="text" name="team_name" placeholder="{{ _('Team name') }}" required class="form-input mr-2">
|
<input type="text" name="team_name" placeholder="{{ _('Team name') }}" required class="form-input mr-2">
|
||||||
<button type="submit" class="btn btn-sm btn-success">{{ _('Create') }}</button>
|
<button type="submit" class="btn btn-sm btn-success">{{ _('Create') }}</button>
|
||||||
@@ -284,7 +284,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<!-- Rename -->
|
<!-- Rename -->
|
||||||
<form method="POST" action="{{ url_for('tryouts.edit_team', tryout_id=tryout.id, team_id=team.team.id) }}" class="form-inline mb-3">
|
<form method="POST" data-ajax 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() }}"/>
|
<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>
|
<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;">
|
<input type="text" id="renameInput{{ team.team.id }}" name="team_name" value="{{ team.team.name }}" required class="form-input" style="flex:1;">
|
||||||
@@ -300,7 +300,7 @@
|
|||||||
<div class="roster-row">
|
<div class="roster-row">
|
||||||
<div class="avatar-sm">{{ member.player.username[:2] | upper }}</div>
|
<div class="avatar-sm">{{ member.player.username[:2] | upper }}</div>
|
||||||
<span class="roster-player-name">{{ member.player.username }}</span>
|
<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">
|
<form method="POST" data-ajax 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() }}"/>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<select name="position" class="form-select roster-role-select" data-submit-on-change>
|
<select name="position" class="form-select roster-role-select" data-submit-on-change>
|
||||||
<option value="">{{ _('No role') }}</option>
|
<option value="">{{ _('No role') }}</option>
|
||||||
@@ -309,7 +309,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</form>
|
</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) }}">
|
<form method="POST" data-ajax 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() }}"/>
|
<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') }}">
|
<button type="submit" class="btn btn-sm btn-danger roster-remove-btn" title="{{ _('Remove from team') }}">
|
||||||
<i class="fas fa-user-minus"></i>
|
<i class="fas fa-user-minus"></i>
|
||||||
@@ -324,7 +324,7 @@
|
|||||||
{% if registered_players %}
|
{% if registered_players %}
|
||||||
<hr class="section-divider">
|
<hr class="section-divider">
|
||||||
<h5 class="mb-2"><i class="fas fa-user-plus"></i> {{ _('Add Player') }}</h5>
|
<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">
|
<form method="POST" data-ajax 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() }}"/>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<select name="player_id" class="form-select" required style="flex:1;">
|
<select name="player_id" class="form-select" required style="flex:1;">
|
||||||
<option value="">{{ _('Select player...') }}</option>
|
<option value="">{{ _('Select player...') }}</option>
|
||||||
@@ -350,7 +350,7 @@
|
|||||||
|
|
||||||
<hr class="section-divider" style="border-color: var(--danger);">
|
<hr class="section-divider" style="border-color: var(--danger);">
|
||||||
<div class="text-center">
|
<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.') }}">
|
<form method="POST" data-ajax 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() }}"/>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<button type="submit" class="btn btn-danger">{{ _('Delete Team') }}</button>
|
<button type="submit" class="btn btn-danger">{{ _('Delete Team') }}</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -529,7 +529,7 @@
|
|||||||
<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?') }}">
|
<form method="POST" data-ajax 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() }}"/>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<button type="submit" class="btn btn-sm btn-danger" title="{{ _('Delete Match') }}">
|
<button type="submit" class="btn btn-sm btn-danger" title="{{ _('Delete Match') }}">
|
||||||
<i class="fas fa-trash"></i> {{ _('Delete') }}
|
<i class="fas fa-trash"></i> {{ _('Delete') }}
|
||||||
@@ -717,35 +717,46 @@ function toggleTryoutPresence(matchId, participantId, btn) {
|
|||||||
btn.classList.add('presence-pending-btn');
|
btn.classList.add('presence-pending-btn');
|
||||||
btn.innerHTML = btn.innerHTML.replace('✅', '⏳');
|
btn.innerHTML = btn.innerHTML.replace('✅', '⏳');
|
||||||
}
|
}
|
||||||
location.reload();
|
window.TT.refreshPage(window.location.pathname);
|
||||||
})
|
})
|
||||||
.catch(function(err) {
|
.catch(function(err) {
|
||||||
console.error('Error toggling tryout presence:', err);
|
console.error('Error toggling tryout presence:', err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mini calendar for matches
|
// Mini calendar for matches. Re-initialised on fragment:updated so the
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
// calendar survives AJAX edits that swap the page content in place.
|
||||||
|
var miniCalendar = null;
|
||||||
|
|
||||||
|
function initMiniCalendar() {
|
||||||
var miniCalendarEl = document.getElementById('mini-calendar');
|
var miniCalendarEl = document.getElementById('mini-calendar');
|
||||||
if (miniCalendarEl) {
|
if (!miniCalendarEl) {
|
||||||
var miniCalendar = new FullCalendar.Calendar(miniCalendarEl, {
|
return;
|
||||||
initialView: 'dayGridMonth',
|
|
||||||
headerToolbar: {
|
|
||||||
left: 'prev,next',
|
|
||||||
center: 'title',
|
|
||||||
right: 'dayGridMonth,timeGridWeek'
|
|
||||||
},
|
|
||||||
events: '/matches/api/events/{{ tryout.id }}',
|
|
||||||
height: '300px',
|
|
||||||
eventClick: function(info) {
|
|
||||||
if (info.event.extendedProps.match_id) {
|
|
||||||
window.location.href = '/matches/' + info.event.extendedProps.match_id + '/edit';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
miniCalendar.render();
|
|
||||||
}
|
}
|
||||||
});
|
if (miniCalendar) {
|
||||||
|
miniCalendar.destroy();
|
||||||
|
miniCalendar = null;
|
||||||
|
}
|
||||||
|
miniCalendar = new FullCalendar.Calendar(miniCalendarEl, {
|
||||||
|
initialView: 'dayGridMonth',
|
||||||
|
headerToolbar: {
|
||||||
|
left: 'prev,next',
|
||||||
|
center: 'title',
|
||||||
|
right: 'dayGridMonth,timeGridWeek'
|
||||||
|
},
|
||||||
|
events: '/matches/api/events/{{ tryout.id }}',
|
||||||
|
height: '300px',
|
||||||
|
eventClick: function(info) {
|
||||||
|
if (info.event.extendedProps.match_id) {
|
||||||
|
window.location.href = '/matches/' + info.event.extendedProps.match_id + '/edit';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
miniCalendar.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', initMiniCalendar);
|
||||||
|
document.addEventListener('fragment:updated', initMiniCalendar);
|
||||||
|
|
||||||
// Behaviours declared in the markup, dispatched by the delegated listener
|
// Behaviours declared in the markup, dispatched by the delegated listener
|
||||||
// in main.js. Inline onclick attributes cannot be authorised by a CSP nonce.
|
// in main.js. Inline onclick attributes cannot be authorised by a CSP nonce.
|
||||||
|
|||||||
@@ -59,4 +59,4 @@ class TestAppSettingsBool:
|
|||||||
AppSettings.set_bool('bool_key', True)
|
AppSettings.set_bool('bool_key', True)
|
||||||
assert AppSettings.get('bool_key') == 'true'
|
assert AppSettings.get('bool_key') == 'true'
|
||||||
AppSettings.set_bool('bool_key', False)
|
AppSettings.set_bool('bool_key', False)
|
||||||
assert AppSettings.get('bool_key') == 'false'
|
assert AppSettings.get('bool_key') == 'false'
|
||||||
|
|||||||
@@ -58,4 +58,4 @@ class TestAuditLogRecord:
|
|||||||
|
|
||||||
entry = AuditLog.query.filter_by(action='before_delete').first()
|
entry = AuditLog.query.filter_by(action='before_delete').first()
|
||||||
assert entry is not None
|
assert entry is not None
|
||||||
assert entry.user_id is None # FK set to NULL on delete
|
assert entry.user_id is None # FK set to NULL on delete
|
||||||
|
|||||||
@@ -41,7 +41,9 @@ class TestBackupCreate:
|
|||||||
|
|
||||||
as_role('admin')
|
as_role('admin')
|
||||||
response = client.post(
|
response = client.post(
|
||||||
'/admin/backup/create', data={'notes': 'test backup'}, follow_redirects=False,
|
'/admin/backup/create',
|
||||||
|
data={'notes': 'test backup'},
|
||||||
|
follow_redirects=False,
|
||||||
)
|
)
|
||||||
assert _redirected(response)
|
assert _redirected(response)
|
||||||
|
|
||||||
@@ -61,7 +63,9 @@ class TestBackupCreate:
|
|||||||
|
|
||||||
as_role('admin')
|
as_role('admin')
|
||||||
response = client.post(
|
response = client.post(
|
||||||
'/admin/backup/create', data={}, follow_redirects=True,
|
'/admin/backup/create',
|
||||||
|
data={},
|
||||||
|
follow_redirects=True,
|
||||||
)
|
)
|
||||||
html = response.data.decode()
|
html = response.data.decode()
|
||||||
assert 'Backup failed' in html
|
assert 'Backup failed' in html
|
||||||
@@ -135,7 +139,8 @@ class TestBackupDownload:
|
|||||||
record_id = record.id
|
record_id = record.id
|
||||||
|
|
||||||
response = client.get(
|
response = client.get(
|
||||||
f'/admin/backup/{record_id}/download', follow_redirects=True,
|
f'/admin/backup/{record_id}/download',
|
||||||
|
follow_redirects=True,
|
||||||
)
|
)
|
||||||
html = response.data.decode()
|
html = response.data.decode()
|
||||||
assert 'missing from disk' in html.lower()
|
assert 'missing from disk' in html.lower()
|
||||||
@@ -162,7 +167,8 @@ class TestBackupDelete:
|
|||||||
record_id = record.id
|
record_id = record.id
|
||||||
|
|
||||||
response = client.post(
|
response = client.post(
|
||||||
f'/admin/backup/{record_id}/delete', follow_redirects=False,
|
f'/admin/backup/{record_id}/delete',
|
||||||
|
follow_redirects=False,
|
||||||
)
|
)
|
||||||
assert _redirected(response)
|
assert _redirected(response)
|
||||||
|
|
||||||
@@ -214,14 +220,13 @@ class TestBackupRestore:
|
|||||||
record_id = record.id
|
record_id = record.id
|
||||||
|
|
||||||
response = client.post(
|
response = client.post(
|
||||||
f'/admin/backup/{record_id}/restore', follow_redirects=True,
|
f'/admin/backup/{record_id}/restore',
|
||||||
|
follow_redirects=True,
|
||||||
)
|
)
|
||||||
html = response.data.decode()
|
html = response.data.decode()
|
||||||
assert 'missing from disk' in html.lower()
|
assert 'missing from disk' in html.lower()
|
||||||
|
|
||||||
def test_restore_creates_safety_backup_first(
|
def test_restore_creates_safety_backup_first(self, app, client, as_role, monkeypatch, tmp_path):
|
||||||
self, app, client, as_role, monkeypatch, tmp_path
|
|
||||||
):
|
|
||||||
import app.routes.admin as admin_module
|
import app.routes.admin as admin_module
|
||||||
|
|
||||||
# The restore route parses DATABASE_URL outside its try block.
|
# The restore route parses DATABASE_URL outside its try block.
|
||||||
@@ -282,4 +287,4 @@ class TestBackupRestore:
|
|||||||
with app.app_context():
|
with app.app_context():
|
||||||
safety = BackupRecord.query.filter_by(filename='db_backup_safety.dump').first()
|
safety = BackupRecord.query.filter_by(filename='db_backup_safety.dump').first()
|
||||||
assert safety is not None
|
assert safety is not None
|
||||||
assert safety.backup_type == 'pre_restore'
|
assert safety.backup_type == 'pre_restore'
|
||||||
|
|||||||
@@ -53,4 +53,4 @@ class TestAdminCsrfProtection:
|
|||||||
# Flask-WTF returns 400 on missing CSRF token
|
# Flask-WTF returns 400 on missing CSRF token
|
||||||
assert response.status_code in (400, 302), (
|
assert response.status_code in (400, 302), (
|
||||||
f'{route} returned {response.status_code} without CSRF token'
|
f'{route} returned {response.status_code} without CSRF token'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -147,4 +147,4 @@ class TestAdminTemplateIntegrity:
|
|||||||
def test_static_js_loads(self, client):
|
def test_static_js_loads(self, client):
|
||||||
response = client.get('/static/js/main.js')
|
response = client.get('/static/js/main.js')
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert 'javascript' in response.content_type or 'text/' in response.content_type
|
assert 'javascript' in response.content_type or 'text/' in response.content_type
|
||||||
|
|||||||
@@ -47,4 +47,4 @@ class TestAdminRouteRegistration:
|
|||||||
return
|
return
|
||||||
assert _redirected(response), (
|
assert _redirected(response), (
|
||||||
f'{route} answered {response.status_code} to an anonymous caller'
|
f'{route} answered {response.status_code} to an anonymous caller'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -154,7 +154,9 @@ class TestSeasonGateEnforcement:
|
|||||||
|
|
||||||
as_role(role)
|
as_role(role)
|
||||||
response = client.post(
|
response = client.post(
|
||||||
f'/team-matches/{match_id}/delete', data={}, follow_redirects=True,
|
f'/team-matches/{match_id}/delete',
|
||||||
|
data={},
|
||||||
|
follow_redirects=True,
|
||||||
)
|
)
|
||||||
html = response.data.decode()
|
html = response.data.decode()
|
||||||
assert 'season is not active' in html.lower() or 'begin a season' in html.lower()
|
assert 'season is not active' in html.lower() or 'begin a season' in html.lower()
|
||||||
@@ -167,4 +169,4 @@ class TestSeasonGateEnforcement:
|
|||||||
data={'title': 'Admin Match', 'date': '2026-10-01', 'start_time': '10:00'},
|
data={'title': 'Admin Match', 'date': '2026-10-01', 'start_time': '10:00'},
|
||||||
follow_redirects=False,
|
follow_redirects=False,
|
||||||
)
|
)
|
||||||
assert _redirected(response) # success, not blocked
|
assert _redirected(response) # success, not blocked
|
||||||
|
|||||||
@@ -84,4 +84,4 @@ class TestAdminImages:
|
|||||||
def test_esports_logo_image_loads(self, client):
|
def test_esports_logo_image_loads(self, client):
|
||||||
# The original logo may still be referenced
|
# The original logo may still be referenced
|
||||||
response = client.get('/static/images/UdeS_logo.png')
|
response = client.get('/static/images/UdeS_logo.png')
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|||||||
@@ -76,9 +76,7 @@ class TestTryoutGateEnforcement:
|
|||||||
assert 'closed' in html.lower() or 'open tryouts' in html.lower()
|
assert 'closed' in html.lower() or 'open tryouts' in html.lower()
|
||||||
|
|
||||||
@pytest.mark.parametrize('role', NON_ADMIN_ROLES)
|
@pytest.mark.parametrize('role', NON_ADMIN_ROLES)
|
||||||
def test_non_admin_cannot_edit_tryout_when_closed(
|
def test_non_admin_cannot_edit_tryout_when_closed(self, app, client, as_role, make_user, role):
|
||||||
self, app, client, as_role, make_user, role
|
|
||||||
):
|
|
||||||
# Create a tryout as admin first
|
# Create a tryout as admin first
|
||||||
as_role('admin')
|
as_role('admin')
|
||||||
client.post(
|
client.post(
|
||||||
@@ -119,7 +117,9 @@ class TestTryoutGateEnforcement:
|
|||||||
|
|
||||||
as_role(role)
|
as_role(role)
|
||||||
response = client.post(
|
response = client.post(
|
||||||
f'/tryouts/{tryout_id}/delete', data={}, follow_redirects=True,
|
f'/tryouts/{tryout_id}/delete',
|
||||||
|
data={},
|
||||||
|
follow_redirects=True,
|
||||||
)
|
)
|
||||||
html = response.data.decode()
|
html = response.data.decode()
|
||||||
assert 'closed' in html.lower() or 'open tryouts' in html.lower()
|
assert 'closed' in html.lower() or 'open tryouts' in html.lower()
|
||||||
@@ -153,4 +153,4 @@ class TestTryoutGateEnforcement:
|
|||||||
data={'title': 'Admin Edited', 'game': 'Valorant', 'date': '2026-12-01'},
|
data={'title': 'Admin Edited', 'game': 'Valorant', 'date': '2026-12-01'},
|
||||||
follow_redirects=False,
|
follow_redirects=False,
|
||||||
)
|
)
|
||||||
assert _redirected(response) # success, not blocked
|
assert _redirected(response) # success, not blocked
|
||||||
|
|||||||
@@ -9,7 +9,12 @@ import pytest
|
|||||||
|
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.models import (
|
from app.models import (
|
||||||
AuditLog, BackupRecord, OrgTeam, TeamMatch, TeamMatchParticipant, TeamPlayer,
|
AuditLog,
|
||||||
|
BackupRecord,
|
||||||
|
OrgTeam,
|
||||||
|
TeamMatch,
|
||||||
|
TeamMatchParticipant,
|
||||||
|
TeamPlayer,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -21,7 +26,9 @@ class TestWipeConfirmation:
|
|||||||
def test_wipe_without_confirmation_fails(self, app, client, as_role):
|
def test_wipe_without_confirmation_fails(self, app, client, as_role):
|
||||||
as_role('admin')
|
as_role('admin')
|
||||||
response = client.post(
|
response = client.post(
|
||||||
'/admin/teams/wipe', data={}, follow_redirects=True,
|
'/admin/teams/wipe',
|
||||||
|
data={},
|
||||||
|
follow_redirects=True,
|
||||||
)
|
)
|
||||||
html = response.data.decode()
|
html = response.data.decode()
|
||||||
assert 'WIPE' in html
|
assert 'WIPE' in html
|
||||||
@@ -29,7 +36,9 @@ class TestWipeConfirmation:
|
|||||||
def test_wipe_with_wrong_confirmation_fails(self, app, client, as_role):
|
def test_wipe_with_wrong_confirmation_fails(self, app, client, as_role):
|
||||||
as_role('admin')
|
as_role('admin')
|
||||||
response = client.post(
|
response = client.post(
|
||||||
'/admin/teams/wipe', data={'confirm': 'yes'}, follow_redirects=True,
|
'/admin/teams/wipe',
|
||||||
|
data={'confirm': 'yes'},
|
||||||
|
follow_redirects=True,
|
||||||
)
|
)
|
||||||
html = response.data.decode()
|
html = response.data.decode()
|
||||||
assert 'WIPE' in html
|
assert 'WIPE' in html
|
||||||
@@ -116,4 +125,4 @@ class TestWipeDataRemoval:
|
|||||||
|
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
entry = AuditLog.query.filter_by(action='teams_wiped').first()
|
entry = AuditLog.query.filter_by(action='teams_wiped').first()
|
||||||
assert entry is not None
|
assert entry is not None
|
||||||
|
|||||||
Reference in New Issue
Block a user