feat: Team-Statistik-Dashboard für Headcoaches
Fügt eine neue Seite hinzu, auf der Headcoaches Statistiken für ihre Teams einsehen können. Die Statistikseite umfasst: - W-L-Bilanz, Siegquote (PCT) und aktuelle Serie - Balkendiagramm für erzielte und zugelassene Runs (RS vs. RA) - "Luck-O-Meter" zum Vergleich der realen und pythagoreischen Siegquote - Inning-Heatmap zur Anzeige der erzielten Runs pro Inning Die Seite ist über einen neuen Button auf dem Dashboard für jedes vom Headcoach trainierte Team erreichbar.
This commit is contained in:
parent
aba0533b82
commit
56e7393524
21
.gitignore
vendored
21
.gitignore
vendored
@ -1,17 +1,6 @@
|
|||||||
*.pyc
|
*.sqlite3
|
||||||
# Generische Python-Ignorierungen
|
|
||||||
|
|
||||||
# Kompilierte Python-Dateien
|
|
||||||
*.pyc
|
|
||||||
__pycache__/
|
|
||||||
|
|
||||||
# Virtuelle Umgebungen (häufige Namen)
|
|
||||||
venv/
|
venv/
|
||||||
env/
|
__pycache__/
|
||||||
.venv/
|
*.pyc
|
||||||
|
.DS_Store
|
||||||
# Abhängigkeitsdateien (z.B. bei Setuptools/pip)
|
db.sqlite3
|
||||||
*.egg-info/
|
|
||||||
.eggs/
|
|
||||||
dist/
|
|
||||||
build/
|
|
||||||
@ -41,6 +41,7 @@ INSTALLED_APPS = [
|
|||||||
'clubs',
|
'clubs',
|
||||||
'calendars',
|
'calendars',
|
||||||
'dashboard',
|
'dashboard',
|
||||||
|
'team_stats',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
|||||||
@ -23,5 +23,6 @@ urlpatterns = [
|
|||||||
path('accounts/', include('accounts.urls')),
|
path('accounts/', include('accounts.urls')),
|
||||||
path('clubs/', include('clubs.urls')),
|
path('clubs/', include('clubs.urls')),
|
||||||
path('calendars/', include('calendars.urls')),
|
path('calendars/', include('calendars.urls')),
|
||||||
|
path('statistics/', include('team_stats.urls')),
|
||||||
path('', include('dashboard.urls')),
|
path('', include('dashboard.urls')),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -6,6 +6,11 @@
|
|||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
<h2>Dashboard</h2>
|
<h2>Dashboard</h2>
|
||||||
<div>
|
<div>
|
||||||
|
{% if user.coached_teams.all %}
|
||||||
|
{% for team in user.coached_teams.all %}
|
||||||
|
<a href="{% url 'team_stats:team_statistics' team.id %}" class="btn btn-secondary">Statistics for {{ team.name }}</a>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
{% if user.coached_teams.all or user.assisted_teams.all %}
|
{% if user.coached_teams.all or user.assisted_teams.all %}
|
||||||
<a href="{% url 'select-event-type' %}" class="btn btn-primary">Create New Event</a>
|
<a href="{% url 'select-event-type' %}" class="btn btn-primary">Create New Event</a>
|
||||||
<a href="{% url 'player_list' %}" class="btn btn-info">Player List</a>
|
<a href="{% url 'player_list' %}" class="btn btn-info">Player List</a>
|
||||||
|
|||||||
4
docs/statiks.md3
Normal file
4
docs/statiks.md3
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
1. Headline: W-L Record, PCT, und Streak (z.B. "Won 3").
|
||||||
|
2. Offense vs Defense: Balkendiagramm RS vs RA.
|
||||||
|
3. Luck-O-Meter: Vergleich von Real Winning % vs. Pythagorean Winning %. (Fans diskutieren lieben Diskussionen darüber, ob ihr Team "gut" oder nur "glücklich" ist).
|
||||||
|
4. Inning-Heatmap: Eine Tabelle von Inning 1 bis 9, die zeigt, in welchem Inning das Team die meisten Runs erzielt (z.B. "Wir sind ein 'Late Inning' Team").
|
||||||
0
team_stats/__init__.py
Normal file
0
team_stats/__init__.py
Normal file
3
team_stats/admin.py
Normal file
3
team_stats/admin.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
6
team_stats/apps.py
Normal file
6
team_stats/apps.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class TeamStatsConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'team_stats'
|
||||||
0
team_stats/migrations/__init__.py
Normal file
0
team_stats/migrations/__init__.py
Normal file
3
team_stats/models.py
Normal file
3
team_stats/models.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Create your models here.
|
||||||
70
team_stats/templates/team_stats/team_statistics.html
Normal file
70
team_stats/templates/team_stats/team_statistics.html
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
|
</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>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
3
team_stats/tests.py
Normal file
3
team_stats/tests.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
8
team_stats/urls.py
Normal file
8
team_stats/urls.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
app_name = 'team_stats'
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('team/<int:team_id>/', views.team_statistics, name='team_statistics'),
|
||||||
|
]
|
||||||
94
team_stats/views.py
Normal file
94
team_stats/views.py
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
@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')
|
||||||
|
|
||||||
|
wins = 0
|
||||||
|
losses = 0
|
||||||
|
runs_scored = 0
|
||||||
|
runs_allowed = 0
|
||||||
|
inning_runs = {i: 0 for i in range(1, 10)}
|
||||||
|
streak_counter = 0
|
||||||
|
current_streak_type = None
|
||||||
|
last_game_result = None
|
||||||
|
|
||||||
|
for game in games:
|
||||||
|
result = game.result
|
||||||
|
|
||||||
|
home_score = sum(result.inning_results.get('home', []))
|
||||||
|
away_score = sum(result.inning_results.get('away', []))
|
||||||
|
|
||||||
|
if game.is_home_game:
|
||||||
|
team_score = home_score
|
||||||
|
opponent_score = away_score
|
||||||
|
else:
|
||||||
|
team_score = away_score
|
||||||
|
opponent_score = home_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':
|
||||||
|
streak_counter += 1
|
||||||
|
else:
|
||||||
|
streak_counter = 1
|
||||||
|
last_game_result = 'win'
|
||||||
|
elif team_score < opponent_score:
|
||||||
|
losses += 1
|
||||||
|
if last_game_result == 'loss':
|
||||||
|
streak_counter += 1
|
||||||
|
else:
|
||||||
|
streak_counter = 1
|
||||||
|
last_game_result = 'loss'
|
||||||
|
|
||||||
|
# Inning Heatmap
|
||||||
|
team_innings = result.inning_results.get('home' if game.is_home_game else 'away', [])
|
||||||
|
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':
|
||||||
|
streak_str = f"Lost {streak_counter}"
|
||||||
|
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 = {
|
||||||
|
'team': team,
|
||||||
|
'wins': wins,
|
||||||
|
'losses': losses,
|
||||||
|
'pct': pct,
|
||||||
|
'streak': streak_str,
|
||||||
|
'runs_scored': runs_scored,
|
||||||
|
'runs_allowed': runs_allowed,
|
||||||
|
'pythagorean_pct': pythagorean_pct,
|
||||||
|
'inning_runs': inning_runs,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(request, 'team_stats/team_statistics.html', context)
|
||||||
Loading…
x
Reference in New Issue
Block a user