style: formater le depot avec ruff format
QUA-002, premiere moitie. **Ce commit ne fait que reformater** : aucun changement de comportement, aucune ligne de logique touchee. 72 fichiers, 4 restaient deja conformes. Il est isole exprès, pour que `git log -p` sur les commits voisins reste lisible. `quote-style = "preserve"` etait deja pose dans pyproject.toml, ce qui evite le brassage guillemets simples / doubles : le diff porte sur les retours a la ligne, l indentation des appels longs et les virgules finales, pas sur le style de chaine. Verification : 263 tests passent avant et apres, ruff check propre. L activation en CI arrive dans le commit suivant, separement, pour que ce diff-ci ne contienne rien d autre. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
+32
-20
@@ -78,8 +78,7 @@ def is_safe_url(url):
|
||||
|
||||
parsed = urlparse(url)
|
||||
if parsed.netloc:
|
||||
return (parsed.netloc == request.host
|
||||
and parsed.scheme in ('', 'http', 'https'))
|
||||
return parsed.netloc == request.host and parsed.scheme in ('', 'http', 'https')
|
||||
# Relative targets must be rooted. 'dashboard' or 'javascript:...' are
|
||||
# not paths on this site.
|
||||
return url.startswith('/')
|
||||
@@ -112,7 +111,7 @@ def cooloff_minutes(failed_attempts):
|
||||
int: Minutes.
|
||||
"""
|
||||
steps = max(failed_attempts // MAX_LOGIN_ATTEMPTS - 1, 0)
|
||||
return min(LOCKOUT_DURATION_MINUTES * (2 ** steps), MAX_LOCKOUT_MINUTES)
|
||||
return min(LOCKOUT_DURATION_MINUTES * (2**steps), MAX_LOCKOUT_MINUTES)
|
||||
|
||||
|
||||
def generate_captcha():
|
||||
@@ -124,6 +123,7 @@ def generate_captcha():
|
||||
dict: A dictionary with 'question' (e.g., '3 + 7') and 'id' keys.
|
||||
"""
|
||||
import random
|
||||
|
||||
a = random.randint(1, 10)
|
||||
b = random.randint(1, 10)
|
||||
captcha_id = str(uuid.uuid4())
|
||||
@@ -199,8 +199,7 @@ def login():
|
||||
|
||||
if user and credentials_ok:
|
||||
if not user.is_active_account:
|
||||
log_auth_event('login.rejected.deactivated',
|
||||
username=username, user_id=user.id)
|
||||
log_auth_event('login.rejected.deactivated', username=username, user_id=user.id)
|
||||
flash(_('This account has been deactivated.'), 'danger')
|
||||
return render_template('pages/login.html')
|
||||
|
||||
@@ -219,9 +218,7 @@ def login():
|
||||
# back into French — the preference lives in the session, and
|
||||
# clearing it discards a decision the user just made.
|
||||
_preserved = {
|
||||
key: session[key]
|
||||
for key in ('csrf_token', LOCALE_SESSION_KEY)
|
||||
if key in session
|
||||
key: session[key] for key in ('csrf_token', LOCALE_SESSION_KEY) if key in session
|
||||
}
|
||||
session.clear()
|
||||
session.update(_preserved)
|
||||
@@ -232,8 +229,7 @@ def login():
|
||||
session.permanent = True
|
||||
|
||||
login_user(user)
|
||||
log_auth_event('login.success',
|
||||
username=user.username, user_id=user.id, role=user.role)
|
||||
log_auth_event('login.success', username=user.username, user_id=user.id, role=user.role)
|
||||
|
||||
# Validate redirect URL to prevent open redirect vulnerability
|
||||
next_page = request.args.get('next')
|
||||
@@ -248,20 +244,33 @@ def login():
|
||||
# who asked (SEC-017).
|
||||
if user:
|
||||
user.failed_login_attempts += 1
|
||||
log_auth_event('login.failure', username=username, user_id=user.id,
|
||||
attempts=user.failed_login_attempts)
|
||||
log_auth_event(
|
||||
'login.failure',
|
||||
username=username,
|
||||
user_id=user.id,
|
||||
attempts=user.failed_login_attempts,
|
||||
)
|
||||
if user.failed_login_attempts >= MAX_LOGIN_ATTEMPTS:
|
||||
minutes = cooloff_minutes(user.failed_login_attempts)
|
||||
user.locked_until = datetime.utcnow() + timedelta(minutes=minutes)
|
||||
log_auth_event('account.throttled', username=username,
|
||||
user_id=user.id, minutes=minutes,
|
||||
attempts=user.failed_login_attempts)
|
||||
log_auth_event(
|
||||
'account.throttled',
|
||||
username=username,
|
||||
user_id=user.id,
|
||||
minutes=minutes,
|
||||
attempts=user.failed_login_attempts,
|
||||
)
|
||||
db.session.commit()
|
||||
else:
|
||||
log_auth_event('login.failure.unknown_user', username=username)
|
||||
|
||||
flash(_('Login unsuccessful. Please check your username and '
|
||||
'password, or ask a president for help.'), 'danger')
|
||||
flash(
|
||||
_(
|
||||
'Login unsuccessful. Please check your username and '
|
||||
'password, or ask a president for help.'
|
||||
),
|
||||
'danger',
|
||||
)
|
||||
|
||||
return render_template('pages/login.html')
|
||||
|
||||
@@ -375,6 +384,7 @@ def register():
|
||||
|
||||
# Create UserGamertag records for each selected game
|
||||
from app.models import UserGamertag
|
||||
|
||||
for game in selected_games:
|
||||
field_name = f'gamertag_{game}'
|
||||
gamertag_value = request.form.get(field_name, '').strip()
|
||||
@@ -458,8 +468,10 @@ def discord_callback():
|
||||
|
||||
if not expected_state or not secrets.compare_digest(expected_state, received_state):
|
||||
flash(
|
||||
_('Discord authorization could not be verified. '
|
||||
'Please start the connection again from this page.'),
|
||||
_(
|
||||
'Discord authorization could not be verified. '
|
||||
'Please start the connection again from this page.'
|
||||
),
|
||||
'danger',
|
||||
)
|
||||
return redirect(url_for('auth.register'))
|
||||
@@ -585,4 +597,4 @@ def logout():
|
||||
if _locale:
|
||||
session[LOCALE_SESSION_KEY] = _locale
|
||||
flash(_('You have been logged out.'), 'info')
|
||||
return redirect(url_for('auth.login'))
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
+96
-48
@@ -8,8 +8,12 @@ from flask_login import login_required, current_user
|
||||
from flask_babel import gettext as _
|
||||
from app.extensions import db
|
||||
from app.models import (
|
||||
Admin, Player,
|
||||
User, Tryout, Evaluation, TryoutRegistration,
|
||||
Admin,
|
||||
Player,
|
||||
User,
|
||||
Tryout,
|
||||
Evaluation,
|
||||
TryoutRegistration,
|
||||
GAME_POSITIONS,
|
||||
)
|
||||
from sqlalchemy import func
|
||||
@@ -74,22 +78,29 @@ def list_evaluations():
|
||||
sort_expr = sort_expr.desc()
|
||||
|
||||
if isinstance(user, Admin):
|
||||
evaluations = Evaluation.query \
|
||||
.outerjoin(Tryout, Evaluation.tryout_id == Tryout.id) \
|
||||
.outerjoin(player_alias, Evaluation.player_id == player_alias.id) \
|
||||
.outerjoin(evaluator_alias, Evaluation.evaluator_id == evaluator_alias.id) \
|
||||
.order_by(sort_expr).all()
|
||||
avg_scores = db.session.query(
|
||||
Evaluation.player_id,
|
||||
func.count(Evaluation.id).label('eval_count'),
|
||||
func.avg(Evaluation.overall_score).label('avg_score'),
|
||||
).group_by(Evaluation.player_id).all()
|
||||
evaluations = (
|
||||
Evaluation.query.outerjoin(Tryout, Evaluation.tryout_id == Tryout.id)
|
||||
.outerjoin(player_alias, Evaluation.player_id == player_alias.id)
|
||||
.outerjoin(evaluator_alias, Evaluation.evaluator_id == evaluator_alias.id)
|
||||
.order_by(sort_expr)
|
||||
.all()
|
||||
)
|
||||
avg_scores = (
|
||||
db.session.query(
|
||||
Evaluation.player_id,
|
||||
func.count(Evaluation.id).label('eval_count'),
|
||||
func.avg(Evaluation.overall_score).label('avg_score'),
|
||||
)
|
||||
.group_by(Evaluation.player_id)
|
||||
.all()
|
||||
)
|
||||
player_scores = {}
|
||||
for row in avg_scores:
|
||||
p = User.query.get(row.player_id)
|
||||
if p:
|
||||
player_scores[p.id] = {
|
||||
'player': p, 'count': row.eval_count,
|
||||
'player': p,
|
||||
'count': row.eval_count,
|
||||
'avg': round(row.avg_score, 1) if row.avg_score else 0,
|
||||
}
|
||||
else:
|
||||
@@ -97,17 +108,23 @@ def list_evaluations():
|
||||
# can_evaluate() is true for the four remaining roles. The former
|
||||
# `else` branch listed evaluations *received* — a player's view,
|
||||
# unreachable from this point (ARCH-007).
|
||||
evaluations = Evaluation.query \
|
||||
.outerjoin(Tryout, Evaluation.tryout_id == Tryout.id) \
|
||||
.outerjoin(player_alias, Evaluation.player_id == player_alias.id) \
|
||||
.outerjoin(evaluator_alias, Evaluation.evaluator_id == evaluator_alias.id) \
|
||||
.filter(Evaluation.evaluator_id == user.id) \
|
||||
.order_by(sort_expr).all()
|
||||
evaluations = (
|
||||
Evaluation.query.outerjoin(Tryout, Evaluation.tryout_id == Tryout.id)
|
||||
.outerjoin(player_alias, Evaluation.player_id == player_alias.id)
|
||||
.outerjoin(evaluator_alias, Evaluation.evaluator_id == evaluator_alias.id)
|
||||
.filter(Evaluation.evaluator_id == user.id)
|
||||
.order_by(sort_expr)
|
||||
.all()
|
||||
)
|
||||
player_scores = {}
|
||||
|
||||
return render_template('pages/evaluations.html',
|
||||
evaluations=evaluations, player_scores=player_scores,
|
||||
sort_column=sort_column, sort_order=sort_order)
|
||||
return render_template(
|
||||
'pages/evaluations.html',
|
||||
evaluations=evaluations,
|
||||
player_scores=player_scores,
|
||||
sort_column=sort_column,
|
||||
sort_order=sort_order,
|
||||
)
|
||||
|
||||
|
||||
@evaluations_bp.route('/<int:tryout_id>/<int:player_id>', methods=['GET', 'POST'])
|
||||
@@ -123,9 +140,13 @@ def evaluate_player(tryout_id, player_id):
|
||||
flash(_('You do not have permission to evaluate players in this tryout.'), 'danger')
|
||||
return redirect(url_for('tryouts.list_tryouts'))
|
||||
|
||||
is_registered = TryoutRegistration.query.filter_by(
|
||||
tryout_id=tryout_id, player_id=player_id,
|
||||
).first() is not None
|
||||
is_registered = (
|
||||
TryoutRegistration.query.filter_by(
|
||||
tryout_id=tryout_id,
|
||||
player_id=player_id,
|
||||
).first()
|
||||
is not None
|
||||
)
|
||||
if not is_registered:
|
||||
flash(_('Player is not registered for this tryout.'), 'danger')
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
@@ -136,7 +157,9 @@ def evaluate_player(tryout_id, player_id):
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
|
||||
existing_eval = Evaluation.query.filter_by(
|
||||
tryout_id=tryout_id, player_id=player_id, evaluator_id=current_user.id,
|
||||
tryout_id=tryout_id,
|
||||
player_id=player_id,
|
||||
evaluator_id=current_user.id,
|
||||
).first()
|
||||
|
||||
if request.method == 'POST':
|
||||
@@ -152,9 +175,21 @@ def evaluate_player(tryout_id, player_id):
|
||||
comments = request.form.get('comments')
|
||||
position = request.form.get('position_recommendation')
|
||||
|
||||
scores = [s for s in [mecanics, cohesion, communication, gamesense,
|
||||
versatility, discipline, analysis, sport_ethics, mental]
|
||||
if s is not None]
|
||||
scores = [
|
||||
s
|
||||
for s in [
|
||||
mecanics,
|
||||
cohesion,
|
||||
communication,
|
||||
gamesense,
|
||||
versatility,
|
||||
discipline,
|
||||
analysis,
|
||||
sport_ethics,
|
||||
mental,
|
||||
]
|
||||
if s is not None
|
||||
]
|
||||
overall = sum(scores) / len(scores) if scores else None
|
||||
|
||||
if existing_eval:
|
||||
@@ -173,14 +208,21 @@ def evaluate_player(tryout_id, player_id):
|
||||
flash(_('Evaluation updated!'), 'success')
|
||||
else:
|
||||
evaluation = Evaluation(
|
||||
tryout_id=tryout_id, player_id=player_id,
|
||||
tryout_id=tryout_id,
|
||||
player_id=player_id,
|
||||
evaluator_id=current_user.id,
|
||||
mecanics_score=mecanics, cohesion_score=cohesion,
|
||||
communication_score=communication, gamesense_score=gamesense,
|
||||
versatility_score=versatility, discipline_score=discipline,
|
||||
analysis_score=analysis, sport_ethics_score=sport_ethics,
|
||||
mental_score=mental, overall_score=overall,
|
||||
comments=comments, position_recommendation=position,
|
||||
mecanics_score=mecanics,
|
||||
cohesion_score=cohesion,
|
||||
communication_score=communication,
|
||||
gamesense_score=gamesense,
|
||||
versatility_score=versatility,
|
||||
discipline_score=discipline,
|
||||
analysis_score=analysis,
|
||||
sport_ethics_score=sport_ethics,
|
||||
mental_score=mental,
|
||||
overall_score=overall,
|
||||
comments=comments,
|
||||
position_recommendation=position,
|
||||
)
|
||||
db.session.add(evaluation)
|
||||
flash(_('Evaluation submitted successfully!'), 'success')
|
||||
@@ -191,16 +233,21 @@ def evaluate_player(tryout_id, player_id):
|
||||
evaluators = None
|
||||
if isinstance(current_user, Admin):
|
||||
all_evaluations = Evaluation.query.filter_by(
|
||||
tryout_id=tryout_id, player_id=player_id,
|
||||
tryout_id=tryout_id,
|
||||
player_id=player_id,
|
||||
).all()
|
||||
evaluators = [{'evaluator': User.query.get(e.evaluator_id), 'eval': e}
|
||||
for e in all_evaluations]
|
||||
evaluators = [
|
||||
{'evaluator': User.query.get(e.evaluator_id), 'eval': e} for e in all_evaluations
|
||||
]
|
||||
|
||||
return render_template('pages/evaluate_player.html',
|
||||
tryout=tryout, player=player,
|
||||
existing_eval=existing_eval,
|
||||
evaluators=evaluators,
|
||||
game_positions=GAME_POSITIONS)
|
||||
return render_template(
|
||||
'pages/evaluate_player.html',
|
||||
tryout=tryout,
|
||||
player=player,
|
||||
existing_eval=existing_eval,
|
||||
evaluators=evaluators,
|
||||
game_positions=GAME_POSITIONS,
|
||||
)
|
||||
|
||||
|
||||
@evaluations_bp.route('/<int:tryout_id>/players')
|
||||
@@ -222,9 +269,10 @@ def players_to_evaluate(tryout_id):
|
||||
p = User.query.get(reg.player_id)
|
||||
if p and isinstance(p, Player):
|
||||
existing = Evaluation.query.filter_by(
|
||||
tryout_id=tryout_id, player_id=p.id, evaluator_id=current_user.id,
|
||||
tryout_id=tryout_id,
|
||||
player_id=p.id,
|
||||
evaluator_id=current_user.id,
|
||||
).first()
|
||||
players.append({'player': p, 'evaluated': existing is not None,
|
||||
'registration': reg})
|
||||
players.append({'player': p, 'evaluated': existing is not None, 'registration': reg})
|
||||
|
||||
return render_template('pages/players_to_evaluate.html', tryout=tryout, players=players)
|
||||
return render_template('pages/players_to_evaluate.html', tryout=tryout, players=players)
|
||||
|
||||
+103
-43
@@ -8,9 +8,18 @@ from flask_login import login_required, current_user
|
||||
from flask_babel import gettext as _
|
||||
from app.extensions import db
|
||||
from app.models import (
|
||||
Admin, Manager, Coach, Player, Scout,
|
||||
User, Tryout, Evaluation, TryoutRegistration, TeamMember,
|
||||
Match, MatchParticipant,
|
||||
Admin,
|
||||
Manager,
|
||||
Coach,
|
||||
Player,
|
||||
Scout,
|
||||
User,
|
||||
Tryout,
|
||||
Evaluation,
|
||||
TryoutRegistration,
|
||||
TeamMember,
|
||||
Match,
|
||||
MatchParticipant,
|
||||
)
|
||||
from app.permissions import coach_tryout_ids
|
||||
from sqlalchemy import func
|
||||
@@ -46,8 +55,9 @@ def set_language(locale):
|
||||
target = request.referrer
|
||||
if target and is_safe_url(target):
|
||||
return redirect(target)
|
||||
return redirect(url_for('main.dashboard') if current_user.is_authenticated
|
||||
else url_for('auth.login'))
|
||||
return redirect(
|
||||
url_for('main.dashboard') if current_user.is_authenticated else url_for('auth.login')
|
||||
)
|
||||
|
||||
|
||||
@main_bp.route('/dashboard')
|
||||
@@ -70,47 +80,82 @@ def dashboard():
|
||||
stats['recent_users'] = User.query.order_by(User.created_at.desc()).limit(10).all()
|
||||
stats['recent_tryouts'] = Tryout.query.order_by(Tryout.created_at.desc()).limit(10).all()
|
||||
today = date.today()
|
||||
stats['upcoming_matches'] = Match.query.filter(
|
||||
Match.status == 'scheduled', Match.date >= today,
|
||||
).order_by(Match.date, Match.start_time).limit(5).all()
|
||||
stats['upcoming_matches'] = (
|
||||
Match.query.filter(
|
||||
Match.status == 'scheduled',
|
||||
Match.date >= today,
|
||||
)
|
||||
.order_by(Match.date, Match.start_time)
|
||||
.limit(5)
|
||||
.all()
|
||||
)
|
||||
|
||||
elif isinstance(user, Manager):
|
||||
stats['total_tryouts'] = Tryout.query.filter_by(created_by=user.id).count()
|
||||
stats['active_tryouts'] = Tryout.query.filter_by(
|
||||
created_by=user.id, status='in_progress').count()
|
||||
created_by=user.id, status='in_progress'
|
||||
).count()
|
||||
stats['total_evaluations'] = Evaluation.query.filter_by(evaluator_id=user.id).count()
|
||||
stats['my_tryouts'] = Tryout.query.filter_by(
|
||||
created_by=user.id).order_by(Tryout.date.desc()).limit(5).all()
|
||||
stats['my_tryouts'] = (
|
||||
Tryout.query.filter_by(created_by=user.id).order_by(Tryout.date.desc()).limit(5).all()
|
||||
)
|
||||
today = date.today()
|
||||
manager_tryout_ids = [t.id for t in Tryout.query.filter_by(created_by=user.id).all()]
|
||||
stats['upcoming_matches'] = Match.query.filter(
|
||||
Match.tryout_id.in_(manager_tryout_ids),
|
||||
Match.status == 'scheduled', Match.date >= today,
|
||||
).order_by(Match.date, Match.start_time).limit(5).all() if manager_tryout_ids else []
|
||||
stats['upcoming_matches'] = (
|
||||
Match.query.filter(
|
||||
Match.tryout_id.in_(manager_tryout_ids),
|
||||
Match.status == 'scheduled',
|
||||
Match.date >= today,
|
||||
)
|
||||
.order_by(Match.date, Match.start_time)
|
||||
.limit(5)
|
||||
.all()
|
||||
if manager_tryout_ids
|
||||
else []
|
||||
)
|
||||
|
||||
elif isinstance(user, Coach):
|
||||
stats['my_evaluations'] = Evaluation.query.filter_by(evaluator_id=user.id).count()
|
||||
registrations = TryoutRegistration.query.filter(
|
||||
TryoutRegistration.status.in_(['registered', 'attended'])).all()
|
||||
TryoutRegistration.status.in_(['registered', 'attended'])
|
||||
).all()
|
||||
registered_player_ids = [r.player_id for r in registrations]
|
||||
evaluated_player_ids = [e.player_id for e in Evaluation.query.filter_by(evaluator_id=user.id).all()]
|
||||
evaluated_player_ids = [
|
||||
e.player_id for e in Evaluation.query.filter_by(evaluator_id=user.id).all()
|
||||
]
|
||||
stats['pending_evaluations'] = len(set(registered_player_ids) - set(evaluated_player_ids))
|
||||
stats['my_recent_evaluations'] = Evaluation.query.filter_by(
|
||||
evaluator_id=user.id).order_by(Evaluation.created_at.desc()).limit(10).all()
|
||||
stats['my_recent_evaluations'] = (
|
||||
Evaluation.query.filter_by(evaluator_id=user.id)
|
||||
.order_by(Evaluation.created_at.desc())
|
||||
.limit(10)
|
||||
.all()
|
||||
)
|
||||
today = date.today()
|
||||
# Was: the first team matching the legacy coach_id column, and only
|
||||
# the tryouts targeting it. A coach attached by the many-to-many
|
||||
# relationship, or coaching a second team, saw no upcoming match.
|
||||
tryout_ids = coach_tryout_ids(user)
|
||||
stats['upcoming_matches'] = Match.query.filter(
|
||||
Match.tryout_id.in_(tryout_ids),
|
||||
Match.status == 'scheduled', Match.date >= today,
|
||||
).order_by(Match.date, Match.start_time).limit(5).all() if tryout_ids else []
|
||||
stats['upcoming_matches'] = (
|
||||
Match.query.filter(
|
||||
Match.tryout_id.in_(tryout_ids),
|
||||
Match.status == 'scheduled',
|
||||
Match.date >= today,
|
||||
)
|
||||
.order_by(Match.date, Match.start_time)
|
||||
.limit(5)
|
||||
.all()
|
||||
if tryout_ids
|
||||
else []
|
||||
)
|
||||
|
||||
elif isinstance(user, Player):
|
||||
stats['my_tryouts'] = TryoutRegistration.query.filter_by(player_id=user.id).count()
|
||||
stats['my_registrations'] = TryoutRegistration.query.filter_by(
|
||||
player_id=user.id).order_by(TryoutRegistration.registered_at.desc()).limit(5).all()
|
||||
stats['my_registrations'] = (
|
||||
TryoutRegistration.query.filter_by(player_id=user.id)
|
||||
.order_by(TryoutRegistration.registered_at.desc())
|
||||
.limit(5)
|
||||
.all()
|
||||
)
|
||||
|
||||
today = date.today()
|
||||
next_matches = []
|
||||
@@ -121,10 +166,15 @@ def dashboard():
|
||||
player_team_memberships = TeamMember.query.filter_by(player_id=user.id).all()
|
||||
player_team_ids = [tm.team_id for tm in player_team_memberships]
|
||||
|
||||
upcoming_matches = Match.query.filter(
|
||||
Match.tryout_id.in_(registered_tryout_ids),
|
||||
Match.status == 'scheduled', Match.date >= today,
|
||||
).order_by(Match.date, Match.start_time).all()
|
||||
upcoming_matches = (
|
||||
Match.query.filter(
|
||||
Match.tryout_id.in_(registered_tryout_ids),
|
||||
Match.status == 'scheduled',
|
||||
Match.date >= today,
|
||||
)
|
||||
.order_by(Match.date, Match.start_time)
|
||||
.all()
|
||||
)
|
||||
|
||||
for match in upcoming_matches:
|
||||
is_participant = False
|
||||
@@ -132,36 +182,46 @@ def dashboard():
|
||||
if match.match_type == 'team_vs_team':
|
||||
if match.team1_id in player_team_ids:
|
||||
is_participant = True
|
||||
team = next((tm for tm in player_team_memberships
|
||||
if tm.team_id == match.team1_id), None)
|
||||
team = next(
|
||||
(tm for tm in player_team_memberships if tm.team_id == match.team1_id), None
|
||||
)
|
||||
elif match.team2_id in player_team_ids:
|
||||
is_participant = True
|
||||
team = next((tm for tm in player_team_memberships
|
||||
if tm.team_id == match.team2_id), None)
|
||||
team = next(
|
||||
(tm for tm in player_team_memberships if tm.team_id == match.team2_id), None
|
||||
)
|
||||
else:
|
||||
if match.id in player_match_ids:
|
||||
is_participant = True
|
||||
|
||||
if is_participant:
|
||||
next_matches.append({
|
||||
'tryout': match.tryout, 'match': match,
|
||||
'team': team.team if team else None,
|
||||
})
|
||||
next_matches.append(
|
||||
{
|
||||
'tryout': match.tryout,
|
||||
'match': match,
|
||||
'team': team.team if team else None,
|
||||
}
|
||||
)
|
||||
|
||||
stats['next_matches'] = next_matches
|
||||
|
||||
elif isinstance(user, Scout):
|
||||
stats['total_players'] = User.query.filter_by(role='player').count()
|
||||
stats['total_evaluations'] = Evaluation.query.count()
|
||||
stats['avg_scores'] = db.session.query(
|
||||
Evaluation.player_id,
|
||||
func.avg(Evaluation.overall_score).label('avg_score'),
|
||||
).group_by(Evaluation.player_id).order_by(
|
||||
func.avg(Evaluation.overall_score).desc()).limit(5).all()
|
||||
stats['avg_scores'] = (
|
||||
db.session.query(
|
||||
Evaluation.player_id,
|
||||
func.avg(Evaluation.overall_score).label('avg_score'),
|
||||
)
|
||||
.group_by(Evaluation.player_id)
|
||||
.order_by(func.avg(Evaluation.overall_score).desc())
|
||||
.limit(5)
|
||||
.all()
|
||||
)
|
||||
stats['top_players'] = []
|
||||
for row in stats['avg_scores']:
|
||||
p = User.query.get(row.player_id)
|
||||
if p:
|
||||
stats['top_players'].append((p, round(row.avg_score, 1)))
|
||||
|
||||
return render_template('pages/dashboard.html', user=user, stats=stats)
|
||||
return render_template('pages/dashboard.html', user=user, stats=stats)
|
||||
|
||||
+243
-114
@@ -8,10 +8,21 @@ from flask_login import login_required, current_user
|
||||
from flask_babel import gettext as _
|
||||
from app.extensions import db
|
||||
from app.models import (
|
||||
Admin, Manager, Coach, Player, Scout,
|
||||
User, Tryout, Match, MatchParticipant, Team, TeamMember,
|
||||
TryoutRegistration, PlayerDisponibility,
|
||||
OneOnOneRequest, PersonalNote,
|
||||
Admin,
|
||||
Manager,
|
||||
Coach,
|
||||
Player,
|
||||
Scout,
|
||||
User,
|
||||
Tryout,
|
||||
Match,
|
||||
MatchParticipant,
|
||||
Team,
|
||||
TeamMember,
|
||||
TryoutRegistration,
|
||||
PlayerDisponibility,
|
||||
OneOnOneRequest,
|
||||
PersonalNote,
|
||||
)
|
||||
from datetime import datetime, timedelta
|
||||
from app.discord_bot import send_schedule_notification
|
||||
@@ -73,56 +84,65 @@ def api_events():
|
||||
end_time_str = match.end_time.strftime('%H:%M') if match.end_time else None
|
||||
|
||||
user_participant = MatchParticipant.query.filter_by(
|
||||
match_id=match.id, player_id=current_user.id,
|
||||
match_id=match.id,
|
||||
player_id=current_user.id,
|
||||
).first()
|
||||
|
||||
events.append({
|
||||
'id': f'match_{match.id}',
|
||||
'title': match.title,
|
||||
'date': match.date.strftime('%Y-%m-%d'),
|
||||
'type': 'match', 'color': match_color,
|
||||
'extendedProps': {
|
||||
'location': match.location or tryout.location or 'TBD',
|
||||
'status': match.status, 'description': match.description or '',
|
||||
'match_type': match.match_type,
|
||||
'tryout_id': tryout.id, 'match_id': match.id,
|
||||
'start_time': start_time_str, 'end_time': end_time_str,
|
||||
'participants': participants_str,
|
||||
'user_participant_id': user_participant.id if user_participant else None,
|
||||
'user_attendance_confirmed': user_participant.attendance_confirmed if user_participant else False,
|
||||
},
|
||||
})
|
||||
events.append(
|
||||
{
|
||||
'id': f'match_{match.id}',
|
||||
'title': match.title,
|
||||
'date': match.date.strftime('%Y-%m-%d'),
|
||||
'type': 'match',
|
||||
'color': match_color,
|
||||
'extendedProps': {
|
||||
'location': match.location or tryout.location or 'TBD',
|
||||
'status': match.status,
|
||||
'description': match.description or '',
|
||||
'match_type': match.match_type,
|
||||
'tryout_id': tryout.id,
|
||||
'match_id': match.id,
|
||||
'start_time': start_time_str,
|
||||
'end_time': end_time_str,
|
||||
'participants': participants_str,
|
||||
'user_participant_id': user_participant.id if user_participant else None,
|
||||
'user_attendance_confirmed': user_participant.attendance_confirmed
|
||||
if user_participant
|
||||
else False,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
# Add approved One on One sessions for the current user (player or coach)
|
||||
if isinstance(current_user, Player):
|
||||
one_on_ones = OneOnOneRequest.query.filter_by(
|
||||
player_id=current_user.id,
|
||||
status='approved'
|
||||
player_id=current_user.id, status='approved'
|
||||
).all()
|
||||
elif isinstance(current_user, Coach):
|
||||
one_on_ones = OneOnOneRequest.query.filter_by(
|
||||
coach_id=current_user.id,
|
||||
status='approved'
|
||||
coach_id=current_user.id, status='approved'
|
||||
).all()
|
||||
else:
|
||||
one_on_ones = []
|
||||
|
||||
for ooo in one_on_ones:
|
||||
events.append({
|
||||
'id': f'one_on_one_{ooo.id}',
|
||||
'title': f'1:1 - {ooo.player.full_name} & {ooo.coach.full_name}',
|
||||
'date': ooo.date.strftime('%Y-%m-%d'),
|
||||
'type': 'one_on_one',
|
||||
'color': '#8b5cf6',
|
||||
'extendedProps': {
|
||||
'location': 'Discord / Voice Chat',
|
||||
'status': 'approved',
|
||||
'description': ooo.points or 'One on One session',
|
||||
'start_time': ooo.start_time.strftime('%H:%M') if ooo.start_time else None,
|
||||
'end_time': ooo.end_time.strftime('%H:%M') if ooo.end_time else None,
|
||||
'participants': f"{ooo.player.full_name} with {ooo.coach.full_name}",
|
||||
},
|
||||
})
|
||||
events.append(
|
||||
{
|
||||
'id': f'one_on_one_{ooo.id}',
|
||||
'title': f'1:1 - {ooo.player.full_name} & {ooo.coach.full_name}',
|
||||
'date': ooo.date.strftime('%Y-%m-%d'),
|
||||
'type': 'one_on_one',
|
||||
'color': '#8b5cf6',
|
||||
'extendedProps': {
|
||||
'location': 'Discord / Voice Chat',
|
||||
'status': 'approved',
|
||||
'description': ooo.points or 'One on One session',
|
||||
'start_time': ooo.start_time.strftime('%H:%M') if ooo.start_time else None,
|
||||
'end_time': ooo.end_time.strftime('%H:%M') if ooo.end_time else None,
|
||||
'participants': f"{ooo.player.full_name} with {ooo.coach.full_name}",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
return jsonify(events)
|
||||
|
||||
@@ -137,13 +157,21 @@ def api_events_for_tryout(tryout_id):
|
||||
is_registered = False
|
||||
player_in_match = False
|
||||
if isinstance(current_user, Player):
|
||||
is_registered = TryoutRegistration.query.filter_by(
|
||||
tryout_id=tryout_id, player_id=current_user.id,
|
||||
).first() is not None
|
||||
player_matches = Match.query.join(MatchParticipant).filter(
|
||||
MatchParticipant.player_id == current_user.id,
|
||||
Match.tryout_id == tryout_id,
|
||||
).all()
|
||||
is_registered = (
|
||||
TryoutRegistration.query.filter_by(
|
||||
tryout_id=tryout_id,
|
||||
player_id=current_user.id,
|
||||
).first()
|
||||
is not None
|
||||
)
|
||||
player_matches = (
|
||||
Match.query.join(MatchParticipant)
|
||||
.filter(
|
||||
MatchParticipant.player_id == current_user.id,
|
||||
Match.tryout_id == tryout_id,
|
||||
)
|
||||
.all()
|
||||
)
|
||||
player_in_match = len(player_matches) > 0
|
||||
|
||||
if not can_view and not is_registered and not player_in_match:
|
||||
@@ -152,7 +180,9 @@ def api_events_for_tryout(tryout_id):
|
||||
events = []
|
||||
|
||||
for match in tryout.matches:
|
||||
match_color = '#10b981' if match.match_type in ('team_vs_team', 'player_vs_player') else '#f59e0b'
|
||||
match_color = (
|
||||
'#10b981' if match.match_type in ('team_vs_team', 'player_vs_player') else '#f59e0b'
|
||||
)
|
||||
participants_str = ''
|
||||
if match.match_type == 'team_vs_team':
|
||||
teams = []
|
||||
@@ -162,8 +192,16 @@ def api_events_for_tryout(tryout_id):
|
||||
teams.append(match.team2.name)
|
||||
participants_str = f"{' vs '.join(teams)}"
|
||||
elif match.match_type == 'player_vs_player':
|
||||
team1_players = [p.player.username for p in match.participants.filter_by(team_side=1).all() if p.player]
|
||||
team2_players = [p.player.username for p in match.participants.filter_by(team_side=2).all() if p.player]
|
||||
team1_players = [
|
||||
p.player.username
|
||||
for p in match.participants.filter_by(team_side=1).all()
|
||||
if p.player
|
||||
]
|
||||
team2_players = [
|
||||
p.player.username
|
||||
for p in match.participants.filter_by(team_side=2).all()
|
||||
if p.player
|
||||
]
|
||||
if team1_players and team2_players:
|
||||
participants_str = f"{', '.join(team1_players)} vs {', '.join(team2_players)}"
|
||||
else:
|
||||
@@ -175,19 +213,25 @@ def api_events_for_tryout(tryout_id):
|
||||
start_time_str = match.start_time.strftime('%H:%M') if match.start_time else None
|
||||
end_time_str = match.end_time.strftime('%H:%M') if match.end_time else None
|
||||
|
||||
events.append({
|
||||
'id': f'match_{match.id}',
|
||||
'title': match.title,
|
||||
'date': match.date.strftime('%Y-%m-%d'),
|
||||
'type': 'match', 'color': match_color,
|
||||
'extendedProps': {
|
||||
'location': match.location or tryout.location or 'TBD',
|
||||
'status': match.status, 'match_type': match.match_type,
|
||||
'tryout_id': tryout.id, 'match_id': match.id,
|
||||
'participants': participants_str,
|
||||
'start_time': start_time_str, 'end_time': end_time_str,
|
||||
},
|
||||
})
|
||||
events.append(
|
||||
{
|
||||
'id': f'match_{match.id}',
|
||||
'title': match.title,
|
||||
'date': match.date.strftime('%Y-%m-%d'),
|
||||
'type': 'match',
|
||||
'color': match_color,
|
||||
'extendedProps': {
|
||||
'location': match.location or tryout.location or 'TBD',
|
||||
'status': match.status,
|
||||
'match_type': match.match_type,
|
||||
'tryout_id': tryout.id,
|
||||
'match_id': match.id,
|
||||
'participants': participants_str,
|
||||
'start_time': start_time_str,
|
||||
'end_time': end_time_str,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
return jsonify(events)
|
||||
|
||||
@@ -207,7 +251,9 @@ def create_match(tryout_id):
|
||||
|
||||
teams = Team.query.filter_by(tryout_id=tryout_id).all()
|
||||
registrations = TryoutRegistration.query.filter_by(tryout_id=tryout_id).all()
|
||||
all_players = [User.query.get(r.player_id) for r in registrations if User.query.get(r.player_id)]
|
||||
all_players = [
|
||||
User.query.get(r.player_id) for r in registrations if User.query.get(r.player_id)
|
||||
]
|
||||
all_players = sorted([p for p in all_players if p], key=lambda x: x.username)
|
||||
prefill_date = request.args.get('date', '')
|
||||
|
||||
@@ -222,15 +268,25 @@ def create_match(tryout_id):
|
||||
|
||||
if not start_time_str:
|
||||
flash(_('Start time is required. Please select a time slot.'), 'danger')
|
||||
return render_template('pages/match_form.html', tryout=tryout, teams=teams,
|
||||
all_players=all_players, prefill_date=prefill_date)
|
||||
return render_template(
|
||||
'pages/match_form.html',
|
||||
tryout=tryout,
|
||||
teams=teams,
|
||||
all_players=all_players,
|
||||
prefill_date=prefill_date,
|
||||
)
|
||||
|
||||
try:
|
||||
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() if date_str else tryout.date
|
||||
except (ValueError, TypeError):
|
||||
flash(_('Invalid date format.'), 'danger')
|
||||
return render_template('pages/match_form.html', tryout=tryout, teams=teams,
|
||||
all_players=all_players, prefill_date=prefill_date)
|
||||
return render_template(
|
||||
'pages/match_form.html',
|
||||
tryout=tryout,
|
||||
teams=teams,
|
||||
all_players=all_players,
|
||||
prefill_date=prefill_date,
|
||||
)
|
||||
|
||||
start_time = None
|
||||
end_time = None
|
||||
@@ -244,12 +300,20 @@ def create_match(tryout_id):
|
||||
end_time = end_dt.time()
|
||||
except ValueError:
|
||||
flash(_('Invalid time format.'), 'danger')
|
||||
return render_template('pages/match_form.html', tryout=tryout, teams=teams, all_players=all_players)
|
||||
return render_template(
|
||||
'pages/match_form.html', tryout=tryout, teams=teams, all_players=all_players
|
||||
)
|
||||
|
||||
match = Match(
|
||||
tryout_id=tryout_id, title=title, description=description,
|
||||
date=date_obj, start_time=start_time, end_time=end_time,
|
||||
location=location, match_type=match_type, created_by=current_user.id,
|
||||
tryout_id=tryout_id,
|
||||
title=title,
|
||||
description=description,
|
||||
date=date_obj,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
location=location,
|
||||
match_type=match_type,
|
||||
created_by=current_user.id,
|
||||
)
|
||||
db.session.add(match)
|
||||
db.session.flush()
|
||||
@@ -264,14 +328,18 @@ def create_match(tryout_id):
|
||||
match.team2_id = int(team2_id) if team2_id else None
|
||||
if match.team1_id:
|
||||
for m in TeamMember.query.filter_by(team_id=match.team1_id).all():
|
||||
participant = MatchParticipant(match_id=match.id, player_id=m.player_id, team_side=1)
|
||||
participant = MatchParticipant(
|
||||
match_id=match.id, player_id=m.player_id, team_side=1
|
||||
)
|
||||
db.session.add(participant)
|
||||
db.session.flush()
|
||||
notified_participant_ids.append(participant.id)
|
||||
notified_player_ids.append(m.player_id)
|
||||
if match.team2_id:
|
||||
for m in TeamMember.query.filter_by(team_id=match.team2_id).all():
|
||||
participant = MatchParticipant(match_id=match.id, player_id=m.player_id, team_side=2)
|
||||
participant = MatchParticipant(
|
||||
match_id=match.id, player_id=m.player_id, team_side=2
|
||||
)
|
||||
db.session.add(participant)
|
||||
db.session.flush()
|
||||
notified_participant_ids.append(participant.id)
|
||||
@@ -279,8 +347,12 @@ def create_match(tryout_id):
|
||||
elif match_type == 'player_vs_player':
|
||||
team1_player_ids = request.form.get('team1_player_ids', '')
|
||||
team2_player_ids = request.form.get('team2_player_ids', '')
|
||||
team1_ids = [int(p) for p in team1_player_ids.split(',') if p] if team1_player_ids else []
|
||||
team2_ids = [int(p) for p in team2_player_ids.split(',') if p] if team2_player_ids else []
|
||||
team1_ids = (
|
||||
[int(p) for p in team1_player_ids.split(',') if p] if team1_player_ids else []
|
||||
)
|
||||
team2_ids = (
|
||||
[int(p) for p in team2_player_ids.split(',') if p] if team2_player_ids else []
|
||||
)
|
||||
for pid in team1_ids:
|
||||
participant = MatchParticipant(match_id=match.id, player_id=pid, team_side=1)
|
||||
db.session.add(participant)
|
||||
@@ -305,20 +377,34 @@ def create_match(tryout_id):
|
||||
|
||||
# Discord notifications
|
||||
event_date_str = date_obj.strftime('%Y-%m-%d')
|
||||
event_time_str = f"{start_time.strftime('%I:%M %p')} - {end_time.strftime('%I:%M %p')}" if start_time and end_time else 'TBD'
|
||||
event_time_str = (
|
||||
f"{start_time.strftime('%I:%M %p')} - {end_time.strftime('%I:%M %p')}"
|
||||
if start_time and end_time
|
||||
else 'TBD'
|
||||
)
|
||||
for i, player_id in enumerate(notified_player_ids):
|
||||
reference_id = notified_participant_ids[i] if i < len(notified_participant_ids) else match.id
|
||||
reference_id = (
|
||||
notified_participant_ids[i] if i < len(notified_participant_ids) else match.id
|
||||
)
|
||||
send_schedule_notification(
|
||||
user_id=player_id, event_type='match', event_title=match.title,
|
||||
event_date=event_date_str, event_time=event_time_str,
|
||||
user_id=player_id,
|
||||
event_type='match',
|
||||
event_title=match.title,
|
||||
event_date=event_date_str,
|
||||
event_time=event_time_str,
|
||||
reference_id=reference_id,
|
||||
)
|
||||
|
||||
flash(_('Match scheduled successfully!'), 'success')
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
|
||||
return render_template('pages/match_form.html', tryout=tryout, teams=teams,
|
||||
all_players=all_players, prefill_date=prefill_date)
|
||||
return render_template(
|
||||
'pages/match_form.html',
|
||||
tryout=tryout,
|
||||
teams=teams,
|
||||
all_players=all_players,
|
||||
prefill_date=prefill_date,
|
||||
)
|
||||
|
||||
|
||||
@matches_bp.route('/<int:match_id>/edit', methods=['GET', 'POST'])
|
||||
@@ -357,15 +443,25 @@ def edit_match(match_id):
|
||||
match.date = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||||
except (ValueError, TypeError):
|
||||
flash(_('Invalid date format.'), 'danger')
|
||||
return render_template('pages/match_form.html', match=match, tryout=tryout,
|
||||
teams=teams, all_players=all_players,
|
||||
current_player_ids=current_player_ids)
|
||||
return render_template(
|
||||
'pages/match_form.html',
|
||||
match=match,
|
||||
tryout=tryout,
|
||||
teams=teams,
|
||||
all_players=all_players,
|
||||
current_player_ids=current_player_ids,
|
||||
)
|
||||
|
||||
if not start_time_str:
|
||||
flash(_('Start time is required.'), 'danger')
|
||||
return render_template('pages/match_form.html', match=match, tryout=tryout,
|
||||
teams=teams, all_players=all_players,
|
||||
current_player_ids=current_player_ids)
|
||||
return render_template(
|
||||
'pages/match_form.html',
|
||||
match=match,
|
||||
tryout=tryout,
|
||||
teams=teams,
|
||||
all_players=all_players,
|
||||
current_player_ids=current_player_ids,
|
||||
)
|
||||
|
||||
try:
|
||||
match.start_time = datetime.strptime(start_time_str, '%H:%M').time()
|
||||
@@ -397,23 +493,37 @@ def edit_match(match_id):
|
||||
match.team2_id = new_team2_id
|
||||
if match.team1_id:
|
||||
for m in TeamMember.query.filter_by(team_id=match.team1_id).all():
|
||||
participant = MatchParticipant(match_id=match.id, player_id=m.player_id, team_side=1)
|
||||
participant = MatchParticipant(
|
||||
match_id=match.id, player_id=m.player_id, team_side=1
|
||||
)
|
||||
db.session.add(participant)
|
||||
db.session.flush()
|
||||
notified_participant_ids.append(participant.id)
|
||||
notified_player_ids.append(m.player_id)
|
||||
if match.team2_id:
|
||||
for m in TeamMember.query.filter_by(team_id=match.team2_id).all():
|
||||
participant = MatchParticipant(match_id=match.id, player_id=m.player_id, team_side=2)
|
||||
participant = MatchParticipant(
|
||||
match_id=match.id, player_id=m.player_id, team_side=2
|
||||
)
|
||||
db.session.add(participant)
|
||||
db.session.flush()
|
||||
notified_participant_ids.append(participant.id)
|
||||
notified_player_ids.append(m.player_id)
|
||||
else:
|
||||
if match.team1_id:
|
||||
notified_player_ids.extend([m.player_id for m in TeamMember.query.filter_by(team_id=match.team1_id).all()])
|
||||
notified_player_ids.extend(
|
||||
[
|
||||
m.player_id
|
||||
for m in TeamMember.query.filter_by(team_id=match.team1_id).all()
|
||||
]
|
||||
)
|
||||
if match.team2_id:
|
||||
notified_player_ids.extend([m.player_id for m in TeamMember.query.filter_by(team_id=match.team2_id).all()])
|
||||
notified_player_ids.extend(
|
||||
[
|
||||
m.player_id
|
||||
for m in TeamMember.query.filter_by(team_id=match.team2_id).all()
|
||||
]
|
||||
)
|
||||
elif match.match_type == 'player_vs_player':
|
||||
MatchParticipant.query.filter_by(match_id=match.id).delete()
|
||||
team1_str = request.form.get('team1_player_ids', '')
|
||||
@@ -446,15 +556,22 @@ def edit_match(match_id):
|
||||
# Discord notifications
|
||||
end_time_val = match.end_time or (match.start_time if match.start_time else None)
|
||||
if match.start_time and end_time_val:
|
||||
event_time_str = f"{match.start_time.strftime('%I:%M %p')} - {end_time_val.strftime('%I:%M %p')}"
|
||||
event_time_str = (
|
||||
f"{match.start_time.strftime('%I:%M %p')} - {end_time_val.strftime('%I:%M %p')}"
|
||||
)
|
||||
else:
|
||||
event_time_str = 'TBD'
|
||||
event_date_str = match.date.strftime('%Y-%m-%d')
|
||||
for i, player_id in enumerate(notified_player_ids):
|
||||
reference_id = notified_participant_ids[i] if i < len(notified_participant_ids) else match.id
|
||||
reference_id = (
|
||||
notified_participant_ids[i] if i < len(notified_participant_ids) else match.id
|
||||
)
|
||||
send_schedule_notification(
|
||||
user_id=player_id, event_type='match', event_title=match.title,
|
||||
event_date=event_date_str, event_time=event_time_str,
|
||||
user_id=player_id,
|
||||
event_type='match',
|
||||
event_title=match.title,
|
||||
event_date=event_date_str,
|
||||
event_time=event_time_str,
|
||||
reference_id=reference_id,
|
||||
)
|
||||
|
||||
@@ -469,12 +586,17 @@ def edit_match(match_id):
|
||||
'team_side': p.team_side,
|
||||
}
|
||||
|
||||
return render_template('pages/match_form.html', match=match, tryout=tryout,
|
||||
teams=teams, all_players=all_players,
|
||||
current_player_ids=current_player_ids,
|
||||
team1_player_ids=team1_player_ids,
|
||||
team2_player_ids=team2_player_ids,
|
||||
participants_map=participants_map)
|
||||
return render_template(
|
||||
'pages/match_form.html',
|
||||
match=match,
|
||||
tryout=tryout,
|
||||
teams=teams,
|
||||
all_players=all_players,
|
||||
current_player_ids=current_player_ids,
|
||||
team1_player_ids=team1_player_ids,
|
||||
team2_player_ids=team2_player_ids,
|
||||
participants_map=participants_map,
|
||||
)
|
||||
|
||||
|
||||
@matches_bp.route('/api/manageable-tryouts')
|
||||
@@ -488,11 +610,14 @@ def api_manageable_tryouts():
|
||||
manageable = []
|
||||
for t in tryouts:
|
||||
if current_user.can_manage_this_tryout(t):
|
||||
manageable.append({
|
||||
'id': t.id, 'title': t.title,
|
||||
'date': t.date.strftime('%Y-%m-%d'),
|
||||
'end_date': t.end_date.strftime('%Y-%m-%d') if t.end_date else None,
|
||||
})
|
||||
manageable.append(
|
||||
{
|
||||
'id': t.id,
|
||||
'title': t.title,
|
||||
'date': t.date.strftime('%Y-%m-%d'),
|
||||
'end_date': t.end_date.strftime('%Y-%m-%d') if t.end_date else None,
|
||||
}
|
||||
)
|
||||
return jsonify(manageable)
|
||||
|
||||
|
||||
@@ -514,7 +639,8 @@ def delete_match(match_id):
|
||||
# Only the context link is dropped. Participants go through the
|
||||
# relationship's delete-orphan cascade.
|
||||
PersonalNote.query.filter_by(match_id=match_id).update(
|
||||
{'match_id': None}, synchronize_session=False)
|
||||
{'match_id': None}, synchronize_session=False
|
||||
)
|
||||
|
||||
db.session.delete(match)
|
||||
db.session.commit()
|
||||
@@ -536,7 +662,8 @@ def get_players_available_at_time(date_str, time_str):
|
||||
available_players = []
|
||||
for player in players:
|
||||
disponibilities = PlayerDisponibility.query.filter_by(
|
||||
player_id=player.id, day_of_week=day_of_week,
|
||||
player_id=player.id,
|
||||
day_of_week=day_of_week,
|
||||
).all()
|
||||
for disp in disponibilities:
|
||||
disp_start = disp.start_time.hour * 60 + disp.start_time.minute
|
||||
@@ -575,8 +702,10 @@ def toggle_presence(match_id, participant_id):
|
||||
|
||||
participant.attendance_confirmed = not participant.attendance_confirmed
|
||||
db.session.commit()
|
||||
return jsonify({
|
||||
'participant_id': participant.id,
|
||||
'attendance_confirmed': participant.attendance_confirmed,
|
||||
'player_name': participant.player.username if participant.player else 'Unknown',
|
||||
})
|
||||
return jsonify(
|
||||
{
|
||||
'participant_id': participant.id,
|
||||
'attendance_confirmed': participant.attendance_confirmed,
|
||||
'player_name': participant.player.username if participant.player else 'Unknown',
|
||||
}
|
||||
)
|
||||
|
||||
+95
-43
@@ -8,8 +8,14 @@ from flask_login import login_required, current_user
|
||||
from flask_babel import gettext as _
|
||||
from app.extensions import db
|
||||
from app.models import (
|
||||
Admin, Manager, Coach, Player,
|
||||
OrgTeam, TeamMatch, TeamMatchParticipant, TeamPlayer,
|
||||
Admin,
|
||||
Manager,
|
||||
Coach,
|
||||
Player,
|
||||
OrgTeam,
|
||||
TeamMatch,
|
||||
TeamMatchParticipant,
|
||||
TeamPlayer,
|
||||
)
|
||||
from app.permissions import can_manage_org_team, coach_org_teams, visible_org_teams
|
||||
from datetime import datetime, timedelta
|
||||
@@ -41,9 +47,13 @@ def list_matches():
|
||||
elif isinstance(current_user, (Coach, Player)):
|
||||
teams = visible_org_teams(current_user)
|
||||
team_ids = [t.id for t in teams]
|
||||
matches_query = TeamMatch.query.filter(
|
||||
TeamMatch.org_team_id.in_(team_ids),
|
||||
) if team_ids else TeamMatch.query.filter(TeamMatch.id == -1)
|
||||
matches_query = (
|
||||
TeamMatch.query.filter(
|
||||
TeamMatch.org_team_id.in_(team_ids),
|
||||
)
|
||||
if team_ids
|
||||
else TeamMatch.query.filter(TeamMatch.id == -1)
|
||||
)
|
||||
else:
|
||||
teams = []
|
||||
matches_query = TeamMatch.query.filter(TeamMatch.id == -1)
|
||||
@@ -58,18 +68,25 @@ def list_matches():
|
||||
confirmed, total = tm.get_confirmed_count()
|
||||
participants = []
|
||||
for p in tm.participants.all():
|
||||
participants.append({
|
||||
'id': p.id, 'player': p.player,
|
||||
'is_confirmed': p.is_confirmed,
|
||||
})
|
||||
match_data.append({
|
||||
'match': tm, 'participants': participants,
|
||||
'confirmed_count': confirmed, 'total_count': total,
|
||||
})
|
||||
participants.append(
|
||||
{
|
||||
'id': p.id,
|
||||
'player': p.player,
|
||||
'is_confirmed': p.is_confirmed,
|
||||
}
|
||||
)
|
||||
match_data.append(
|
||||
{
|
||||
'match': tm,
|
||||
'participants': participants,
|
||||
'confirmed_count': confirmed,
|
||||
'total_count': total,
|
||||
}
|
||||
)
|
||||
|
||||
return render_template('pages/team_matches.html',
|
||||
teams=teams, match_data=match_data,
|
||||
now=datetime.utcnow())
|
||||
return render_template(
|
||||
'pages/team_matches.html', teams=teams, match_data=match_data, now=datetime.utcnow()
|
||||
)
|
||||
|
||||
|
||||
@team_matches_bp.route('/<int:team_id>/create', methods=['GET', 'POST'])
|
||||
@@ -87,6 +104,7 @@ def create_match(team_id):
|
||||
default_title = 'Practice' if is_practice else f'Team Match — {team.name}'
|
||||
|
||||
if is_practice and request.method == 'GET':
|
||||
|
||||
class TryoutProxy:
|
||||
def __init__(self, team_obj):
|
||||
self.id = 0
|
||||
@@ -98,10 +116,16 @@ def create_match(team_id):
|
||||
proxy_tryout = TryoutProxy(team)
|
||||
all_players = [tp.player for tp in team_players if tp.player]
|
||||
|
||||
return render_template('pages/match_form.html',
|
||||
tryout=proxy_tryout, teams=[], all_players=all_players,
|
||||
prefill_date=prefill_date, is_practice=True,
|
||||
team_id=team_id, team=team)
|
||||
return render_template(
|
||||
'pages/match_form.html',
|
||||
tryout=proxy_tryout,
|
||||
teams=[],
|
||||
all_players=all_players,
|
||||
prefill_date=prefill_date,
|
||||
is_practice=True,
|
||||
team_id=team_id,
|
||||
team=team,
|
||||
)
|
||||
|
||||
if request.method == 'POST':
|
||||
title = request.form.get('title', default_title)
|
||||
@@ -114,16 +138,24 @@ def create_match(team_id):
|
||||
|
||||
if not date_str:
|
||||
flash(_('Date is required.'), 'danger')
|
||||
return render_template('pages/team_match_form.html', team=team,
|
||||
team_players=team_players, prefill_date=prefill_date)
|
||||
return render_template(
|
||||
'pages/team_match_form.html',
|
||||
team=team,
|
||||
team_players=team_players,
|
||||
prefill_date=prefill_date,
|
||||
)
|
||||
|
||||
try:
|
||||
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||||
except (ValueError, TypeError):
|
||||
flash(_('Invalid date format.'), 'danger')
|
||||
return render_template('pages/team_match_form.html', team=team,
|
||||
team_players=team_players, prefill_date=prefill_date,
|
||||
is_practice=is_practice)
|
||||
return render_template(
|
||||
'pages/team_match_form.html',
|
||||
team=team,
|
||||
team_players=team_players,
|
||||
prefill_date=prefill_date,
|
||||
is_practice=is_practice,
|
||||
)
|
||||
|
||||
start_time = None
|
||||
end_time = None
|
||||
@@ -138,16 +170,24 @@ def create_match(team_id):
|
||||
end_time = end_dt.time()
|
||||
except ValueError:
|
||||
flash(_('Invalid time format.'), 'danger')
|
||||
return render_template('pages/team_match_form.html', team=team,
|
||||
team_players=team_players, prefill_date=prefill_date,
|
||||
is_practice=is_practice)
|
||||
return render_template(
|
||||
'pages/team_match_form.html',
|
||||
team=team,
|
||||
team_players=team_players,
|
||||
prefill_date=prefill_date,
|
||||
is_practice=is_practice,
|
||||
)
|
||||
|
||||
team_match = TeamMatch(
|
||||
org_team_id=team_id, title=title,
|
||||
org_team_id=team_id,
|
||||
title=title,
|
||||
description=description or None,
|
||||
opponent=opponent or None,
|
||||
date=date_obj, start_time=start_time, end_time=end_time,
|
||||
location=location or None, created_by=current_user.id,
|
||||
date=date_obj,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
location=location or None,
|
||||
created_by=current_user.id,
|
||||
)
|
||||
db.session.add(team_match)
|
||||
db.session.flush()
|
||||
@@ -155,7 +195,8 @@ def create_match(team_id):
|
||||
notified_participant_ids = []
|
||||
for tp in team_players:
|
||||
participant = TeamMatchParticipant(
|
||||
team_match_id=team_match.id, player_id=tp.player_id,
|
||||
team_match_id=team_match.id,
|
||||
player_id=tp.player_id,
|
||||
)
|
||||
db.session.add(participant)
|
||||
db.session.flush()
|
||||
@@ -165,14 +206,22 @@ def create_match(team_id):
|
||||
|
||||
# Discord notifications
|
||||
event_date_str = date_obj.strftime('%Y-%m-%d')
|
||||
event_time_str = f"{start_time.strftime('%I:%M %p')} - {end_time.strftime('%I:%M %p')}" if start_time and end_time else 'TBD'
|
||||
event_time_str = (
|
||||
f"{start_time.strftime('%I:%M %p')} - {end_time.strftime('%I:%M %p')}"
|
||||
if start_time and end_time
|
||||
else 'TBD'
|
||||
)
|
||||
|
||||
for i, tp in enumerate(team_players):
|
||||
reference_id = notified_participant_ids[i] if i < len(notified_participant_ids) else team_match.id
|
||||
reference_id = (
|
||||
notified_participant_ids[i] if i < len(notified_participant_ids) else team_match.id
|
||||
)
|
||||
send_schedule_notification(
|
||||
user_id=tp.player_id, event_type='match',
|
||||
user_id=tp.player_id,
|
||||
event_type='match',
|
||||
event_title=team_match.title,
|
||||
event_date=event_date_str, event_time=event_time_str,
|
||||
event_date=event_date_str,
|
||||
event_time=event_time_str,
|
||||
reference_id=reference_id,
|
||||
)
|
||||
|
||||
@@ -229,8 +278,9 @@ def edit_match(match_id):
|
||||
flash(_('Match updated successfully!'), 'success')
|
||||
return redirect(url_for('team_matches.list_matches'))
|
||||
|
||||
return render_template('pages/team_match_form.html',
|
||||
match=team_match, team=team, team_players=[])
|
||||
return render_template(
|
||||
'pages/team_match_form.html', match=team_match, team=team, team_players=[]
|
||||
)
|
||||
|
||||
|
||||
@team_matches_bp.route('/<int:match_id>/delete', methods=['POST'])
|
||||
@@ -282,8 +332,10 @@ def toggle_presence(match_id, participant_id):
|
||||
|
||||
participant.is_confirmed = not participant.is_confirmed
|
||||
db.session.commit()
|
||||
return jsonify({
|
||||
'participant_id': participant.id,
|
||||
'is_confirmed': participant.is_confirmed,
|
||||
'player_name': participant.player.username if participant.player else 'Unknown',
|
||||
})
|
||||
return jsonify(
|
||||
{
|
||||
'participant_id': participant.id,
|
||||
'is_confirmed': participant.is_confirmed,
|
||||
'player_name': participant.player.username if participant.player else 'Unknown',
|
||||
}
|
||||
)
|
||||
|
||||
+108
-40
@@ -8,9 +8,19 @@ from flask_login import login_required, current_user
|
||||
from flask_babel import gettext as _
|
||||
from app.extensions import db
|
||||
from app.models import (
|
||||
Admin, Manager, Coach, Player,
|
||||
OrgTeam, User, PersonalNote, TeamNote, Tryout, TeamPlayer,
|
||||
TeamMatch, Contract, OneOnOneRequest,
|
||||
Admin,
|
||||
Manager,
|
||||
Coach,
|
||||
Player,
|
||||
OrgTeam,
|
||||
User,
|
||||
PersonalNote,
|
||||
TeamNote,
|
||||
Tryout,
|
||||
TeamPlayer,
|
||||
TeamMatch,
|
||||
Contract,
|
||||
OneOnOneRequest,
|
||||
)
|
||||
from app.permissions import visible_org_teams
|
||||
from datetime import datetime
|
||||
@@ -33,11 +43,21 @@ def list_teams():
|
||||
|
||||
teams = visible_org_teams(current_user)
|
||||
|
||||
coaches = User.query.filter_by(role='coach', is_active_account=True).order_by(User.username).all()
|
||||
managers = User.query.filter_by(role='manager', is_active_account=True).order_by(User.username).all()
|
||||
coaches = (
|
||||
User.query.filter_by(role='coach', is_active_account=True).order_by(User.username).all()
|
||||
)
|
||||
managers = (
|
||||
User.query.filter_by(role='manager', is_active_account=True).order_by(User.username).all()
|
||||
)
|
||||
all_players = User.query.filter_by(role='player').order_by(User.username).all()
|
||||
return render_template('pages/teams.html', teams=teams, coaches=coaches,
|
||||
managers=managers, all_players=all_players, can_manage=can_manage)
|
||||
return render_template(
|
||||
'pages/teams.html',
|
||||
teams=teams,
|
||||
coaches=coaches,
|
||||
managers=managers,
|
||||
all_players=all_players,
|
||||
can_manage=can_manage,
|
||||
)
|
||||
|
||||
|
||||
@teams_bp.route('/my-teams')
|
||||
@@ -55,29 +75,40 @@ def my_teams():
|
||||
team_data = []
|
||||
|
||||
for org_team in player_teams:
|
||||
matches = TeamMatch.query.filter(
|
||||
TeamMatch.org_team_id == org_team.id,
|
||||
TeamMatch.status == 'scheduled',
|
||||
).order_by(TeamMatch.date.asc(), TeamMatch.start_time.asc()).all()
|
||||
matches = (
|
||||
TeamMatch.query.filter(
|
||||
TeamMatch.org_team_id == org_team.id,
|
||||
TeamMatch.status == 'scheduled',
|
||||
)
|
||||
.order_by(TeamMatch.date.asc(), TeamMatch.start_time.asc())
|
||||
.all()
|
||||
)
|
||||
|
||||
matches_data = []
|
||||
for tm in matches:
|
||||
confirmed, total = tm.get_confirmed_count()
|
||||
participant = TeamMatchParticipant.query.filter_by(
|
||||
team_match_id=tm.id, player_id=current_user.id,
|
||||
team_match_id=tm.id,
|
||||
player_id=current_user.id,
|
||||
).first()
|
||||
matches_data.append({
|
||||
'match': tm,
|
||||
'participant_id': participant.id if participant else None,
|
||||
'is_confirmed': participant.is_confirmed if participant else False,
|
||||
'confirmed_count': confirmed, 'total_count': total,
|
||||
})
|
||||
matches_data.append(
|
||||
{
|
||||
'match': tm,
|
||||
'participant_id': participant.id if participant else None,
|
||||
'is_confirmed': participant.is_confirmed if participant else False,
|
||||
'confirmed_count': confirmed,
|
||||
'total_count': total,
|
||||
}
|
||||
)
|
||||
|
||||
team_data.append({
|
||||
'team': org_team, 'matches': matches_data,
|
||||
'coaches': org_team.get_coaches(),
|
||||
'managers': org_team.get_managers(),
|
||||
})
|
||||
team_data.append(
|
||||
{
|
||||
'team': org_team,
|
||||
'matches': matches_data,
|
||||
'coaches': org_team.get_coaches(),
|
||||
'managers': org_team.get_managers(),
|
||||
}
|
||||
)
|
||||
|
||||
return render_template('pages/my_teams.html', team_data=team_data, now=now)
|
||||
|
||||
@@ -215,11 +246,12 @@ def delete_team(team_id):
|
||||
|
||||
# Entities that survive it.
|
||||
Tryout.query.filter_by(target_org_team_id=team_id).update(
|
||||
{'target_org_team_id': None}, synchronize_session=False)
|
||||
Contract.query.filter_by(team_id=team_id).update(
|
||||
{'team_id': None}, synchronize_session=False)
|
||||
{'target_org_team_id': None}, synchronize_session=False
|
||||
)
|
||||
Contract.query.filter_by(team_id=team_id).update({'team_id': None}, synchronize_session=False)
|
||||
OneOnOneRequest.query.filter_by(org_team_id=team_id).update(
|
||||
{'org_team_id': None}, synchronize_session=False)
|
||||
{'org_team_id': None}, synchronize_session=False
|
||||
)
|
||||
|
||||
db.session.delete(team)
|
||||
db.session.commit()
|
||||
@@ -247,14 +279,24 @@ def add_coach(team_id):
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
if team.coaches.filter_by(id=coach.id).first():
|
||||
flash(_('%(username)s is already a coach of %(name)s.', username=coach.username, name=team.name), 'info')
|
||||
flash(
|
||||
_(
|
||||
'%(username)s is already a coach of %(name)s.',
|
||||
username=coach.username,
|
||||
name=team.name,
|
||||
),
|
||||
'info',
|
||||
)
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
team.coaches.append(coach)
|
||||
if not team.coach_id:
|
||||
team.coach_id = coach.id
|
||||
db.session.commit()
|
||||
flash(_('%(username)s added as coach of %(name)s.', username=coach.username, name=team.name), 'success')
|
||||
flash(
|
||||
_('%(username)s added as coach of %(name)s.', username=coach.username, name=team.name),
|
||||
'success',
|
||||
)
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
|
||||
@@ -278,14 +320,24 @@ def add_manager(team_id):
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
if team.managers.filter_by(id=manager.id).first():
|
||||
flash(_('%(username)s is already a manager of %(name)s.', username=manager.username, name=team.name), 'info')
|
||||
flash(
|
||||
_(
|
||||
'%(username)s is already a manager of %(name)s.',
|
||||
username=manager.username,
|
||||
name=team.name,
|
||||
),
|
||||
'info',
|
||||
)
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
team.managers.append(manager)
|
||||
if not team.manager_id:
|
||||
team.manager_id = manager.id
|
||||
db.session.commit()
|
||||
flash(_('%(username)s added as manager of %(name)s.', username=manager.username, name=team.name), 'success')
|
||||
flash(
|
||||
_('%(username)s added as manager of %(name)s.', username=manager.username, name=team.name),
|
||||
'success',
|
||||
)
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
|
||||
@@ -361,7 +413,10 @@ def add_player(team_id):
|
||||
|
||||
existing = TeamPlayer.query.filter_by(player_id=player.id, org_team_id=team.id).first()
|
||||
if existing:
|
||||
flash(_('%(username)s is already on %(name)s.', username=player.username, name=team.name), 'info')
|
||||
flash(
|
||||
_('%(username)s is already on %(name)s.', username=player.username, name=team.name),
|
||||
'info',
|
||||
)
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
tp = TeamPlayer(player_id=player.id, org_team_id=team.id, status=status)
|
||||
@@ -383,12 +438,18 @@ def remove_player(team_id, player_id):
|
||||
player = User.query.get_or_404(player_id)
|
||||
tp = TeamPlayer.query.filter_by(player_id=player_id, org_team_id=team_id).first()
|
||||
if not tp:
|
||||
flash(_('%(username)s is not on %(name)s.', username=player.username, name=team.name), 'danger')
|
||||
flash(
|
||||
_('%(username)s is not on %(name)s.', username=player.username, name=team.name),
|
||||
'danger',
|
||||
)
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
db.session.delete(tp)
|
||||
db.session.commit()
|
||||
flash(_('%(username)s removed from %(name)s.', username=player.username, name=team.name), 'success')
|
||||
flash(
|
||||
_('%(username)s removed from %(name)s.', username=player.username, name=team.name),
|
||||
'success',
|
||||
)
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
|
||||
@@ -406,10 +467,14 @@ def toggle_player_status(team_id, player_id):
|
||||
|
||||
tp.status = 'substitute' if tp.status == 'starter' else 'starter'
|
||||
db.session.commit()
|
||||
return jsonify({
|
||||
'success': True, 'player_id': player_id,
|
||||
'new_status': tp.status, 'player_name': tp.player.username,
|
||||
})
|
||||
return jsonify(
|
||||
{
|
||||
'success': True,
|
||||
'player_id': player_id,
|
||||
'new_status': tp.status,
|
||||
'player_name': tp.player.username,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@teams_bp.route('/<int:team_id>/add-team-note', methods=['POST'])
|
||||
@@ -446,7 +511,10 @@ def add_player_note(team_id, player_id):
|
||||
|
||||
tp = TeamPlayer.query.filter_by(player_id=player_id, org_team_id=team_id).first()
|
||||
if not tp:
|
||||
flash(_('%(username)s is not on %(name)s.', username=player.username, name=team.name), 'danger')
|
||||
flash(
|
||||
_('%(username)s is not on %(name)s.', username=player.username, name=team.name),
|
||||
'danger',
|
||||
)
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
content = request.form.get('content', '').strip()
|
||||
@@ -455,4 +523,4 @@ def add_player_note(team_id, player_id):
|
||||
db.session.add(note)
|
||||
db.session.commit()
|
||||
flash(_('Note added for %(username)s!', username=player.username), 'success')
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
return redirect(url_for('teams.list_teams'))
|
||||
|
||||
+230
-92
@@ -9,10 +9,23 @@ from flask_login import login_required, current_user
|
||||
from flask_babel import gettext as _
|
||||
from app.extensions import db
|
||||
from app.models import (
|
||||
Admin, Manager, Coach, Player, Scout,
|
||||
User, Tryout, TryoutRegistration, Evaluation, Team, TeamMember,
|
||||
OrgTeam, Match, MatchParticipant, PersonalNote,
|
||||
ESPORT_GAMES, GAME_POSITIONS,
|
||||
Admin,
|
||||
Manager,
|
||||
Coach,
|
||||
Player,
|
||||
Scout,
|
||||
User,
|
||||
Tryout,
|
||||
TryoutRegistration,
|
||||
Evaluation,
|
||||
Team,
|
||||
TeamMember,
|
||||
OrgTeam,
|
||||
Match,
|
||||
MatchParticipant,
|
||||
PersonalNote,
|
||||
ESPORT_GAMES,
|
||||
GAME_POSITIONS,
|
||||
)
|
||||
from datetime import datetime
|
||||
|
||||
@@ -44,8 +57,12 @@ def create_tryout():
|
||||
return redirect(url_for('tryouts.list_tryouts'))
|
||||
|
||||
org_teams = OrgTeam.query.order_by(OrgTeam.name).all()
|
||||
managers = User.query.filter_by(role='manager', is_active_account=True).order_by(User.username).all()
|
||||
coaches = User.query.filter_by(role='coach', is_active_account=True).order_by(User.username).all()
|
||||
managers = (
|
||||
User.query.filter_by(role='manager', is_active_account=True).order_by(User.username).all()
|
||||
)
|
||||
coaches = (
|
||||
User.query.filter_by(role='coach', is_active_account=True).order_by(User.username).all()
|
||||
)
|
||||
|
||||
if request.method == 'POST':
|
||||
title = request.form.get('title')
|
||||
@@ -63,8 +80,14 @@ def create_tryout():
|
||||
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||||
except (ValueError, TypeError):
|
||||
flash(_('Invalid start date format.'), 'danger')
|
||||
return render_template('pages/tryout_form.html', tryout=None, org_teams=org_teams,
|
||||
managers=managers, coaches=coaches, esport_games=ESPORT_GAMES)
|
||||
return render_template(
|
||||
'pages/tryout_form.html',
|
||||
tryout=None,
|
||||
org_teams=org_teams,
|
||||
managers=managers,
|
||||
coaches=coaches,
|
||||
esport_games=ESPORT_GAMES,
|
||||
)
|
||||
|
||||
end_date_obj = None
|
||||
if end_date_str:
|
||||
@@ -72,19 +95,35 @@ def create_tryout():
|
||||
end_date_obj = datetime.strptime(end_date_str, '%Y-%m-%d').date()
|
||||
if end_date_obj < date_obj:
|
||||
flash(_('End date cannot be before start date.'), 'danger')
|
||||
return render_template('pages/tryout_form.html', tryout=None, org_teams=org_teams,
|
||||
managers=managers, coaches=coaches, esport_games=ESPORT_GAMES)
|
||||
return render_template(
|
||||
'pages/tryout_form.html',
|
||||
tryout=None,
|
||||
org_teams=org_teams,
|
||||
managers=managers,
|
||||
coaches=coaches,
|
||||
esport_games=ESPORT_GAMES,
|
||||
)
|
||||
except (ValueError, TypeError):
|
||||
flash(_('Invalid end date format.'), 'danger')
|
||||
return render_template('pages/tryout_form.html', tryout=None, org_teams=org_teams,
|
||||
managers=managers, coaches=coaches, esport_games=ESPORT_GAMES)
|
||||
return render_template(
|
||||
'pages/tryout_form.html',
|
||||
tryout=None,
|
||||
org_teams=org_teams,
|
||||
managers=managers,
|
||||
coaches=coaches,
|
||||
esport_games=ESPORT_GAMES,
|
||||
)
|
||||
|
||||
tryout = Tryout(
|
||||
title=title, description=description, game=game, date=date_obj,
|
||||
title=title,
|
||||
description=description,
|
||||
game=game,
|
||||
date=date_obj,
|
||||
end_date=end_date_obj,
|
||||
location=location,
|
||||
max_players=int(max_players) if max_players else None,
|
||||
created_by=current_user.id, status='upcoming',
|
||||
created_by=current_user.id,
|
||||
status='upcoming',
|
||||
target_org_team_id=int(target_org_team_id) if target_org_team_id else None,
|
||||
manager_id=int(manager_id) if manager_id else None,
|
||||
)
|
||||
@@ -100,8 +139,14 @@ def create_tryout():
|
||||
flash(_('Tryout created successfully!'), 'success')
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id))
|
||||
|
||||
return render_template('pages/tryout_form.html', tryout=None, org_teams=org_teams,
|
||||
managers=managers, coaches=coaches, esport_games=ESPORT_GAMES)
|
||||
return render_template(
|
||||
'pages/tryout_form.html',
|
||||
tryout=None,
|
||||
org_teams=org_teams,
|
||||
managers=managers,
|
||||
coaches=coaches,
|
||||
esport_games=ESPORT_GAMES,
|
||||
)
|
||||
|
||||
|
||||
@tryouts_bp.route('/<int:tryout_id>/edit', methods=['GET', 'POST'])
|
||||
@@ -119,8 +164,12 @@ def edit_tryout(tryout_id):
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id))
|
||||
|
||||
org_teams = OrgTeam.query.order_by(OrgTeam.name).all()
|
||||
managers = User.query.filter_by(role='manager', is_active_account=True).order_by(User.full_name).all()
|
||||
coaches = User.query.filter_by(role='coach', is_active_account=True).order_by(User.full_name).all()
|
||||
managers = (
|
||||
User.query.filter_by(role='manager', is_active_account=True).order_by(User.full_name).all()
|
||||
)
|
||||
coaches = (
|
||||
User.query.filter_by(role='coach', is_active_account=True).order_by(User.full_name).all()
|
||||
)
|
||||
|
||||
if request.method == 'POST':
|
||||
title = request.form.get('title')
|
||||
@@ -138,8 +187,14 @@ def edit_tryout(tryout_id):
|
||||
date_obj = datetime.strptime(date_str, '%Y-%m-%d').date()
|
||||
except (ValueError, TypeError):
|
||||
flash(_('Invalid start date format.'), 'danger')
|
||||
return render_template('pages/tryout_form.html', tryout=tryout, org_teams=org_teams,
|
||||
managers=managers, coaches=coaches, esport_games=ESPORT_GAMES)
|
||||
return render_template(
|
||||
'pages/tryout_form.html',
|
||||
tryout=tryout,
|
||||
org_teams=org_teams,
|
||||
managers=managers,
|
||||
coaches=coaches,
|
||||
esport_games=ESPORT_GAMES,
|
||||
)
|
||||
|
||||
end_date_obj = None
|
||||
if end_date_str:
|
||||
@@ -147,12 +202,24 @@ def edit_tryout(tryout_id):
|
||||
end_date_obj = datetime.strptime(end_date_str, '%Y-%m-%d').date()
|
||||
if end_date_obj < date_obj:
|
||||
flash(_('End date cannot be before start date.'), 'danger')
|
||||
return render_template('pages/tryout_form.html', tryout=tryout, org_teams=org_teams,
|
||||
managers=managers, coaches=coaches, esport_games=ESPORT_GAMES)
|
||||
return render_template(
|
||||
'pages/tryout_form.html',
|
||||
tryout=tryout,
|
||||
org_teams=org_teams,
|
||||
managers=managers,
|
||||
coaches=coaches,
|
||||
esport_games=ESPORT_GAMES,
|
||||
)
|
||||
except (ValueError, TypeError):
|
||||
flash(_('Invalid end date format.'), 'danger')
|
||||
return render_template('pages/tryout_form.html', tryout=tryout, org_teams=org_teams,
|
||||
managers=managers, coaches=coaches, esport_games=ESPORT_GAMES)
|
||||
return render_template(
|
||||
'pages/tryout_form.html',
|
||||
tryout=tryout,
|
||||
org_teams=org_teams,
|
||||
managers=managers,
|
||||
coaches=coaches,
|
||||
esport_games=ESPORT_GAMES,
|
||||
)
|
||||
|
||||
tryout.title = title
|
||||
tryout.description = description
|
||||
@@ -175,8 +242,14 @@ def edit_tryout(tryout_id):
|
||||
flash(_('Tryout updated successfully!'), 'success')
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout.id))
|
||||
|
||||
return render_template('pages/tryout_form.html', tryout=tryout, org_teams=org_teams,
|
||||
managers=managers, coaches=coaches, esport_games=ESPORT_GAMES)
|
||||
return render_template(
|
||||
'pages/tryout_form.html',
|
||||
tryout=tryout,
|
||||
org_teams=org_teams,
|
||||
managers=managers,
|
||||
coaches=coaches,
|
||||
esport_games=ESPORT_GAMES,
|
||||
)
|
||||
|
||||
|
||||
@tryouts_bp.route('/<int:tryout_id>')
|
||||
@@ -193,12 +266,21 @@ def view_tryout(tryout_id):
|
||||
elif isinstance(current_user, Coach):
|
||||
can_view = current_user.can_manage_this_tryout(tryout)
|
||||
elif isinstance(current_user, Player):
|
||||
is_registered = TryoutRegistration.query.filter_by(
|
||||
tryout_id=tryout_id, player_id=current_user.id).first() is not None
|
||||
player_in_match = MatchParticipant.query.join(Match).filter(
|
||||
MatchParticipant.player_id == current_user.id,
|
||||
Match.tryout_id == tryout_id,
|
||||
).first() is not None
|
||||
is_registered = (
|
||||
TryoutRegistration.query.filter_by(
|
||||
tryout_id=tryout_id, player_id=current_user.id
|
||||
).first()
|
||||
is not None
|
||||
)
|
||||
player_in_match = (
|
||||
MatchParticipant.query.join(Match)
|
||||
.filter(
|
||||
MatchParticipant.player_id == current_user.id,
|
||||
Match.tryout_id == tryout_id,
|
||||
)
|
||||
.first()
|
||||
is not None
|
||||
)
|
||||
can_view = is_registered or player_in_match
|
||||
elif isinstance(current_user, Scout):
|
||||
can_view = True
|
||||
@@ -215,39 +297,55 @@ def view_tryout(tryout_id):
|
||||
if current_user.can_evaluate():
|
||||
for p in registered_players:
|
||||
existing = Evaluation.query.filter_by(
|
||||
tryout_id=tryout_id, player_id=p.id, evaluator_id=current_user.id,
|
||||
tryout_id=tryout_id,
|
||||
player_id=p.id,
|
||||
evaluator_id=current_user.id,
|
||||
).first()
|
||||
player_eval_status[p.id] = existing is not None
|
||||
|
||||
is_registered = TryoutRegistration.query.filter_by(
|
||||
tryout_id=tryout_id, player_id=current_user.id,
|
||||
).first() is not None
|
||||
is_registered = (
|
||||
TryoutRegistration.query.filter_by(
|
||||
tryout_id=tryout_id,
|
||||
player_id=current_user.id,
|
||||
).first()
|
||||
is not None
|
||||
)
|
||||
|
||||
teams = Team.query.filter_by(tryout_id=tryout_id).all()
|
||||
team_data = []
|
||||
for team in teams:
|
||||
members = TeamMember.query.filter_by(team_id=team.id).all()
|
||||
team_data.append({
|
||||
'team': team,
|
||||
'members': [{'player': User.query.get(m.player_id), 'position': m.position}
|
||||
for m in members],
|
||||
})
|
||||
team_data.append(
|
||||
{
|
||||
'team': team,
|
||||
'members': [
|
||||
{'player': User.query.get(m.player_id), 'position': m.position} for m in members
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
can_edit = current_user.can_manage_this_tryout(tryout)
|
||||
|
||||
can_view_calendar = can_edit
|
||||
if isinstance(current_user, Player):
|
||||
player_in_match = MatchParticipant.query.join(Match).filter(
|
||||
MatchParticipant.player_id == current_user.id,
|
||||
Match.tryout_id == tryout_id,
|
||||
).first() is not None
|
||||
player_in_match = (
|
||||
MatchParticipant.query.join(Match)
|
||||
.filter(
|
||||
MatchParticipant.player_id == current_user.id,
|
||||
Match.tryout_id == tryout_id,
|
||||
)
|
||||
.first()
|
||||
is not None
|
||||
)
|
||||
can_view_calendar = is_registered or player_in_match
|
||||
|
||||
all_players = None
|
||||
if can_edit:
|
||||
all_players = User.query.filter_by(role='player').order_by(User.username).all()
|
||||
|
||||
matches = Match.query.filter_by(tryout_id=tryout_id).order_by(Match.date, Match.start_time).all()
|
||||
matches = (
|
||||
Match.query.filter_by(tryout_id=tryout_id).order_by(Match.date, Match.start_time).all()
|
||||
)
|
||||
match_data = []
|
||||
for match in matches:
|
||||
all_participants = list(match.participants.all())
|
||||
@@ -257,47 +355,79 @@ def view_tryout(tryout_id):
|
||||
player_presence = []
|
||||
for p in all_participants:
|
||||
if p.player:
|
||||
player_presence.append({
|
||||
'participant_id': p.id, 'player_id': p.player_id,
|
||||
'player_name': p.player.username,
|
||||
'attendance_confirmed': p.attendance_confirmed,
|
||||
})
|
||||
player_presence.append(
|
||||
{
|
||||
'participant_id': p.id,
|
||||
'player_id': p.player_id,
|
||||
'player_name': p.player.username,
|
||||
'attendance_confirmed': p.attendance_confirmed,
|
||||
}
|
||||
)
|
||||
|
||||
if match.match_type == 'team_vs_team':
|
||||
participants = {
|
||||
'team1': match.team1.name if match.team1 else 'TBD',
|
||||
'team2': match.team2.name if match.team2 else 'TBD',
|
||||
'team1_players': [{'name': m.player.username, 'position': m.position}
|
||||
for m in match.team1.members.all()] if match.team1 else [],
|
||||
'team2_players': [{'name': m.player.username, 'position': m.position}
|
||||
for m in match.team2.members.all()] if match.team2 else [],
|
||||
'team1_players': [
|
||||
{'name': m.player.username, 'position': m.position}
|
||||
for m in match.team1.members.all()
|
||||
]
|
||||
if match.team1
|
||||
else [],
|
||||
'team2_players': [
|
||||
{'name': m.player.username, 'position': m.position}
|
||||
for m in match.team2.members.all()
|
||||
]
|
||||
if match.team2
|
||||
else [],
|
||||
}
|
||||
elif match.match_type == 'player_vs_player':
|
||||
team1_players = [{'name': p.player.username, 'position': p.position}
|
||||
for p in match.participants.filter_by(team_side=1).all() if p.player]
|
||||
team2_players = [{'name': p.player.username, 'position': p.position}
|
||||
for p in match.participants.filter_by(team_side=2).all() if p.player]
|
||||
team1_players = [
|
||||
{'name': p.player.username, 'position': p.position}
|
||||
for p in match.participants.filter_by(team_side=1).all()
|
||||
if p.player
|
||||
]
|
||||
team2_players = [
|
||||
{'name': p.player.username, 'position': p.position}
|
||||
for p in match.participants.filter_by(team_side=2).all()
|
||||
if p.player
|
||||
]
|
||||
participants = {
|
||||
'team1': 'Team 1', 'team2': 'Team 2',
|
||||
'team1_players': team1_players, 'team2_players': team2_players,
|
||||
'team1': 'Team 1',
|
||||
'team2': 'Team 2',
|
||||
'team1_players': team1_players,
|
||||
'team2_players': team2_players,
|
||||
}
|
||||
else:
|
||||
participants = [p.player.username for p in match.participants.all()]
|
||||
|
||||
match_data.append({
|
||||
'match': match, 'participants': participants,
|
||||
'confirmed_count': confirmed_count, 'total_count': total_count,
|
||||
'player_presence': player_presence,
|
||||
})
|
||||
match_data.append(
|
||||
{
|
||||
'match': match,
|
||||
'participants': participants,
|
||||
'confirmed_count': confirmed_count,
|
||||
'total_count': total_count,
|
||||
'player_presence': player_presence,
|
||||
}
|
||||
)
|
||||
|
||||
return render_template('pages/view_tryout.html',
|
||||
tryout=tryout, registered_players=registered_players,
|
||||
evaluations=evaluations, player_eval_status=player_eval_status,
|
||||
is_registered=is_registered, registrations=registrations,
|
||||
team_data=team_data, can_edit=can_edit,
|
||||
can_view_calendar=can_view_calendar, all_players=all_players,
|
||||
matches=matches, match_data=match_data,
|
||||
game_positions=GAME_POSITIONS, now=datetime.utcnow())
|
||||
return render_template(
|
||||
'pages/view_tryout.html',
|
||||
tryout=tryout,
|
||||
registered_players=registered_players,
|
||||
evaluations=evaluations,
|
||||
player_eval_status=player_eval_status,
|
||||
is_registered=is_registered,
|
||||
registrations=registrations,
|
||||
team_data=team_data,
|
||||
can_edit=can_edit,
|
||||
can_view_calendar=can_view_calendar,
|
||||
all_players=all_players,
|
||||
matches=matches,
|
||||
match_data=match_data,
|
||||
game_positions=GAME_POSITIONS,
|
||||
now=datetime.utcnow(),
|
||||
)
|
||||
|
||||
|
||||
@tryouts_bp.route('/<int:tryout_id>/register', methods=['POST'])
|
||||
@@ -314,7 +444,8 @@ def register_for_tryout(tryout_id):
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
|
||||
existing = TryoutRegistration.query.filter_by(
|
||||
tryout_id=tryout_id, player_id=current_user.id).first()
|
||||
tryout_id=tryout_id, player_id=current_user.id
|
||||
).first()
|
||||
if existing:
|
||||
flash(_('You are already registered for this tryout.'), 'info')
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
@@ -358,7 +489,8 @@ def update_registration_status(tryout_id, player_id):
|
||||
return redirect(url_for('tryouts.list_tryouts'))
|
||||
|
||||
registration = TryoutRegistration.query.filter_by(
|
||||
tryout_id=tryout_id, player_id=player_id).first_or_404()
|
||||
tryout_id=tryout_id, player_id=player_id
|
||||
).first_or_404()
|
||||
new_status = request.form.get('status')
|
||||
if new_status in ['registered', 'attended', 'no_show']:
|
||||
registration.status = new_status
|
||||
@@ -385,10 +517,12 @@ def register_player(tryout_id):
|
||||
flash(_('Can only register players.'), 'danger')
|
||||
return redirect(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:
|
||||
flash(_('%(username)s is already registered for this tryout.', username=player.username), 'info')
|
||||
flash(
|
||||
_('%(username)s is already registered for this tryout.', username=player.username),
|
||||
'info',
|
||||
)
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
|
||||
if tryout.max_players:
|
||||
@@ -416,7 +550,8 @@ def remove_player(tryout_id, player_id):
|
||||
player = User.query.get_or_404(player_id)
|
||||
|
||||
registration = TryoutRegistration.query.filter_by(
|
||||
tryout_id=tryout_id, player_id=player_id).first()
|
||||
tryout_id=tryout_id, player_id=player_id
|
||||
).first()
|
||||
if registration:
|
||||
db.session.delete(registration)
|
||||
|
||||
@@ -479,8 +614,10 @@ def add_to_team(tryout_id, team_id):
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
|
||||
# Only players registered for this tryout may be placed on its teams.
|
||||
is_registered = TryoutRegistration.query.filter_by(
|
||||
tryout_id=tryout_id, player_id=player_id).first() is not None
|
||||
is_registered = (
|
||||
TryoutRegistration.query.filter_by(tryout_id=tryout_id, player_id=player_id).first()
|
||||
is not None
|
||||
)
|
||||
if not is_registered:
|
||||
flash(_('That player is not registered for this tryout.'), 'danger')
|
||||
return redirect(url_for('tryouts.view_tryout', tryout_id=tryout_id))
|
||||
@@ -513,24 +650,25 @@ def delete_tryout(tryout_id):
|
||||
# about a player, not tryout data. Only their context links are cleared.
|
||||
# Missing this step made the deletion fail on the foreign keys below.
|
||||
PersonalNote.query.filter_by(tryout_id=tryout_id).update(
|
||||
{'tryout_id': None}, synchronize_session=False)
|
||||
{'tryout_id': None}, synchronize_session=False
|
||||
)
|
||||
if match_ids:
|
||||
PersonalNote.query.filter(PersonalNote.match_id.in_(match_ids)).update(
|
||||
{'match_id': None}, synchronize_session=False)
|
||||
{'match_id': None}, synchronize_session=False
|
||||
)
|
||||
if team_ids:
|
||||
PersonalNote.query.filter(PersonalNote.team_id.in_(team_ids)).update(
|
||||
{'team_id': None}, synchronize_session=False)
|
||||
{'team_id': None}, synchronize_session=False
|
||||
)
|
||||
|
||||
if match_ids:
|
||||
MatchParticipant.query.filter(
|
||||
MatchParticipant.match_id.in_(match_ids)
|
||||
).delete(synchronize_session=False)
|
||||
MatchParticipant.query.filter(MatchParticipant.match_id.in_(match_ids)).delete(
|
||||
synchronize_session=False
|
||||
)
|
||||
Match.query.filter(Match.id.in_(match_ids)).delete(synchronize_session=False)
|
||||
|
||||
if team_ids:
|
||||
TeamMember.query.filter(
|
||||
TeamMember.team_id.in_(team_ids)
|
||||
).delete(synchronize_session=False)
|
||||
TeamMember.query.filter(TeamMember.team_id.in_(team_ids)).delete(synchronize_session=False)
|
||||
Team.query.filter(Team.id.in_(team_ids)).delete(synchronize_session=False)
|
||||
|
||||
TryoutRegistration.query.filter_by(tryout_id=tryout_id).delete()
|
||||
@@ -539,4 +677,4 @@ def delete_tryout(tryout_id):
|
||||
db.session.delete(tryout)
|
||||
db.session.commit()
|
||||
flash(_('Tryout deleted successfully.'), 'success')
|
||||
return redirect(url_for('tryouts.list_tryouts'))
|
||||
return redirect(url_for('tryouts.list_tryouts'))
|
||||
|
||||
+478
-207
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user