Feat: Implementiere Team-Statistiken für Unterteams und Saisonauswahl

Erweitert die Team-Statistikansicht, sodass Head Coaches von übergeordneten Teams
auch die Statistiken ihrer untergeordneten Teams einsehen können.
Jedes Team wird mit seinen eigenen Statistiken separat dargestellt.
Zusätzlich wurde eine Filterfunktion implementiert, die es ermöglicht,
die Statistiken nach Saison zu filtern.
This commit is contained in:
Matthias Nagel 2025-11-19 08:43:44 +01:00
parent 9cfe5e50c4
commit c63ad532b5
2 changed files with 139 additions and 85 deletions

View File

@ -4,67 +4,109 @@
<div class="container mt-4">
<h1 class="mb-4">Statistics for {{ team.name }}</h1>
<!-- W-L Record, PCT, and Streak -->
<div class="row">
<div class="col-md-4">
<div class="card">
<div class="card-header">Record</div>
<div class="card-body">
<h5 class="card-title">{{ wins }} - {{ losses }}</h5>
<p class="card-text">PCT: {{ pct|floatformat:3 }}</p>
<p class="card-text">Streak: {{ streak }}</p>
<!-- Season Filter Form -->
<div class="mb-4">
<form method="get" action="">
<div class="row align-items-end">
<div class="col-md-3">
<label for="season" class="form-label">Select Season:</label>
<select name="season" id="season" class="form-select">
<option value="">All Time</option>
{% for season in available_seasons %}
<option value="{{ season }}" {% if season|stringformat:"s" == selected_season %}selected{% endif %}>
{{ season }}
</option>
{% endfor %}
</select>
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-primary">Filter</button>
</div>
</div>
</div>
<div class="col-md-8">
<div class="card">
<div class="card-header">Offense vs Defense</div>
<div class="card-body">
<p>Runs Scored: {{ runs_scored }}</p>
<p>Runs Allowed: {{ runs_allowed }}</p>
<div class="progress" style="height: 30px;">
<div class="progress-bar bg-success" role="progressbar" style="width: {% widthratio runs_scored runs_scored|add:runs_allowed 100 %}%;" aria-valuenow="{{ runs_scored }}" aria-valuemin="0" aria-valuemax="{{ runs_scored|add:runs_allowed }}">{{ runs_scored }}</div>
<div class="progress-bar bg-danger" role="progressbar" style="width: {% widthratio runs_allowed runs_scored|add:runs_allowed 100 %}%;" aria-valuenow="{{ runs_allowed }}" aria-valuemin="0" aria-valuemax="{{ runs_scored|add:runs_allowed }}">{{ runs_allowed }}</div>
</form>
</div>
{% for stats in teams_stats %}
{% if not forloop.first %}
<hr class="my-4">
{% endif %}
<h2 class="mb-3">{{ stats.team.name }}</h2>
{% if stats.total_games > 0 %}
<!-- W-L Record, PCT, and Streak -->
<div class="row">
<div class="col-md-4">
<div class="card">
<div class="card-header">Record</div>
<div class="card-body">
<h5 class="card-title">{{ stats.wins }} - {{ stats.losses }}</h5>
<p class="card-text">PCT: {{ stats.pct|floatformat:3 }}</p>
<p class="card-text">Streak: {{ stats.streak }}</p>
</div>
</div>
</div>
<div class="col-md-8">
<div class="card">
<div class="card-header">Offense vs Defense</div>
<div class="card-body">
<p>Runs Scored: {{ stats.runs_scored }}</p>
<p>Runs Allowed: {{ stats.runs_allowed }}</p>
{% with total_runs=stats.runs_scored|add:stats.runs_allowed %}
<div class="progress" style="height: 30px;">
{% if total_runs > 0 %}
<div class="progress-bar bg-success" role="progressbar" style="width: {% widthratio stats.runs_scored total_runs 100 %}%;" aria-valuenow="{{ stats.runs_scored }}" aria-valuemin="0" aria-valuemax="{{ total_runs }}">{{ stats.runs_scored }}</div>
<div class="progress-bar bg-danger" role="progressbar" style="width: {% widthratio stats.runs_allowed total_runs 100 %}%;" aria-valuenow="{{ stats.runs_allowed }}" aria-valuemin="0" aria-valuemax="{{ total_runs }}">{{ stats.runs_allowed }}</div>
{% else %}
<div class="progress-bar" role="progressbar" style="width: 100%;">0</div>
{% endif %}
</div>
{% endwith %}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Luck-O-Meter and Inning Heatmap -->
<div class="row mt-4">
<div class="col-md-4">
<div class="card">
<div class="card-header">Luck-O-Meter</div>
<div class="card-body">
<p>Real Winning %: {{ pct|floatformat:3 }}</p>
<p>Pythagorean Winning %: {{ pythagorean_pct|floatformat:3 }}</p>
<!-- Luck-O-Meter and Inning Heatmap -->
<div class="row mt-4">
<div class="col-md-4">
<div class="card">
<div class="card-header">Luck-O-Meter</div>
<div class="card-body">
<p>Real Winning %: {{ stats.pct|floatformat:3 }}</p>
<p>Pythagorean Winning %: {{ stats.pythagorean_pct|floatformat:3 }}</p>
</div>
</div>
</div>
<div class="col-md-8">
<div class="card">
<div class="card-header">Inning Heatmap (Runs Scored)</div>
<div class="card-body">
<table class="table table-bordered text-center">
<thead>
<tr>
{% for inning, runs in stats.inning_runs.items %}
<th>{{ inning }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
<tr>
{% for inning, runs in stats.inning_runs.items %}
<td>{{ runs }}</td>
{% endfor %}
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-8">
<div class="card">
<div class="card-header">Inning Heatmap (Runs Scored)</div>
<div class="card-body">
<table class="table table-bordered text-center">
<thead>
<tr>
{% for inning, runs in inning_runs.items %}
<th>{{ inning }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
<tr>
{% for inning, runs in inning_runs.items %}
<td>{{ runs }}</td>
{% endfor %}
</tr>
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info" role="alert">
No games played for this team{% if selected_season %} in the {{ selected_season }} season{% endif %}.
</div>
</div>
</div>
{% endif %}
{% endfor %}
</div>
{% endblock %}

View File

@ -2,17 +2,17 @@ from django.shortcuts import render, get_object_or_404
from django.contrib.auth.decorators import login_required
from django.http import HttpResponseForbidden
from clubs.models import Team
from calendars.models import Game, GameResult
from calendars.models import Game
@login_required
def team_statistics(request, team_id):
team = get_object_or_404(Team, pk=team_id)
# Check if the user is the head coach of the team
if request.user != team.head_coach:
return HttpResponseForbidden("You are not authorized to view this page.")
games = Game.objects.filter(team=team, result__isnull=False).order_by('start_time')
def _calculate_statistics(team, season=None):
"""
Calculates statistics for a given team and season.
"""
games_query = Game.objects.filter(team=team, result__isnull=False)
if season:
games_query = games_query.filter(season=season)
games = games_query.order_by('start_time')
wins = 0
losses = 0
@ -25,16 +25,13 @@ def team_statistics(request, team_id):
for game in games:
result = game.result
print("DEBUGGER:"+str(result))
print("INNING_RESULTS:"+str(result.inning_results))
sorted_items = sorted(result.inning_results.items(),key=lambda x: int(x[0].split('_')[1]))
sorted_items = sorted(result.inning_results.items(), key=lambda x: int(x[0].split('_')[1]))
home_innings = [
item[1].get('home')
for item in sorted_items
if isinstance(item[1].get('home'), int)
]
sorted_items = sorted(result.inning_results.items(),key=lambda x: int(x[0].split('_')[1]))
away_innings = [
item[1].get('guest')
for item in sorted_items
@ -42,25 +39,17 @@ def team_statistics(request, team_id):
]
home_score = sum(home_innings)
away_score = sum(away_innings)
print("HOMEINNING:"+str(home_innings))
if game.is_home_game:
print("HEIMSPIEL")
team_score = home_score
opponent_score = away_score
print("Teamscore"+str(team_score))
print("Gegnerscore"+str(opponent_score))
else:
print("Auswerts")
team_score = away_score
opponent_score = home_score
print("Teamscore"+str(team_score))
print("Gegnerscore"+str(opponent_score))
runs_scored += team_score
runs_allowed += opponent_score
# W-L Record and Streak
if team_score > opponent_score:
wins += 1
if last_game_result == 'win':
@ -75,19 +64,15 @@ def team_statistics(request, team_id):
else:
streak_counter = 1
last_game_result = 'loss'
print("Win:"+str(wins))
print("Looses:"+str(losses))
# Inning Heatmap
team_innings = home_innings if game.is_home_game else away_innings
for i, runs in enumerate(team_innings):
if i + 1 in inning_runs:
inning_runs[i + 1] += runs
# Winning Percentage (PCT)
total_games = wins + losses
pct = (wins / total_games) * 100 if total_games > 0 else 0
# Streak
if last_game_result == 'win':
streak_str = f"Won {streak_counter}"
elif last_game_result == 'loss':
@ -95,14 +80,12 @@ def team_statistics(request, team_id):
else:
streak_str = "N/A"
# Pythagorean Winning Percentage
if runs_scored > 0 or runs_allowed > 0:
pythagorean_pct = (runs_scored**2 / (runs_scored**2 + runs_allowed**2)) * 100
else:
pythagorean_pct = 0
context = {
return {
'team': team,
'wins': wins,
'losses': losses,
@ -112,7 +95,36 @@ def team_statistics(request, team_id):
'runs_allowed': runs_allowed,
'pythagorean_pct': pythagorean_pct,
'inning_runs': inning_runs,
'total_games': total_games,
}
@login_required
def team_statistics(request, team_id):
team = get_object_or_404(Team, pk=team_id)
# User must be the head coach of the parent team to view the stats
if request.user != team.head_coach:
return HttpResponseForbidden("You are not authorized to view this page.")
selected_season = request.GET.get('season')
# Create a list of the parent team and all its child teams
teams_to_display = [team] + list(team.child_teams.all())
# Calculate statistics for each team
teams_stats = []
for t in teams_to_display:
stats = _calculate_statistics(t, selected_season)
teams_stats.append(stats)
# Get all available seasons for the filter dropdown
available_seasons = Game.objects.values_list('season', flat=True).distinct().order_by('season')
context = {
'team': team, # Primary team for page header/auth
'teams_stats': teams_stats,
'available_seasons': available_seasons,
'selected_season': selected_season,
}
print("DebuggeR:"+str(context))
return render(request, 'team_stats/team_statistics.html', context)