changement de l'affichage des tryouts et des matchs

This commit is contained in:
cedrick2711
2026-07-22 22:19:58 -04:00
parent 346229778f
commit 5e909014f8
14 changed files with 573 additions and 326 deletions
Binary file not shown.
+2 -2
View File
@@ -88,7 +88,7 @@ def create_app():
response.headers['X-Content-Type-Options'] = 'nosniff'
response.headers['X-Frame-Options'] = 'DENY'
response.headers['X-XSS-Protection'] = '1; mode=block'
response.headers['Content-Security-Policy'] = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com; font-src 'self' https://cdnjs.cloudflare.com; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none';"
response.headers['Content-Security-Policy'] = "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net; font-src 'self' https://cdnjs.cloudflare.com; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none';"
response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
return response
@@ -133,5 +133,5 @@ def create_app():
if __name__ == '__main__':
app = create_app()
# Debug mode should only be enabled via environment variable for security
debug_mode = os.environ.get('FLASK_DEBUG', 'false').lower() == 'true'
debug_mode = os.getenv('FLASK_DEBUG', 'false').lower() == 'true'
app.run(debug=debug_mode, host='0.0.0.0', port=5000)
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -21,7 +21,7 @@ def is_safe_url(url):
"""
if not url:
return False
parsed = url_parse(url)
parsed = urlparse(url)
# Allow relative URLs (no netloc) or same-origin URLs
return not parsed.netloc or parsed.netloc == request.host
+58 -5
View File
@@ -6,8 +6,9 @@ This module handles player evaluation creation, management, and viewing.
from flask import Blueprint, render_template, redirect, url_for, flash, request
from flask_login import login_required, current_user
from extensions import db
from models import User, Tryout, Evaluation, TryoutRegistration, GAME_POSITIONS
from models import User, Tryout, Evaluation, TryoutRegistration, GAME_POSITIONS, OrgTeam
from sqlalchemy import func
from sqlalchemy.orm import aliased
evaluations_bp = Blueprint('evaluations', __name__, url_prefix='/evaluations')
@@ -41,13 +42,55 @@ def list_evaluations():
Evaluators (coach/manager): Their given evaluations.
Players: Their received evaluations.
Supports sorting by any column header via 'sort' and 'order' query parameters.
Returns:
Response: Rendered evaluations list template.
"""
user = current_user
# Get sort parameters
sort_column = request.args.get('sort', 'created_at')
sort_order = request.args.get('order', 'desc')
# Validate sort_order
if sort_order not in ('asc', 'desc'):
sort_order = 'desc'
# Map sort columns to SQLAlchemy expressions using aliased User models for relationship sorting
player_alias = aliased(User, name='eval_player')
evaluator_alias = aliased(User, name='eval_evaluator')
sort_map = {
'tryout': Tryout.title,
'player': player_alias.full_name,
'evaluator': evaluator_alias.full_name,
'mecanics_score': Evaluation.mecanics_score,
'cohesion_score': Evaluation.cohesion_score,
'communication_score': Evaluation.communication_score,
'gamesense_score': Evaluation.gamesense_score,
'versatility_score': Evaluation.versatility_score,
'discipline_score': Evaluation.discipline_score,
'analysis_score': Evaluation.analysis_score,
'sport_ethics_score': Evaluation.sport_ethics_score,
'mental_score': Evaluation.mental_score,
'overall_score': Evaluation.overall_score,
'position_recommendation': Evaluation.position_recommendation,
'created_at': Evaluation.created_at,
}
sort_expr = sort_map.get(sort_column, Evaluation.created_at)
if sort_order == 'asc':
sort_expr = sort_expr.asc()
else:
sort_expr = sort_expr.desc()
if user.role == 'president':
evaluations = Evaluation.query.order_by(Evaluation.created_at.desc()).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'),
@@ -60,13 +103,23 @@ def list_evaluations():
player_scores[p.id] = {'player': p, 'count': row.eval_count, 'avg': round(row.avg_score, 1) if row.avg_score else 0}
elif user.can_evaluate():
evaluations = Evaluation.query.filter_by(evaluator_id=user.id).order_by(Evaluation.created_at.desc()).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 = {}
else:
evaluations = Evaluation.query.filter_by(player_id=user.id).order_by(Evaluation.created_at.desc()).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.player_id == user.id) \
.order_by(sort_expr).all()
player_scores = {}
return render_template('pages/evaluations.html', evaluations=evaluations, player_scores=player_scores)
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'])
+5 -3
View File
@@ -30,7 +30,7 @@ def list_tryouts():
- President: All tryouts
- Manager: Only their created tryouts
- Coach: Tryouts targeting their org team
- Player: Upcoming and in-progress tryouts
- Player: Only tryouts they are registered for or participating in
Returns:
Response: Rendered tryouts list template.
@@ -47,7 +47,9 @@ def list_tryouts():
else:
tryouts = []
elif current_user.role == 'player':
tryouts = Tryout.query.filter(Tryout.status.in_(['upcoming', 'in_progress'])).order_by(Tryout.date.desc()).all()
# Players only see tryouts they are registered for or participating in matches
from routes.matches import get_visible_tryouts_for_user
tryouts = get_visible_tryouts_for_user()
else:
tryouts = Tryout.query.order_by(Tryout.date.desc()).all()
return render_template('pages/tryouts.html', tryouts=tryouts, now=datetime.utcnow())
@@ -254,7 +256,7 @@ def view_tryout(tryout_id):
all_players = User.query.filter_by(role='player').order_by(User.full_name).all()
# Get matches for this tryout with participant info
matches = Match.query.filter_by(tryout_id=tryout_id).order_by(Match.date).all()
matches = Match.query.filter_by(tryout_id=tryout_id).order_by(Match.date, Match.start_time).all()
match_data = []
for match in matches:
if match.match_type == 'team_vs_team':
+144
View File
@@ -1749,3 +1749,147 @@ a:hover { color: var(--primary-dark); }
transform: translateY(-50%);
margin: 0;
}
/* ===== Tryouts Grid (Card-based Layout) ===== */
.tryouts-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.tryout-card {
transition: transform 0.2s ease, box-shadow 0.2s ease;
border: 1px solid var(--border-color, #e0e0e0);
}
.tryout-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.tryout-card .card-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
.tryout-title {
font-size: 1.1rem;
font-weight: 600;
margin: 0;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tryout-info-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.info-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.85rem;
}
.info-icon {
width: 16px;
text-align: center;
color: var(--text-muted, #888);
flex-shrink: 0;
}
.info-label {
font-weight: 600;
color: var(--text-secondary, #555);
min-width: 50px;
}
.info-value {
color: var(--text-primary, #333);
flex: 1;
text-align: right;
}
.tryout-description {
font-size: 0.85rem;
color: var(--text-secondary, #555);
line-height: 1.4;
}
.tryout-description p {
margin: 0;
}
.tryout-card .card-footer {
display: flex;
gap: 8px;
padding: 12px 16px;
background: var(--bg-secondary, #f8f9fa);
border-top: 1px solid var(--border-color, #e0e0e0);
}
/* ===== Full-width Schedule Card ===== */
.tryout-schedule-card {
width: 100%;
}
/* ===== Dark mode adjustments ===== */
[data-theme="dark"] .tryout-card {
border-color: var(--border-color, #333);
}
[data-theme="dark"] .tryout-card .card-footer {
background: var(--bg-secondary, #2a2a2a);
border-top-color: var(--border-color, #333);
}
[data-theme="dark"] .info-label {
color: var(--text-secondary, #aaa);
}
/* Sortable Table Headers */
.sort-link {
display: inline-flex;
align-items: center;
gap: 4px;
color: var(--gray-500);
font-weight: 600;
text-decoration: none;
transition: var(--transition);
}
.sort-link:hover {
color: var(--gray-800);
}
.sort-link .fas {
font-size: 0.75rem;
}
.sort-inactive {
opacity: 0.3;
font-size: 0.75rem;
}
.sort-link:hover .sort-inactive {
opacity: 0.5;
}
[data-theme="dark"] .sort-link {
color: var(--text-secondary);
}
[data-theme="dark"] .sort-link:hover {
color: var(--text-primary);
}
+35 -18
View File
@@ -4,6 +4,23 @@
{% block breadcrumb %}<span class="breadcrumb">Home / Evaluations</span>{% endblock %}
{% block content %}
{% set sort_column = sort_column | default('created_at') %}
{% set sort_order = sort_order | default('desc') %}
{% macro sort_link(column, label) %}
<th>
{% set new_order = 'asc' if (sort_column == column and sort_order == 'desc') else 'desc' %}
<a href="{{ url_for('evaluations.list_evaluations', sort=column, order=new_order) }}" class="sort-link">
{{ label }}
{% if sort_column == column %}
<i class="fas fa-arrow-{{ 'up' if sort_order == 'asc' else 'down' }}"></i>
{% else %}
<i class="fas fa-arrows-alt-v sort-inactive"></i>
{% endif %}
</a>
</th>
{% endmacro %}
{% if current_user.role == 'president' and player_scores %}
<div class="stats-grid mb-4">
{% for pid, data in player_scores.items() %}
@@ -35,29 +52,29 @@
<table class="table">
<thead>
<tr>
<th>Tryout</th>
<th>Player</th>
<th>Evaluator</th>
<th>Mecanics</th>
<th>Cohesion</th>
<th>Communication</th>
<th>Gamesense</th>
<th>Versatility</th>
<th>Discipline</th>
<th>Analysis</th>
<th>Sport Ethics</th>
<th>Mental</th>
<th>Overall</th>
<th>Position</th>
<th>Date</th>
{{ sort_link('tryout', 'Tryout') }}
{{ sort_link('player', 'Player') }}
{{ sort_link('evaluator', 'Evaluator') }}
{{ sort_link('mecanics_score', 'Mecanics') }}
{{ sort_link('cohesion_score', 'Cohesion') }}
{{ sort_link('communication_score', 'Communication') }}
{{ sort_link('gamesense_score', 'Gamesense') }}
{{ sort_link('versatility_score', 'Versatility') }}
{{ sort_link('discipline_score', 'Discipline') }}
{{ sort_link('analysis_score', 'Analysis') }}
{{ sort_link('sport_ethics_score', 'Sport Ethics') }}
{{ sort_link('mental_score', 'Mental') }}
{{ sort_link('overall_score', 'Overall') }}
{{ sort_link('position_recommendation', 'Position') }}
{{ sort_link('created_at', 'Date') }}
</tr>
</thead>
<tbody>
{% for eval in evaluations %}
<tr>
<td>{{ eval.tryout.title }}</td>
<td>{{ eval.player.full_name }}</td>
<td>{{ eval.evaluator.full_name }}</td>
<td>{{ eval.tryout.title if eval.tryout else 'Deleted Tryout' }}</td>
<td>{{ eval.player.full_name if eval.player else 'Deleted Player' }}</td>
<td>{{ eval.evaluator.full_name if eval.evaluator else 'Deleted Evaluator' }}</td>
<td>{{ eval.mecanics_score or '-' }}</td>
<td>{{ eval.cohesion_score or '-' }}</td>
<td>{{ eval.communication_score or '-' }}</td>
+14 -8
View File
@@ -77,22 +77,28 @@
</span>
</div>
<div class="detail-item">
<span class="detail-label"><i class="fas fa-chart-line"></i> TRN (Tracker Network)</span>
<span class="detail-label"><i class="fas fa-chart-line"></i> Games & TRN</span>
<span class="detail-value">
{% if user.gamertags %}
{% set games_list = user.get_games_list() %}
{% if games_list %}
<div style="display: flex; flex-direction: column; gap: 8px;">
{% for gamertag in user.gamertags %}
{% for game in games_list %}
{% set gt = user.gamertags | selectattr('game', 'equalto', game) | first %}
<div>
<span class="badge badge-esport" style="margin-right: 8px;">{{ gamertag.game }}</span>
<a href="{{ gamertag.get_trn_url() }}" target="_blank" rel="noopener noreferrer" class="trn-link">
<i class="fas fa-external-link-alt"></i> {{ gamertag.gamertag }}
{% if gamertag.platform %}<small>({{ gamertag.platform }})</small>{% endif %}
{% if gt and gt.get_trn_url() %}
<a href="{{ gt.get_trn_url() }}" target="_blank" rel="noopener noreferrer" class="trn-link">
<span class="badge badge-esport" style="margin-right: 8px;">{{ game }}</span>
<i class="fas fa-external-link-alt"></i> {{ gt.gamertag }}
{% if gt.platform %}<small>({{ gt.platform }})</small>{% endif %}
</a>
{% else %}
<span class="badge badge-esport">{{ game }}</span>
{% endif %}
</div>
{% endfor %}
</div>
{% else %}
<span class="text-muted">Not connected</span>
<span class="text-muted">Not specified</span>
{% endif %}
</span>
</div>
+64 -41
View File
@@ -12,50 +12,77 @@
{% endblock %}
{% block content %}
<div class="card">
<div class="card-body">
<div class="table-container">
<table class="table">
<thead>
<tr>
<th>Title</th>
<th>Target Team</th>
<th>Date</th>
<th>Location</th>
<th>Status</th>
<th>Players</th>
<th>Evaluations</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<div class="tryouts-grid">
{% for tryout in tryouts %}
<tr>
<td class="cell-title">{{ tryout.title }}</td>
<td>{{ tryout.target_org_team.name if tryout.target_org_team else '-' }}</td>
<td>{{ tryout.date.strftime('%b %d, %Y') }}</td>
<td>{{ tryout.location or 'TBD' }}</td>
<td>
<span class="badge badge-{{ tryout.status }}">
{{ tryout.status | replace('_', ' ') | title }}
</span>
</td>
<td>{{ tryout.registrations.count() }}</td>
<td>{{ tryout.evaluations.count() }}</td>
<td>
<div class="card tryout-card">
<div class="card-header">
<h3 class="tryout-title">{{ tryout.title }}</h3>
<span class="badge badge-{{ tryout.status }}">{{ tryout.status | replace('_', ' ') | title }}</span>
</div>
<div class="card-body">
<div class="tryout-info-grid">
<div class="info-item">
<i class="fas fa-gamepad info-icon"></i>
<span class="info-label">Game</span>
<span class="info-value">{{ tryout.game }}</span>
</div>
<div class="info-item">
<i class="fas fa-calendar info-icon"></i>
<span class="info-label">Date</span>
<span class="info-value">{{ tryout.date.strftime('%b %d, %Y') }}</span>
</div>
<div class="info-item">
<i class="fas fa-map-marker-alt info-icon"></i>
<span class="info-label">Location</span>
<span class="info-value">{{ tryout.location or 'TBD' }}</span>
</div>
<div class="info-item">
<i class="fas fa-users info-icon"></i>
<span class="info-label">Players</span>
<span class="info-value">{{ tryout.registrations.count() }}</span>
</div>
{% if current_user.can_evaluate() %}
<div class="info-item">
<i class="fas fa-clipboard-check info-icon"></i>
<span class="info-label">Evaluations</span>
<span class="info-value">{{ tryout.evaluations.count() }}</span>
</div>
{% endif %}
{% if tryout.target_org_team %}
<div class="info-item">
<i class="fas fa-flag info-icon"></i>
<span class="info-label">Target Team</span>
<span class="info-value">{{ tryout.target_org_team.name }}</span>
</div>
{% endif %}
{% if tryout.max_players %}
<div class="info-item">
<i class="fas fa-user-friends info-icon"></i>
<span class="info-label">Max Players</span>
<span class="info-value">{{ tryout.max_players }}</span>
</div>
{% endif %}
</div>
{% if tryout.description %}
<div class="tryout-description mt-3">
<p>{{ tryout.description | truncate(120) }}</p>
</div>
{% endif %}
</div>
<div class="card-footer">
<a href="{{ url_for('tryouts.view_tryout', tryout_id=tryout.id) }}" class="btn btn-sm btn-outline">
<i class="fas fa-eye"></i> View
<i class="fas fa-eye"></i> View Details
</a>
{% if current_user.can_evaluate() %}
<a href="{{ url_for('evaluations.players_to_evaluate', tryout_id=tryout.id) }}" class="btn btn-sm btn-primary">
<i class="fas fa-clipboard-check"></i> Evaluate
</a>
{% endif %}
</td>
</tr>
</div>
</div>
{% else %}
<tr>
<td colspan="8" class="text-center">
<div class="card" style="grid-column: 1 / -1;">
<div class="card-body text-center">
<div class="empty-state">
<i class="fas fa-calendar-times"></i>
<h3>No tryouts found</h3>
@@ -64,12 +91,8 @@
<a href="{{ url_for('tryouts.create_tryout') }}" class="btn btn-primary">Create Tryout</a>
{% endif %}
</div>
</td>
</tr>
</div>
</div>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}
+16 -14
View File
@@ -246,9 +246,10 @@
{% endfor %}
</div>
</div>
</div>
{% if can_view_calendar %}
<div class="card">
<div class="card tryout-schedule-card mb-4">
<div class="card-header">
<h3><i class="fas fa-futbol"></i> Schedule</h3>
{% if can_edit %}
@@ -263,20 +264,9 @@
{% endif %}
</div>
<div class="card-body">
{% if matches %}
<!-- Mini Calendar -->
<div id="mini-calendar" style="min-height: 300px;"></div>
{% else %}
<!-- No matches yet - show message -->
<div class="text-center py-4">
<i class="fas fa-calendar-alt fa-3x text-muted mb-3"></i>
<p class="text-muted">No matches scheduled yet. Check back later!</p>
</div>
{% endif %}
{% if matches %}
<!-- Matches Table -->
<div class="table-container mt-3">
<div class="table-container">
<table class="table">
<thead>
<tr>
@@ -388,10 +378,22 @@
</table>
</div>
{% endif %}
{% if matches %}
<!-- Mini Calendar -->
<div id="mini-calendar" style="min-height: 300px; margin-top: 20px;"></div>
{% else %}
<!-- No matches yet - show message -->
<div class="text-center py-4">
<i class="fas fa-calendar-alt fa-3x text-muted mb-3"></i>
<p class="text-muted">No matches scheduled yet. Check back later!</p>
</div>
{% endif %}
</div>
</div>
{% endif %}
{% if current_user.can_evaluate() %}
<div class="card">
<div class="card-header">
<h3><i class="fas fa-star"></i> Evaluation Summary</h3>
@@ -443,7 +445,7 @@
</div>
</div>
</div>
</div>
{% endif %}
</div>
<script>