Active la regle isort (I) de ruff. 45 fichiers reordonnes, aucun changement de comportement : la suite passe avant comme apres. app/models/__init__.py en est exclu. Ses imports sont ranges en onze couches commentees qui decrivent le graphe de dependances ; trier par ordre alphabetique laisse chaque titre au-dessus d un import qu il ne decrit pas, et ce fichier n a qu un role, etre lu. Commit isole, comme le formatage : un diff de brassage ne doit pas servir de couverture a un changement de comportement.
243 lines
8.6 KiB
Python
243 lines
8.6 KiB
Python
"""Main dashboard routes for the Team Tryouts application.
|
|
|
|
Uses polymorphic isinstance checks instead of role-string comparisons.
|
|
"""
|
|
|
|
from datetime import date
|
|
|
|
from flask import Blueprint, flash, redirect, render_template, request, url_for
|
|
from flask_babel import gettext as _
|
|
from flask_login import current_user, login_required
|
|
from sqlalchemy import func
|
|
|
|
from app.extensions import db
|
|
from app.models import (
|
|
Admin,
|
|
Coach,
|
|
Evaluation,
|
|
Manager,
|
|
Match,
|
|
MatchParticipant,
|
|
Player,
|
|
Scout,
|
|
TeamMember,
|
|
Tryout,
|
|
TryoutRegistration,
|
|
User,
|
|
)
|
|
from app.permissions import coach_tryout_ids
|
|
|
|
main_bp = Blueprint('main', __name__)
|
|
|
|
|
|
@main_bp.route('/')
|
|
def index():
|
|
"""Redirect root URL to login page."""
|
|
return redirect(url_for('auth.login'))
|
|
|
|
|
|
@main_bp.route('/lang/<locale>')
|
|
def set_language(locale):
|
|
"""Switch the interface language and return where the user came from.
|
|
|
|
Available to anonymous visitors too: the login page has to be readable
|
|
before anyone can sign in.
|
|
|
|
A GET link rather than a form: the only thing a forged request could
|
|
achieve is changing the visitor's own display language, which carries
|
|
no consequence worth a token. The redirect target is still validated —
|
|
an unchecked `Referer` would make this an open redirect.
|
|
"""
|
|
from app.i18n import set_locale
|
|
from app.routes.auth import is_safe_url
|
|
|
|
if not set_locale(locale):
|
|
flash(_('That language is not available.'), 'warning')
|
|
|
|
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')
|
|
)
|
|
|
|
|
|
@main_bp.route('/dashboard')
|
|
@login_required
|
|
def dashboard():
|
|
"""Render the main dashboard with role-specific statistics.
|
|
|
|
Each User subclass provides its own stats view.
|
|
"""
|
|
user = current_user
|
|
stats = {}
|
|
|
|
if isinstance(user, Admin):
|
|
stats['total_users'] = User.query.count()
|
|
stats['total_players'] = User.query.filter_by(role='player').count()
|
|
stats['total_tryouts'] = Tryout.query.count()
|
|
stats['total_evaluations'] = Evaluation.query.count()
|
|
stats['active_tryouts'] = Tryout.query.filter_by(status='in_progress').count()
|
|
stats['completed_tryouts'] = Tryout.query.filter_by(status='completed').count()
|
|
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()
|
|
)
|
|
|
|
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()
|
|
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()
|
|
)
|
|
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 []
|
|
)
|
|
|
|
elif isinstance(user, Coach):
|
|
stats['my_evaluations'] = Evaluation.query.filter_by(evaluator_id=user.id).count()
|
|
|
|
# A count, computed as a count. This used to load every registration
|
|
# row in the club and every evaluation this coach had written, build
|
|
# two Python sets and subtract them — two full table reads to produce
|
|
# one integer (PERF-004).
|
|
already_evaluated = (
|
|
db.session.query(Evaluation.player_id)
|
|
.filter(
|
|
Evaluation.evaluator_id == user.id,
|
|
Evaluation.player_id == TryoutRegistration.player_id,
|
|
)
|
|
.exists()
|
|
)
|
|
stats['pending_evaluations'] = (
|
|
db.session.query(func.count(func.distinct(TryoutRegistration.player_id)))
|
|
.filter(
|
|
TryoutRegistration.status.in_(['registered', 'attended']),
|
|
~already_evaluated,
|
|
)
|
|
.scalar()
|
|
)
|
|
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 []
|
|
)
|
|
|
|
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()
|
|
)
|
|
|
|
today = date.today()
|
|
next_matches = []
|
|
all_registrations = TryoutRegistration.query.filter_by(player_id=user.id).all()
|
|
registered_tryout_ids = [r.tryout_id for r in all_registrations]
|
|
player_participant_matches = MatchParticipant.query.filter_by(player_id=user.id).all()
|
|
player_match_ids = [p.match_id for p in player_participant_matches]
|
|
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()
|
|
)
|
|
|
|
for match in upcoming_matches:
|
|
is_participant = False
|
|
team = None
|
|
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
|
|
)
|
|
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
|
|
)
|
|
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,
|
|
}
|
|
)
|
|
|
|
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['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)
|