Compare commits

..

No commits in common. "fd47f25c36a67948a038fbd0121fa2e0e885d584" and "c63ad532b583280f8bf2cde969829de45456c73c" have entirely different histories.

41 changed files with 184 additions and 1189 deletions

View File

@ -1,5 +1,18 @@
from django import forms
from .models import CustomUser
from .models import InvitationCode, CustomUser
class InvitationCodeForm(forms.Form):
code = forms.CharField(max_length=255, label="Einladungscode")
def clean_code(self):
code = self.cleaned_data.get('code')
try:
invitation_code = InvitationCode.objects.get(code=code)
if not invitation_code.is_valid():
raise forms.ValidationError("Dieser Einladungscode ist nicht mehr gültig.")
except InvitationCode.DoesNotExist:
raise forms.ValidationError("Ungültiger Einladungscode.")
return code
class CustomUserCreationForm(forms.ModelForm):
password = forms.CharField(widget=forms.PasswordInput)
@ -24,22 +37,3 @@ class PlayerCreationForm(forms.ModelForm):
class Meta:
model = CustomUser
fields = ('username', 'first_name', 'last_name', 'email', 'birth_date', 'player_number', 'team')
class PlayerVerificationForm(forms.Form):
password = forms.CharField(widget=forms.PasswordInput, label="Passwort")
password_confirm = forms.CharField(widget=forms.PasswordInput, label="Passwort bestätigen")
def clean(self):
cleaned_data = super().clean()
if cleaned_data.get('password') != cleaned_data.get('password_confirm'):
raise forms.ValidationError("Die Passwörter stimmen nicht überein.")
return cleaned_data
class ParentVerificationForm(PlayerVerificationForm):
username = forms.CharField(max_length=150, label="Benutzername")
def clean_username(self):
username = self.cleaned_data.get('username')
if CustomUser.objects.filter(username=username).exists():
raise forms.ValidationError("Dieser Benutzername ist bereits vergeben.")
return username

View File

@ -1,31 +0,0 @@
# Generated by Django 5.2.6 on 2025-11-23 15:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0004_customuser_parents'),
]
operations = [
migrations.AddField(
model_name='customuser',
name='is_verified',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='customuser',
name='verification_code',
field=models.UUIDField(blank=True, editable=False, null=True, unique=True),
),
migrations.AlterField(
model_name='customuser',
name='is_active',
field=models.BooleanField(default=False),
),
migrations.DeleteModel(
name='InvitationCode',
),
]

View File

@ -1,4 +1,3 @@
import uuid
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.utils import timezone
@ -9,11 +8,6 @@ class CustomUser(AbstractUser):
player_number = models.IntegerField(default=999)
team = models.ForeignKey('clubs.Team', on_delete=models.SET_NULL, null=True, blank=True, related_name='players')
parents = models.ManyToManyField('self', symmetrical=False, blank=True, related_name='children')
is_verified = models.BooleanField(default=False)
verification_code = models.UUIDField(null=True, blank=True, unique=True, editable=False)
# New users are not active until they verify and set a password
is_active = models.BooleanField(default=False)
@property
def age(self):
@ -22,6 +16,23 @@ class CustomUser(AbstractUser):
today = datetime.date.today()
return today.year - self.birth_date.year - ((today.month, today.day) < (self.birth_date.month, self.birth_date.day))
class InvitationCode(models.Model):
code = models.CharField(max_length=255, unique=True)
user = models.ForeignKey(CustomUser, on_delete=models.CASCADE, null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
is_active = models.BooleanField(default=True)
def is_valid(self):
if not self.is_active:
return False
two_weeks_ago = timezone.now() - datetime.timedelta(weeks=2)
if self.created_at < two_weeks_ago:
return False
return True
def __str__(self):
return self.code
class AbsencePeriod(models.Model):
user = models.ForeignKey(CustomUser, on_delete=models.CASCADE, related_name='absence_periods')
start_date = models.DateField()

View File

@ -1,15 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Kontoaktivierung - Baseball Organisator</title>
</head>
<body>
<h2>Hallo,</h2>
<p>ein Konto wurde für Sie beim Baseball Organisator erstellt, da Sie als Elternteil für einen Spieler registriert wurden.</p>
<p>Bitte klicken Sie auf den folgenden Link, um Ihr Konto zu aktivieren. Sie werden aufgefordert, einen Benutzernamen zu wählen und ein Passwort festzulegen:</p>
<p><a href="{{ verification_url }}">Konto jetzt aktivieren</a></p>
<p>Ihr Verifizierungscode lautet: <strong>{{ verification_code }}</strong></p>
<p>Wenn Sie den Link nicht klicken können, kopieren Sie bitte die folgende URL und fügen Sie sie in die Adresszeile Ihres Browsers ein:<br>{{ verification_url }}</p>
<p>Vielen Dank,<br>Ihr Baseball Organisator Team</p>
</body>
</html>

View File

@ -1,14 +0,0 @@
Hallo,
ein Konto wurde für Sie beim Baseball Organisator erstellt, da Sie als Elternteil für einen Spieler registriert wurden.
Bitte klicken Sie auf den folgenden Link, um Ihr Konto zu aktivieren. Sie werden aufgefordert, einen Benutzernamen zu wählen und ein Passwort festzulegen:
{{ verification_url }}
Ihr Verifizierungscode lautet: {{ verification_code }}
Wenn Sie den Link nicht klicken können, kopieren Sie ihn bitte und fügen Sie ihn in die Adresszeile Ihres Browsers ein.
Vielen Dank,
Ihr Baseball Organisator Team

View File

@ -1,15 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Willkommen beim Baseball Organisator</title>
</head>
<body>
<h2>Hallo {{ user.first_name }},</h2>
<p>willkommen beim Baseball Organisator!</p>
<p>Ein Konto wurde für dich von deinem Coach erstellt. Bitte klicke auf den folgenden Link, um dein Passwort festzulegen und dein Konto zu aktivieren:</p>
<p><a href="{{ verification_url }}">Konto jetzt aktivieren</a></p>
<p>Dein Verifizierungscode lautet: <strong>{{ verification_code }}</strong></p>
<p>Wenn du den Link nicht klicken kannst, kopiere bitte die folgende URL und füge sie in die Adresszeile deines Browsers ein:<br>{{ verification_url }}</p>
<p>Vielen Dank,<br>Dein Baseball Organisator Team</p>
</body>
</html>

View File

@ -1,14 +0,0 @@
Hallo {{ user.first_name }},
willkommen beim Baseball Organisator!
Ein Konto wurde für dich von deinem Coach erstellt. Bitte klicke auf den folgenden Link, um dein Passwort festzulegen und dein Konto zu aktivieren:
{{ verification_url }}
Dein Verifizierungscode lautet: {{ verification_code }}
Wenn du den Link nicht klicken kannst, kopiere ihn bitte und füge ihn in die Adresszeile deines Browsers ein.
Vielen Dank,
Dein Baseball Organisator Team

View File

@ -1,51 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h2>Konto verifizieren</h2>
</div>
<div class="card-body">
<p>Bitte legen Sie Ihre Zugangsdaten fest.</p>
<form method="post">
{% csrf_token %}
{% if is_parent %}
<div class="mb-3">
<label for="{{ form.username.id_for_label }}" class="form-label">{{ form.username.label }}</label>
{{ form.username }}
{% for error in form.username.errors %}
<div class="alert alert-danger p-1 mt-1">{{ error }}</div>
{% endfor %}
</div>
{% endif %}
<div class="mb-3">
<label for="{{ form.password.id_for_label }}" class="form-label">{{ form.password.label }}</label>
{{ form.password }}
{% for error in form.password.errors %}
<div class="alert alert-danger p-1 mt-1">{{ error }}</div>
{% endfor %}
</div>
<div class="mb-3">
<label for="{{ form.password_confirm.id_for_label }}" class="form-label">{{ form.password_confirm.label }}</label>
{{ form.password_confirm }}
{% for error in form.password_confirm.errors %}
<div class="alert alert-danger p-1 mt-1">{{ error }}</div>
{% endfor %}
</div>
{% for error in form.non_field_errors %}
<div class="alert alert-danger">{{ error }}</div>
{% endfor %}
<button type="submit" class="btn btn-primary">Konto aktivieren</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -3,7 +3,8 @@ from . import views
from django.contrib.auth import views as auth_views
urlpatterns = [
path('verify/<uuid:verification_code>/', views.verify_account, name='verify_account'),
path('invitation/', views.invitation_code_view, name='invitation_code'),
path('register/', views.register_view, name='register'),
path('login/', views.MyLoginView.as_view(template_name='accounts/login.html'), name='login'),
path('logout/', auth_views.LogoutView.as_view(), name='logout'),
path('profile/', views.edit_profile, name='edit_profile'),

View File

@ -1,74 +0,0 @@
import os
from django.conf import settings
from django.core.mail import send_mail
from django.template.loader import render_to_string
from django.urls import reverse
import uuid
def send_verification_email(user, request, is_parent=False):
"""
Sends a verification email to a new user (player or parent).
"""
# Ensure user has a verification code
if not user.verification_code:
user.verification_code = uuid.uuid4()
user.save()
# Build the verification URL
verification_path = reverse('verify_account', kwargs={'verification_code': str(user.verification_code)})
verification_url = request.build_absolute_uri(verification_path)
# Determine which template and subject to use
if is_parent:
subject = 'Verifizieren Sie Ihr Eltern-Konto für den Baseball Organisator'
template_prefix = 'accounts/email/parent_verification'
else:
subject = 'Willkommen beim Baseball Organisator! Bitte verifizieren Sie Ihr Konto.'
template_prefix = 'accounts/email/player_verification'
context = {
'user': user,
'verification_url': verification_url,
'verification_code': user.verification_code
}
# Render email body from templates
email_body_txt = render_to_string(f'{template_prefix}.txt', context)
email_body_html = render_to_string(f'{template_prefix}.html', context)
# Send or simulate email based on settings
if settings.MTP_EMAIL_SEND == 1:
send_mail(
subject=subject,
message=email_body_txt,
from_email=settings.DEFAULT_FROM_EMAIL, # Make sure this is set in settings.py
recipient_list=[user.email],
html_message=email_body_html,
fail_silently=False,
)
else:
# Simulate email by saving to a file
mbox_content = f"""From: {settings.DEFAULT_FROM_EMAIL}
To: {user.email}
Subject: {subject}
MIME-Version: 1.0
Content-Type: multipart/alternative; boundary="boundary"
--boundary
Content-Type: text/plain; charset="utf-8"
{email_body_txt}
--boundary
Content-Type: text/html; charset="utf-8"
{email_body_html}
--boundary--
"""
# Ensure the tmp_mails directory exists
os.makedirs('tmp_mails', exist_ok=True)
# Save the email to a file
file_path = os.path.join('tmp_mails', f'{user.email}_{user.verification_code}.mbox')
with open(file_path, 'w') as f:
f.write(mbox_content)

View File

@ -3,58 +3,128 @@ from django.urls import reverse_lazy
from django.views.generic.edit import CreateView
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.contrib.auth import views as auth_views, login
from django.contrib import messages
from django.db import Q, IntegrityError
from django.contrib.auth import views as auth_views
from django.db.models import Q
from django.http import JsonResponse
from .forms import CustomUserCreationForm, CustomUserChangeForm, PlayerCreationForm, PlayerVerificationForm, ParentVerificationForm
from .models import CustomUser
from .utils import send_verification_email
from .forms import InvitationCodeForm, CustomUserCreationForm, CustomUserChangeForm, PlayerCreationForm
from .models import CustomUser, InvitationCode
import uuid
...
def invitation_code_view(request):
if request.method == 'POST':
form = InvitationCodeForm(request.POST)
if form.is_valid():
code = form.cleaned_data['code']
request.session['invitation_code'] = code
return redirect('register')
else:
form = InvitationCodeForm()
return render(request, 'accounts/invitation_code.html', {'form': form})
def register_view(request):
invitation_code_str = request.session.get('invitation_code')
if not invitation_code_str:
return redirect('invitation_code')
try:
invitation_code = InvitationCode.objects.get(code=invitation_code_str)
if not invitation_code.is_valid():
# Handle invalid code, maybe redirect with a message
return redirect('invitation_code')
except InvitationCode.DoesNotExist:
return redirect('invitation_code')
if request.method == 'POST':
form = CustomUserCreationForm(request.POST)
if form.is_valid():
user = form.save(commit=False)
user.set_password(form.cleaned_data['password'])
user.save()
invitation_code.is_active = False
invitation_code.user = user
invitation_code.save()
# Log the user in and redirect to the dashboard
return redirect('login') # Or wherever you want to redirect after registration
else:
form = CustomUserCreationForm()
return render(request, 'accounts/register.html', {'form': form})
@login_required
def edit_profile(request):
if request.method == 'POST':
form = CustomUserChangeForm(request.POST, instance=request.user)
if form.is_valid():
form.save()
return redirect('edit_profile') # Or wherever you want to redirect
else:
form = CustomUserChangeForm(instance=request.user)
return render(request, 'accounts/edit_profile.html', {'form': form})
class HeadCoachCheckMixin(UserPassesTestMixin):
def test_func(self):
return self.request.user.is_superuser or self.request.user.coached_teams.exists()
class PlayerCreateView(LoginRequiredMixin, HeadCoachCheckMixin, CreateView):
model = CustomUser
form_class = PlayerCreationForm
template_name = 'accounts/player_form.html'
success_url = reverse_lazy('dashboard')
def form_valid(self, form):
# Create player user
player = form.save(commit=False)
player.is_active = False # Player can only login after using invitation code
player.save()
# Create invitation code for player
InvitationCode.objects.create(code=str(uuid.uuid4()), user=player)
# Handle parents
for i in ['1', '2']:
search_identifier = form.cleaned_data.get(f'parent{i}_search')
new_email = form.cleaned_data.get(f'parent{i}_new')
if search_identifier:
import re
match = re.search(r'\((\w+)\)', search_identifier)
if match:
username = match.group(1)
try:
parent_user = CustomUser.objects.get(username=username)
player.parents.add(parent_user)
except CustomUser.DoesNotExist:
form.add_error(f'parent{i}_search', 'User not found.')
else:
# if no user is selected from the list, maybe the user typed an email/username directly
try:
parent_user = CustomUser.objects.get(Q(username=search_identifier) | Q(email=search_identifier))
player.parents.add(parent_user)
except CustomUser.DoesNotExist:
form.add_error(f'parent{i}_search', 'User not found.')
except CustomUser.MultipleObjectsReturned:
form.add_error(f'parent{i}_search', 'Multiple users found. Please be more specific.')
elif new_email:
try:
parent_user = CustomUser.objects.create(
email=new_email,
username=new_email,
is_active=False
)
parent_user.set_unusable_password()
parent_user.verification_code = uuid.uuid4()
parent_user.save()
# Send verification email to new parent
send_verification_email(parent_user, self.request, is_parent=True)
player.parents.add(parent_user)
except IntegrityError:
form.add_error(f'parent{i}_new', f"A user with the email '{new_email}' already exists. Please use the search field to add them.")
parent_user, created = CustomUser.objects.get_or_create(email=new_email, defaults={'username': new_email, 'is_active': False})
InvitationCode.objects.create(code=str(uuid.uuid4()), user=parent_user)
player.parents.add(parent_user)
if form.errors:
return self.form_invalid(form)
...
def verify_account(request, verification_code):
user = get_object_or_404(CustomUser, verification_code=verification_code, is_verified=False)
# Determine if user is a parent (has no team) or player
is_parent = user.team is None
FormClass = ParentVerificationForm if is_parent else PlayerVerificationForm
if request.method == 'POST':
form = FormClass(request.POST)
if form.is_valid():
user.set_password(form.cleaned_data['password'])
if is_parent:
user.username = form.cleaned_data['username']
user.is_active = True
user.is_verified = True
user.verification_code = None # Invalidate the code
user.save()
login(request, user) # Log the user in
messages.success(request, 'Your account has been verified! You are now logged in.')
return redirect(self.success_url)
class MyLoginView(auth_views.LoginView):
def get(self, request, *args, **kwargs):
if request.user.is_authenticated:
return redirect('dashboard')
else:
form = FormClass()
return render(request, 'accounts/verify_account.html', {'form': form, 'is_parent': is_parent})
return super().get(request, *args, **kwargs)
def user_search(request):
q = request.GET.get('q', '')
users = CustomUser.objects.filter(last_name__istartswith=q).values('username', 'first_name', 'last_name')
results = []
for user in users:
results.append(f"{user['last_name']}, {user['first_name']} ({user['username']})")
return JsonResponse(results, safe=False)

View File

@ -25,7 +25,7 @@ SECRET_KEY = 'django-insecure-)p-ei0pchzmkv!72^wr$!_s=9a_*4kuzsy(5_(urc*w(uummf3
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ['*']
ALLOWED_HOSTS = []
# Application definition
@ -42,7 +42,6 @@ INSTALLED_APPS = [
'calendars',
'dashboard',
'team_stats',
'polls',
]
MIDDLEWARE = [
@ -141,6 +140,3 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
AUTH_USER_MODEL = 'accounts.CustomUser'
LOGIN_REDIRECT_URL = '/'
MTP_EMAIL_SEND = 0
DEFAULT_FROM_EMAIL = 'webmaster@localhost'

View File

@ -24,6 +24,5 @@ urlpatterns = [
path('clubs/', include('clubs.urls')),
path('calendars/', include('calendars.urls')),
path('statistics/', include('team_stats.urls')),
path('polls/', include('polls.urls')),
path('', include('dashboard.urls')),
]

View File

@ -12,14 +12,9 @@ class EventForm(forms.ModelForm):
class TrainingForm(forms.ModelForm):
start_time = forms.DateTimeField(input_formats=['%d.%m.%Y %H:%M', '%Y-%m-%dT%H:%M'], widget=forms.DateTimeInput(format='%d.%m.%Y %H:%M', attrs={'type': 'datetime-local'}))
end_time = forms.DateTimeField(input_formats=['%d.%m.%Y %H:%M', '%Y-%m-%dT%H:%M'], widget=forms.DateTimeInput(format='%d.%m.%Y %H:%M', attrs={'type': 'datetime-local'}), required=False)
is_recurring = forms.BooleanField(required=False, widget=forms.CheckboxInput(attrs={'class': 'form-check-input'}))
recurrence_interval = forms.IntegerField(required=False, min_value=1, widget=forms.NumberInput(attrs={'class': 'form-control'}), help_text="Repeat every X days.")
recurrence_end_date = forms.DateField(required=False, widget=forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}))
class Meta:
model = Training
fields = ['title', 'description', 'start_time', 'end_time', 'location_address', 'team', 'is_recurring', 'recurrence_interval', 'recurrence_end_date']
fields = ['title', 'description', 'start_time', 'end_time', 'location_address', 'team']
class GameForm(forms.ModelForm):
start_time = forms.DateTimeField(input_formats=['%d.%m.%Y %H:%M', '%Y-%m-%dT%H:%M'], widget=forms.DateTimeInput(format='%d.%m.%Y %H:%M', attrs={'type': 'datetime-local'}))

View File

@ -1,34 +0,0 @@
# Generated by Django 5.2.6 on 2025-11-22 09:00
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('calendars', '0005_game_number_of_innings_and_more'),
]
operations = [
migrations.AddField(
model_name='event',
name='is_recurring',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='event',
name='parent_event',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='child_events', to='calendars.event'),
),
migrations.AddField(
model_name='event',
name='recurrence_end_date',
field=models.DateField(blank=True, null=True),
),
migrations.AddField(
model_name='event',
name='recurrence_interval',
field=models.PositiveIntegerField(blank=True, help_text='In days', null=True),
),
]

View File

@ -10,12 +10,6 @@ class Event(models.Model):
location_address = models.CharField(max_length=255)
maps_shortlink = models.URLField(blank=True, editable=False)
team = models.ForeignKey('clubs.Team', on_delete=models.CASCADE, related_name='events')
# Fields for recurring events
is_recurring = models.BooleanField(default=False)
recurrence_interval = models.PositiveIntegerField(null=True, blank=True, help_text="In days")
recurrence_end_date = models.DateField(null=True, blank=True)
parent_event = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name='child_events')
def save(self, *args, **kwargs):
if self.location_address and not self.maps_shortlink:

View File

@ -8,28 +8,9 @@
<h2>Delete Event</h2>
</div>
<div class="card-body">
<p>Are you sure you want to delete the event: <strong>"{{ object.title }}"</strong> on <strong>{{ object.start_time|date:"d.m.Y" }}</strong>?</p>
<p>Are you sure you want to delete "{{ object.title }}"?</p>
<form method="post">
{% csrf_token %}
{% if is_series %}
<fieldset class="mb-3">
<legend class="h6">Deletion Scope</legend>
<div class="form-check">
<input class="form-check-input" type="radio" name="delete_scope" id="delete_one" value="one" checked>
<label class="form-check-label" for="delete_one">
Delete only this event
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="delete_scope" id="delete_all" value="all">
<label class="form-check-label" for="delete_all">
Delete the entire series
</label>
</div>
</fieldset>
{% endif %}
<button type="submit" class="btn btn-danger">Confirm Delete</button>
<a href="{% url 'dashboard' %}" class="btn btn-secondary">Cancel</a>
</form>

View File

@ -1,83 +1,40 @@
{% extends "base.html" %}
{% load l10n %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-8">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h2>{% if object %}Edit Event{% else %}Create Training{% endif %}</h2>
<h2>{% if object %}Edit Event{% else %}Create Event{% endif %}</h2>
</div>
<div class="card-body">
<form method="post">
{% csrf_token %}
<!-- Standard Fields -->
<div class="mb-3">{{ form.title.label_tag }} {{ form.title }}</div>
<div class="mb-3">{{ form.description.label_tag }} {{ form.description }}</div>
<div class="row">
<div class="col-md-6 mb-3">{{ form.start_time.label_tag }} {{ form.start_time }}</div>
<div class="col-md-6 mb-3">{{ form.end_time.label_tag }} {{ form.end_time }}</div>
</div>
<div class="mb-3">{{ form.location_address.label_tag }} {{ form.location_address }}</div>
<div class="mb-3">{{ form.team.label_tag }} {{ form.team }}</div>
<!-- Recurrence Fields -->
{% if 'is_recurring' in form.fields %}
<hr>
<div class="form-check mb-3">
{{ form.is_recurring }}
<label class="form-check-label" for="{{ form.is_recurring.id_for_label }}">{{ form.is_recurring.label }}</label>
{% for field in form %}
<div class="mb-3">
<label for="{{ field.id_for_label }}" class="form-label">{{ field.label }}</label>
{% if object and field.name == 'start_time' %}
<p>Current: {{ object.start_time|localize }}</p>
{% endif %}
{% if object and field.name == 'end_time' %}
<p>Current: {{ object.end_time|localize }}</p>
{% endif %}
{{ field }}
{% if field.help_text %}
<small class="form-text text-muted">{{ field.help_text }}</small>
{% endif %}
{% for error in field.errors %}
<div class="alert alert-danger">{{ error }}</div>
{% endfor %}
</div>
<div id="recurrence-options" style="display: none;">
<div class="row">
<div class="col-md-6 mb-3">
{{ form.recurrence_interval.label_tag }}
{{ form.recurrence_interval }}
<small class="form-text text-muted">{{ form.recurrence_interval.help_text }}</small>
</div>
<div class="col-md-6 mb-3">
{{ form.recurrence_end_date.label_tag }}
{{ form.recurrence_end_date }}
</div>
</div>
</div>
{% endif %}
{% for error in form.non_field_errors %}
<div class="alert alert-danger">{{ error }}</div>
{% endfor %}
<button type="submit" class="btn btn-primary">Save</button>
<a href="{% url 'dashboard' %}" class="btn btn-secondary">Cancel</a>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javascript %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const recurringCheckbox = document.getElementById('{{ form.is_recurring.id_for_label }}');
const recurrenceOptions = document.getElementById('recurrence-options');
function toggleRecurrenceOptions() {
if (recurringCheckbox.checked) {
recurrenceOptions.style.display = 'block';
} else {
recurrenceOptions.style.display = 'none';
}
}
if (recurringCheckbox) {
// Set initial state on page load
toggleRecurrenceOptions();
// Add event listener
recurringCheckbox.addEventListener('change', toggleRecurrenceOptions);
}
});
</script>
{% endblock %}

View File

@ -65,38 +65,6 @@ class TrainingCreateView(LoginRequiredMixin, ManageableTeamsMixin, CreateView):
template_name = 'calendars/event_form.html'
success_url = reverse_lazy('dashboard')
def form_valid(self, form):
is_recurring = form.cleaned_data.get('is_recurring')
interval = form.cleaned_data.get('recurrence_interval')
end_date = form.cleaned_data.get('recurrence_end_date')
if is_recurring and interval and end_date:
# Save the first event, which will be the parent
self.object = form.save()
# Get details from the parent event
start_time = self.object.start_time
duration = self.object.end_time - start_time if self.object.end_time else datetime.timedelta(hours=1)
current_start_time = start_time + datetime.timedelta(days=interval)
while current_start_time.date() <= end_date:
Training.objects.create(
title=self.object.title,
description=self.object.description,
start_time=current_start_time,
end_time=current_start_time + duration,
location_address=self.object.location_address,
team=self.object.team,
parent_event=self.object,
is_recurring=False # Child events are not themselves recurring
)
current_start_time += datetime.timedelta(days=interval)
return redirect(self.get_success_url())
else:
return super().form_valid(form)
class GameCreateView(LoginRequiredMixin, ManageableTeamsMixin, CreateView):
model = Game
form_class = GameForm
@ -108,20 +76,10 @@ class EventUpdateView(LoginRequiredMixin, CoachCheckMixin, UpdateView):
template_name = 'calendars/event_form.html'
success_url = reverse_lazy('dashboard')
def get_object(self, queryset=None):
# Fetch the event and then check if it's a game or training to return the specific instance
obj = super().get_object(queryset)
if hasattr(obj, 'game'):
return obj.game
if hasattr(obj, 'training'):
return obj.training
return obj
def get_form_class(self):
# self.object is now the correct child instance because of the overridden get_object
if isinstance(self.object, Game):
if hasattr(self.object, 'game'):
return GameForm
if isinstance(self.object, Training):
if hasattr(self.object, 'training'):
return TrainingForm
return EventForm
@ -130,29 +88,6 @@ class EventDeleteView(LoginRequiredMixin, CoachCheckMixin, DeleteView):
template_name = 'calendars/event_confirm_delete.html'
success_url = reverse_lazy('dashboard')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# An event is part of a series if it has a parent or is a parent
context['is_series'] = self.object.parent_event is not None or self.object.child_events.exists()
return context
def post(self, request, *args, **kwargs):
self.object = self.get_object()
delete_scope = request.POST.get('delete_scope')
if delete_scope == 'all':
# Identify the parent of the series
parent = self.object.parent_event if self.object.parent_event else self.object
# Delete all children and the parent itself
parent.child_events.all().delete()
parent.delete()
return redirect(self.success_url)
else:
# Default behavior: delete only the single event
return super().post(request, *args, **kwargs)
@login_required
def manage_participation(request, child_id, event_id, status):
child = get_object_or_404(CustomUser, id=child_id)

View File

@ -1,63 +0,0 @@
{% extends "base.html" %}
{% load static %}
{% block content %}
<div class="container mt-4">
<h1 class="mb-4">Past Games</h1>
{% if not games_by_team %}
<div class="alert alert-info" role="alert">
You are not associated with any team that has played games.
</div>
{% else %}
{% for team, games in games_by_team.items %}
<h2 class="mt-5 mb-3">{{ team.name }}</h2>
{% if not games %}
<p>No past games with results for this team.</p>
{% else %}
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="thead-dark">
<tr>
<th scope="col">Date</th>
<th scope="col">Opponent</th>
<th scope="col" colspan="11" class="text-center">Scoreline</th>
<th scope="col">Final</th>
</tr>
<tr>
<th colspan="2"></th>
{% for i in "123456789" %}
<th scope="col" class="text-center">{{ i }}</th>
{% endfor %}
<th class="text-center">R</th>
<th class="text-center">H</th>
</tr>
</thead>
<tbody>
{% for data in games %}
<tr>
<td rowspan="2">{{ data.game.start_time|date:"d.m.Y" }}</td>
<td>{{ data.game.opponent }}</td>
{% for inning_score in data.away_innings %}
<td class="text-center">{{ inning_score }}</td>
{% endfor %}
<td class="text-center fw-bold">{{ data.away_score }}</td>
<td class="text-center fw-bold">{{ data.game.result.away_hits }}</td>
</tr>
<tr>
<td class="fw-bold">{{ team.name }}</td>
{% for inning_score in data.home_innings %}
<td class="text-center">{{ inning_score }}</td>
{% endfor %}
<td class="text-center fw-bold">{{ data.home_score }}</td>
<td class="text-center fw-bold">{{ data.game.result.home_hits }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% endfor %}
{% endif %}
</div>
{% endblock %}

View File

@ -4,5 +4,4 @@ from . import views
urlpatterns = [
path('', views.dashboard, name='dashboard'),
path('players/', views.player_list, name='player_list'),
path('past-games/', views.past_games, name='past_games'),
]

View File

@ -1,12 +1,11 @@
from django.shortcuts import render
from django.contrib.auth.decorators import login_required
from calendars.models import Event, EventParticipation, Game
from calendars.models import Event, EventParticipation
from clubs.models import Team
from accounts.models import CustomUser
from django.db.models import Q
from django.utils import timezone
import datetime
from itertools import chain
def get_all_child_teams(parent_team):
"""
@ -41,7 +40,7 @@ def dashboard(request):
assisted_teams = user.assisted_teams.all()
from itertools import chain
all_teams = list(set(chain(player_teams, expanded_coached_teams, assisted_teams)))
now = timezone.now()
@ -87,9 +86,6 @@ def dashboard(request):
'days_until_event': days_until_event,
'local_start_time_iso': local_start_time.isoformat()
})
# Sort the final list by event start time
events_with_participation.sort(key=lambda x: x['event'].start_time)
# Get children's events
if hasattr(user, 'children'):
@ -106,9 +102,6 @@ def dashboard(request):
for event in child_events:
participation, created = EventParticipation.objects.get_or_create(user=child, event=event)
child_events_list.append({'event': event, 'participation': participation})
# Sort each child's event list
child_events_list.sort(key=lambda x: x['event'].start_time)
children_events.append({'child': child, 'events': child_events_list})
context = {
@ -137,68 +130,3 @@ def player_list(request):
'teams': all_teams
}
return render(request, 'dashboard/player_list.html', context)
@login_required
def past_games(request):
user = request.user
user_teams = set()
# Player's team
if user.team:
user_teams.add(user.team)
# Coached teams
for team in user.coached_teams.all():
user_teams.add(team)
user_teams.update(get_all_child_teams(team))
# Assisted teams
for team in user.assisted_teams.all():
user_teams.add(team)
# Parents' children's teams
if hasattr(user, 'children'):
for child in user.children.all():
if child.team:
user_teams.add(child.team)
# Fetch past games for all collected teams
games_qs = Game.objects.filter(
team__in=list(user_teams),
start_time__lt=timezone.now(),
result__isnull=False
).select_related('team', 'result').order_by('team__name', '-start_time')
# Group games by team
games_by_team = {}
for game in games_qs:
if game.team not in games_by_team:
games_by_team[game.team] = []
# Prepare scoreline data
result = game.result
sorted_items = sorted(result.inning_results.items(), key=lambda x: int(x[0].split('_')[1]))
home_innings = [item[1].get('home', 'X') for item in sorted_items]
away_innings = [item[1].get('guest', 'X') for item in sorted_items]
# Pad innings to 9
home_innings.extend([''] * (9 - len(home_innings)))
away_innings.extend([''] * (9 - len(away_innings)))
home_score = sum(i for i in home_innings if isinstance(i, int))
away_score = sum(i for i in away_innings if isinstance(i, int))
game_data = {
'game': game,
'home_score': home_score,
'away_score': away_score,
'home_innings': home_innings[:9],
'away_innings': away_innings[:9]
}
games_by_team[game.team].append(game_data)
context = {
'games_by_team': games_by_team
}
return render(request, 'dashboard/past_games.html', context)

View File

View File

@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View File

@ -1,6 +0,0 @@
from django.apps import AppConfig
class PollsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'polls'

View File

@ -1,23 +0,0 @@
from django import forms
from django.forms import formset_factory
from .models import Poll, Choice
class PollForm(forms.ModelForm):
class Meta:
model = Poll
fields = ['question', 'team', 'multiple_choice']
widgets = {
'question': forms.TextInput(attrs={'class': 'form-control'}),
'team': forms.Select(attrs={'class': 'form-control'}),
'multiple_choice': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
}
class ChoiceForm(forms.ModelForm):
class Meta:
model = Choice
fields = ['choice_text']
widgets = {
'choice_text': forms.TextInput(attrs={'class': 'form-control', 'required': True}),
}
ChoiceFormSet = formset_factory(ChoiceForm, extra=5, max_num=5)

View File

@ -1,38 +0,0 @@
# Generated by Django 5.2.6 on 2025-11-21 21:18
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('clubs', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Poll',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('question', models.CharField(max_length=255)),
('multiple_choice', models.BooleanField(default=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='polls_created', to=settings.AUTH_USER_MODEL)),
('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='polls', to='clubs.team')),
],
),
migrations.CreateModel(
name='Choice',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('choice_text', models.CharField(max_length=100)),
('votes', models.ManyToManyField(blank=True, related_name='voted_choices', to=settings.AUTH_USER_MODEL)),
('poll', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='choices', to='polls.poll')),
],
),
]

View File

@ -1,21 +0,0 @@
from django.db import models
from clubs.models import Team
from accounts.models import CustomUser
class Poll(models.Model):
question = models.CharField(max_length=255)
team = models.ForeignKey(Team, on_delete=models.CASCADE, related_name='polls')
creator = models.ForeignKey(CustomUser, on_delete=models.CASCADE, related_name='polls_created')
multiple_choice = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.question
class Choice(models.Model):
poll = models.ForeignKey(Poll, on_delete=models.CASCADE, related_name='choices')
choice_text = models.CharField(max_length=100)
votes = models.ManyToManyField(CustomUser, related_name='voted_choices', blank=True)
def __str__(self):
return self.choice_text

View File

@ -1,29 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-4">
<h1>{{ poll.question }}</h1>
<p class="text-muted">Asked by {{ poll.creator.get_full_name }} for team {{ poll.team.name }}</p>
<form action="{% url 'polls:vote' poll.id %}" method="post">
{% csrf_token %}
<fieldset class="mb-3">
<legend>Your Vote</legend>
{% for choice in poll.choices.all %}
<div class="form-check">
{% if poll.multiple_choice %}
<input class="form-check-input" type="checkbox" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}">
{% else %}
<input class="form-check-input" type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}">
{% endif %}
<label class="form-check-label" for="choice{{ forloop.counter }}">
{{ choice.choice_text }}
</label>
</div>
{% endfor %}
</fieldset>
<button type="submit" class="btn btn-success">Submit Vote</button>
<a href="{% url 'polls:poll_results' poll.id %}" class="btn btn-secondary">View Results</a>
</form>
</div>
{% endblock %}

View File

@ -1,40 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-4">
<h1>Create a New Poll</h1>
<form method="post">
{% csrf_token %}
<div class="mb-3">
{{ form.question.label_tag }}
{{ form.question }}
</div>
<div class="mb-3">
{{ form.team.label_tag }}
{{ form.team }}
</div>
<div class="form-check mb-3">
{{ form.multiple_choice }}
{{ form.multiple_choice.label_tag }}
</div>
<hr>
<h4>Choices</h4>
{{ choice_formset.management_form }}
<div id="choice-forms">
{% for form in choice_formset %}
<div class="mb-2">
{{ form.choice_text.label_tag }}
{{ form.choice_text }}
</div>
{% endfor %}
</div>
<button type="submit" class="btn btn-primary">Save Poll</button>
</form>
</div>
{% endblock %}

View File

@ -1,31 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>Polls</h1>
{% if user.coached_teams.exists %}
<a href="{% url 'polls:poll_create' %}" class="btn btn-primary">Create Poll</a>
{% endif %}
</div>
{% if polls %}
<div class="list-group">
{% for poll in polls %}
<a href="{% url 'polls:poll_detail' poll.pk %}" class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">{{ poll.question }}</h5>
<small>{{ poll.created_at|date:"d.m.Y" }}</small>
</div>
<p class="mb-1">For team: {{ poll.team.name }}</p>
<small>Created by: {{ poll.creator.get_full_name }}</small>
</a>
{% endfor %}
</div>
{% else %}
<div class="alert alert-info" role="alert">
No polls available for your teams yet.
</div>
{% endif %}
</div>
{% endblock %}

View File

@ -1,22 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-4">
<h1>{{ poll.question }}</h1>
<p class="text-muted">Results for the poll asked by {{ poll.creator.get_full_name }}</p>
<ul class="list-group">
{% for choice in poll.choices.all %}
<li class="list-group-item d-flex justify-content-between align-items-center">
{{ choice.choice_text }}
<span class="badge bg-primary rounded-pill">{{ choice.votes.count }} vote(s)</span>
</li>
{% endfor %}
</ul>
<div class="mt-3">
<a href="{% url 'polls:poll_detail' poll.id %}" class="btn btn-secondary">Back to Vote</a>
<a href="{% url 'polls:poll_list' %}" class="btn btn-outline-primary">Back to All Polls</a>
</div>
</div>
{% endblock %}

View File

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@ -1,12 +0,0 @@
from django.urls import path
from . import views
app_name = 'polls'
urlpatterns = [
path('', views.PollListView.as_view(), name='poll_list'),
path('create/', views.PollCreateView.as_view(), name='poll_create'),
path('<int:pk>/', views.PollDetailView.as_view(), name='poll_detail'),
path('<int:pk>/results/', views.PollResultsView.as_view(), name='poll_results'),
path('<int:poll_id>/vote/', views.vote, name='vote'),
]

View File

@ -1,128 +0,0 @@
from django.shortcuts import render, get_object_or_404, redirect
from django.views.generic import ListView, CreateView, DetailView
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.urls import reverse_lazy, reverse
from django.db import transaction
from .models import Poll, Choice
from .forms import PollForm, ChoiceFormSet
from clubs.models import Team
from accounts.models import CustomUser
class PollListView(LoginRequiredMixin, ListView):
model = Poll
template_name = 'polls/poll_list.html'
context_object_name = 'polls'
def get_queryset(self):
user = self.request.user
user_teams = set()
if user.team:
user_teams.add(user.team)
# Add teams where the user is a coach or assistant
user_teams.update(user.coached_teams.all())
user_teams.update(user.assisted_teams.all())
# Add teams of children for parents
if hasattr(user, 'children'):
for child in user.children.all():
if child.team:
user_teams.add(child.team)
return Poll.objects.filter(team__in=user_teams).order_by('-created_at')
class PollCreateView(LoginRequiredMixin, UserPassesTestMixin, CreateView):
model = Poll
form_class = PollForm
template_name = 'polls/poll_form.html'
def test_func(self):
# Only head coaches can create polls
return self.request.user.coached_teams.exists()
def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs)
if self.request.POST:
data['choice_formset'] = ChoiceFormSet(self.request.POST)
else:
data['choice_formset'] = ChoiceFormSet()
# Filter the team queryset for the form to only show teams the user coaches
data['form'].fields['team'].queryset = self.request.user.coached_teams.all()
return data
def form_valid(self, form):
context = self.get_context_data()
choice_formset = context['choice_formset']
# Set the creator before saving the poll
form.instance.creator = self.request.user
if choice_formset.is_valid():
self.object = form.save()
with transaction.atomic():
for choice_form in choice_formset:
if choice_form.cleaned_data.get('choice_text'):
Choice.objects.create(
poll=self.object,
choice_text=choice_form.cleaned_data['choice_text']
)
return redirect(self.get_success_url())
else:
return self.form_invalid(form)
def get_success_url(self):
return reverse('polls:poll_list')
class PollDetailView(LoginRequiredMixin, UserPassesTestMixin, DetailView):
model = Poll
template_name = 'polls/poll_detail.html'
def test_func(self):
# Check if user is part of the team for which the poll is
poll = self.get_object()
user = self.request.user
user_teams = set()
if user.team:
user_teams.add(user.team)
user_teams.update(user.coached_teams.all())
user_teams.update(user.assisted_teams.all())
if hasattr(user, 'children'):
for child in user.children.all():
if child.team:
user_teams.add(child.team)
return poll.team in user_teams
def vote(request, poll_id):
poll = get_object_or_404(Poll, pk=poll_id)
user = request.user
if request.method == 'POST':
if poll.multiple_choice:
selected_choice_ids = request.POST.getlist('choice')
# First, remove user's previous votes for this poll
for choice in poll.choices.all():
choice.votes.remove(user)
# Then, add new votes
for choice_id in selected_choice_ids:
choice = get_object_or_404(Choice, pk=choice_id)
choice.votes.add(user)
else:
selected_choice_id = request.POST.get('choice')
if selected_choice_id:
# Remove user's vote from all choices in this poll first
for choice in poll.choices.all():
choice.votes.remove(user)
# Add the new vote
choice = get_object_or_404(Choice, pk=selected_choice_id)
choice.votes.add(user)
return redirect('polls:poll_results', pk=poll.id)
class PollResultsView(LoginRequiredMixin, DetailView):
model = Poll
template_name = 'polls/poll_results.html'
context_object_name = 'poll'

View File

@ -1,107 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Season Report for {{ team.name }} - {{ season }}</title>
<style>
body {
font-family: sans-serif;
font-size: 10px;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
border: 1px solid #ccc;
padding: 4px;
text-align: center;
}
thead th {
background-color: #f2f2f2;
}
.player-header {
height: 150px;
white-space: nowrap;
position: relative;
}
.player-header > div {
transform: rotate(-90deg);
position: absolute;
left: 0;
bottom: 0;
transform-origin: 0 100%;
width: 150px; /* Should match height */
}
.status-attending {
color: green;
}
.status-rejected {
color: red;
}
.status-maybe {
color: #aaa;
}
.print-button {
margin: 1rem;
padding: 0.5rem 1rem;
}
@media print {
@page {
size: A4 landscape;
margin: 1cm;
}
body {
font-size: 9px;
}
.print-button {
display: none;
}
}
</style>
</head>
<body>
<button onclick="window.print();" class="print-button">Print Report</button>
<h2>Season Report</h2>
<p><strong>Team:</strong> {{ team.name }}</p>
<p><strong>Season:</strong> {{ season }}</p>
<table>
<thead>
<tr>
<th>Date</th>
<th>Opponent</th>
{% for player in players %}
<th class="player-header"><div>{{ player.get_full_name }}</div></th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for row in report_data %}
<tr>
<td>{{ row.game.start_time|date:"d.m.Y" }}</td>
<td>{{ row.game.opponent }}</td>
{% for status in row.statuses %}
<td class="status-{{ status }}">
{% if status == 'attending' %}
{% elif status == 'rejected' %}
{% else %}
?
{% endif %}
</td>
{% endfor %}
</tr>
{% empty %}
<tr>
<td colspan="{{ players|length|add:2 }}">No games found for this season.</td>
</tr>
{% endfor %}
</tbody>
</table>
</body>
</html>

View File

@ -22,11 +22,6 @@
<div class="col-md-2">
<button type="submit" class="btn btn-primary">Filter</button>
</div>
{% if selected_season %}
<div class="col-md-3">
<a href="{% url 'team_stats:season_report' team_id=team.id season=selected_season %}" class="btn btn-secondary" target="_blank">Generate Season Report</a>
</div>
{% endif %}
</div>
</form>
</div>
@ -72,9 +67,9 @@
</div>
</div>
<!-- Luck-O-Meter, Supporter Stats -->
<!-- Luck-O-Meter and Inning Heatmap -->
<div class="row mt-4">
<div class="col-md-6">
<div class="col-md-4">
<div class="card">
<div class="card-header">Luck-O-Meter</div>
<div class="card-body">
@ -83,20 +78,7 @@
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">Supporter Stats</div>
<div class="card-body">
<p>Games with Supporters: {{ stats.games_with_supporters }}</p>
<p>Avg. Supporter Share: {{ stats.avg_supporter_player_percentage|floatformat:2 }}%</p>
</div>
</div>
</div>
</div>
<!-- Inning Heatmap -->
<div class="row mt-4">
<div class="col-md-12">
<div class="col-md-8">
<div class="card">
<div class="card-header">Inning Heatmap (Runs Scored)</div>
<div class="card-body">

View File

@ -5,5 +5,4 @@ app_name = 'team_stats'
urlpatterns = [
path('team/<int:team_id>/', views.team_statistics, name='team_statistics'),
path('team/<int:team_id>/report/<str:season>/', views.season_report, name='season_report'),
]

View File

@ -2,13 +2,13 @@ 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, EventParticipation
from calendars.models import Game
def _calculate_statistics(team, season=None):
"""
Calculates statistics for a given team and season.
"""
games_query = Game.objects.filter(team=team, result__isnull=False).prefetch_related('opened_for_teams')
games_query = Game.objects.filter(team=team, result__isnull=False)
if season:
games_query = games_query.filter(season=season)
@ -20,25 +20,10 @@ def _calculate_statistics(team, season=None):
runs_allowed = 0
inning_runs = {i: 0 for i in range(1, 10)}
streak_counter = 0
current_streak_type = None
last_game_result = None
games_with_supporters = 0
total_supporter_player_percentage = 0
for game in games:
# Supporter stats
if game.opened_for_teams.exists():
games_with_supporters += 1
attending_players = EventParticipation.objects.filter(event=game, status='attending').select_related('user__team')
total_attendees = attending_players.count()
if total_attendees > 0:
supporter_players_count = attending_players.exclude(user__team=game.team).count()
supporter_percentage = (supporter_players_count / total_attendees) * 100
total_supporter_player_percentage += supporter_percentage
# Standard game stats
result = game.result
sorted_items = sorted(result.inning_results.items(), key=lambda x: int(x[0].split('_')[1]))
@ -99,8 +84,6 @@ def _calculate_statistics(team, season=None):
pythagorean_pct = (runs_scored**2 / (runs_scored**2 + runs_allowed**2)) * 100
else:
pythagorean_pct = 0
avg_supporter_player_percentage = (total_supporter_player_percentage / games_with_supporters) if games_with_supporters > 0 else 0
return {
'team': team,
@ -113,8 +96,6 @@ def _calculate_statistics(team, season=None):
'pythagorean_pct': pythagorean_pct,
'inning_runs': inning_runs,
'total_games': total_games,
'games_with_supporters': games_with_supporters,
'avg_supporter_player_percentage': avg_supporter_player_percentage,
}
@login_required
@ -147,50 +128,3 @@ def team_statistics(request, team_id):
}
return render(request, 'team_stats/team_statistics.html', context)
@login_required
def season_report(request, team_id, season):
team = get_object_or_404(Team, pk=team_id)
# Security check: only head coach can view
if request.user != team.head_coach:
return HttpResponseForbidden("You are not authorized to view this report.")
# 1. Get all players for the team, ordered
players = team.players.all().order_by('last_name', 'first_name')
player_ids = [p.id for p in players]
# 2. Get all games for the team and season
games = Game.objects.filter(team=team, season=season).order_by('start_time')
game_ids = [g.id for g in games]
# 3. Get all relevant participation data in one query
participations = EventParticipation.objects.filter(
event_id__in=game_ids,
user_id__in=player_ids
)
# 4. Create a fast lookup map
participation_map = {
(p.event_id, p.user_id): p.status for p in participations
}
# 5. Build the final data structure for the template
report_data = []
for game in games:
statuses = []
for player in players:
status = participation_map.get((game.id, player.id), 'maybe')
statuses.append(status)
report_data.append({
'game': game,
'statuses': statuses
})
context = {
'team': team,
'season': season,
'players': players,
'report_data': report_data,
}
return render(request, 'team_stats/season_report.html', context)

View File

@ -38,12 +38,6 @@
<li class="nav-item">
<a class="nav-link" href="{% url 'edit_profile' %}">Profile</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'past_games' %}">Past Games</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'polls:poll_list' %}">Polls</a>
</li>
{% if user.coached_teams.all %}
<li class="nav-item">
<a class="nav-link" href="{% url 'player-add' %}">Create New Player</a>