Compare commits
No commits in common. "fd47f25c36a67948a038fbd0121fa2e0e885d584" and "c63ad532b583280f8bf2cde969829de45456c73c" have entirely different histories.
fd47f25c36
...
c63ad532b5
@ -1,5 +1,18 @@
|
|||||||
from django import forms
|
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):
|
class CustomUserCreationForm(forms.ModelForm):
|
||||||
password = forms.CharField(widget=forms.PasswordInput)
|
password = forms.CharField(widget=forms.PasswordInput)
|
||||||
@ -24,22 +37,3 @@ class PlayerCreationForm(forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = CustomUser
|
model = CustomUser
|
||||||
fields = ('username', 'first_name', 'last_name', 'email', 'birth_date', 'player_number', 'team')
|
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
|
|
||||||
|
|||||||
@ -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',
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,4 +1,3 @@
|
|||||||
import uuid
|
|
||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@ -9,11 +8,6 @@ class CustomUser(AbstractUser):
|
|||||||
player_number = models.IntegerField(default=999)
|
player_number = models.IntegerField(default=999)
|
||||||
team = models.ForeignKey('clubs.Team', on_delete=models.SET_NULL, null=True, blank=True, related_name='players')
|
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')
|
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
|
@property
|
||||||
def age(self):
|
def age(self):
|
||||||
@ -22,6 +16,23 @@ class CustomUser(AbstractUser):
|
|||||||
today = datetime.date.today()
|
today = datetime.date.today()
|
||||||
return today.year - self.birth_date.year - ((today.month, today.day) < (self.birth_date.month, self.birth_date.day))
|
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):
|
class AbsencePeriod(models.Model):
|
||||||
user = models.ForeignKey(CustomUser, on_delete=models.CASCADE, related_name='absence_periods')
|
user = models.ForeignKey(CustomUser, on_delete=models.CASCADE, related_name='absence_periods')
|
||||||
start_date = models.DateField()
|
start_date = models.DateField()
|
||||||
|
|||||||
@ -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>
|
|
||||||
@ -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
|
|
||||||
@ -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>
|
|
||||||
@ -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
|
|
||||||
@ -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 %}
|
|
||||||
@ -3,7 +3,8 @@ from . import views
|
|||||||
from django.contrib.auth import views as auth_views
|
from django.contrib.auth import views as auth_views
|
||||||
|
|
||||||
urlpatterns = [
|
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('login/', views.MyLoginView.as_view(template_name='accounts/login.html'), name='login'),
|
||||||
path('logout/', auth_views.LogoutView.as_view(), name='logout'),
|
path('logout/', auth_views.LogoutView.as_view(), name='logout'),
|
||||||
path('profile/', views.edit_profile, name='edit_profile'),
|
path('profile/', views.edit_profile, name='edit_profile'),
|
||||||
|
|||||||
@ -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)
|
|
||||||
@ -3,58 +3,128 @@ from django.urls import reverse_lazy
|
|||||||
from django.views.generic.edit import CreateView
|
from django.views.generic.edit import CreateView
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
|
||||||
from django.contrib.auth import views as auth_views, login
|
from django.contrib.auth import views as auth_views
|
||||||
from django.contrib import messages
|
from django.db.models import Q
|
||||||
from django.db import Q, IntegrityError
|
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from .forms import CustomUserCreationForm, CustomUserChangeForm, PlayerCreationForm, PlayerVerificationForm, ParentVerificationForm
|
from .forms import InvitationCodeForm, CustomUserCreationForm, CustomUserChangeForm, PlayerCreationForm
|
||||||
from .models import CustomUser
|
from .models import CustomUser, InvitationCode
|
||||||
from .utils import send_verification_email
|
|
||||||
import uuid
|
import uuid
|
||||||
...
|
|
||||||
elif new_email:
|
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:
|
try:
|
||||||
parent_user = CustomUser.objects.create(
|
invitation_code = InvitationCode.objects.get(code=invitation_code_str)
|
||||||
email=new_email,
|
if not invitation_code.is_valid():
|
||||||
username=new_email,
|
# Handle invalid code, maybe redirect with a message
|
||||||
is_active=False
|
return redirect('invitation_code')
|
||||||
)
|
except InvitationCode.DoesNotExist:
|
||||||
parent_user.set_unusable_password()
|
return redirect('invitation_code')
|
||||||
parent_user.verification_code = uuid.uuid4()
|
|
||||||
parent_user.save()
|
|
||||||
# Send verification email to new parent
|
if request.method == 'POST':
|
||||||
send_verification_email(parent_user, self.request, is_parent=True)
|
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:
|
||||||
|
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)
|
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.")
|
|
||||||
|
|
||||||
if form.errors:
|
if form.errors:
|
||||||
return self.form_invalid(form)
|
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
|
return redirect(self.success_url)
|
||||||
is_parent = user.team is None
|
|
||||||
|
|
||||||
FormClass = ParentVerificationForm if is_parent else PlayerVerificationForm
|
class MyLoginView(auth_views.LoginView):
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
if request.method == 'POST':
|
if request.user.is_authenticated:
|
||||||
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('dashboard')
|
return redirect('dashboard')
|
||||||
else:
|
return super().get(request, *args, **kwargs)
|
||||||
form = FormClass()
|
|
||||||
|
|
||||||
return render(request, 'accounts/verify_account.html', {'form': form, 'is_parent': is_parent})
|
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)
|
||||||
@ -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!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
|
|
||||||
ALLOWED_HOSTS = ['*']
|
ALLOWED_HOSTS = []
|
||||||
|
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
@ -42,7 +42,6 @@ INSTALLED_APPS = [
|
|||||||
'calendars',
|
'calendars',
|
||||||
'dashboard',
|
'dashboard',
|
||||||
'team_stats',
|
'team_stats',
|
||||||
'polls',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
@ -141,6 +140,3 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
|||||||
AUTH_USER_MODEL = 'accounts.CustomUser'
|
AUTH_USER_MODEL = 'accounts.CustomUser'
|
||||||
|
|
||||||
LOGIN_REDIRECT_URL = '/'
|
LOGIN_REDIRECT_URL = '/'
|
||||||
|
|
||||||
MTP_EMAIL_SEND = 0
|
|
||||||
DEFAULT_FROM_EMAIL = 'webmaster@localhost'
|
|
||||||
|
|||||||
@ -24,6 +24,5 @@ urlpatterns = [
|
|||||||
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('statistics/', include('team_stats.urls')),
|
||||||
path('polls/', include('polls.urls')),
|
|
||||||
path('', include('dashboard.urls')),
|
path('', include('dashboard.urls')),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -12,14 +12,9 @@ class EventForm(forms.ModelForm):
|
|||||||
class TrainingForm(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'}))
|
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)
|
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:
|
class Meta:
|
||||||
model = Training
|
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):
|
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'}))
|
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'}))
|
||||||
|
|||||||
@ -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),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -11,12 +11,6 @@ class Event(models.Model):
|
|||||||
maps_shortlink = models.URLField(blank=True, editable=False)
|
maps_shortlink = models.URLField(blank=True, editable=False)
|
||||||
team = models.ForeignKey('clubs.Team', on_delete=models.CASCADE, related_name='events')
|
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):
|
def save(self, *args, **kwargs):
|
||||||
if self.location_address and not self.maps_shortlink:
|
if self.location_address and not self.maps_shortlink:
|
||||||
self.maps_shortlink = f"https://www.google.com/maps/search/?api=1&query={urllib.parse.quote(self.location_address)}"
|
self.maps_shortlink = f"https://www.google.com/maps/search/?api=1&query={urllib.parse.quote(self.location_address)}"
|
||||||
|
|||||||
@ -8,28 +8,9 @@
|
|||||||
<h2>Delete Event</h2>
|
<h2>Delete Event</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<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">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% 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>
|
<button type="submit" class="btn btn-danger">Confirm Delete</button>
|
||||||
<a href="{% url 'dashboard' %}" class="btn btn-secondary">Cancel</a>
|
<a href="{% url 'dashboard' %}" class="btn btn-secondary">Cancel</a>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -1,83 +1,40 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
{% load l10n %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-md-8">
|
<div class="col-md-6">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<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>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<!-- Standard Fields -->
|
{% for field in form %}
|
||||||
<div class="mb-3">{{ form.title.label_tag }} {{ form.title }}</div>
|
<div class="mb-3">
|
||||||
<div class="mb-3">{{ form.description.label_tag }} {{ form.description }}</div>
|
<label for="{{ field.id_for_label }}" class="form-label">{{ field.label }}</label>
|
||||||
<div class="row">
|
{% if object and field.name == 'start_time' %}
|
||||||
<div class="col-md-6 mb-3">{{ form.start_time.label_tag }} {{ form.start_time }}</div>
|
<p>Current: {{ object.start_time|localize }}</p>
|
||||||
<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>
|
|
||||||
</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 %}
|
{% endif %}
|
||||||
|
{% if object and field.name == 'end_time' %}
|
||||||
{% for error in form.non_field_errors %}
|
<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>
|
<div class="alert alert-danger">{{ error }}</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary">Save</button>
|
<button type="submit" class="btn btn-primary">Save</button>
|
||||||
<a href="{% url 'dashboard' %}" class="btn btn-secondary">Cancel</a>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% 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 %}
|
|
||||||
|
|||||||
@ -65,38 +65,6 @@ class TrainingCreateView(LoginRequiredMixin, ManageableTeamsMixin, CreateView):
|
|||||||
template_name = 'calendars/event_form.html'
|
template_name = 'calendars/event_form.html'
|
||||||
success_url = reverse_lazy('dashboard')
|
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):
|
class GameCreateView(LoginRequiredMixin, ManageableTeamsMixin, CreateView):
|
||||||
model = Game
|
model = Game
|
||||||
form_class = GameForm
|
form_class = GameForm
|
||||||
@ -108,20 +76,10 @@ class EventUpdateView(LoginRequiredMixin, CoachCheckMixin, UpdateView):
|
|||||||
template_name = 'calendars/event_form.html'
|
template_name = 'calendars/event_form.html'
|
||||||
success_url = reverse_lazy('dashboard')
|
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):
|
def get_form_class(self):
|
||||||
# self.object is now the correct child instance because of the overridden get_object
|
if hasattr(self.object, 'game'):
|
||||||
if isinstance(self.object, Game):
|
|
||||||
return GameForm
|
return GameForm
|
||||||
if isinstance(self.object, Training):
|
if hasattr(self.object, 'training'):
|
||||||
return TrainingForm
|
return TrainingForm
|
||||||
return EventForm
|
return EventForm
|
||||||
|
|
||||||
@ -130,29 +88,6 @@ class EventDeleteView(LoginRequiredMixin, CoachCheckMixin, DeleteView):
|
|||||||
template_name = 'calendars/event_confirm_delete.html'
|
template_name = 'calendars/event_confirm_delete.html'
|
||||||
success_url = reverse_lazy('dashboard')
|
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
|
@login_required
|
||||||
def manage_participation(request, child_id, event_id, status):
|
def manage_participation(request, child_id, event_id, status):
|
||||||
child = get_object_or_404(CustomUser, id=child_id)
|
child = get_object_or_404(CustomUser, id=child_id)
|
||||||
|
|||||||
@ -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 %}
|
|
||||||
@ -4,5 +4,4 @@ from . import views
|
|||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', views.dashboard, name='dashboard'),
|
path('', views.dashboard, name='dashboard'),
|
||||||
path('players/', views.player_list, name='player_list'),
|
path('players/', views.player_list, name='player_list'),
|
||||||
path('past-games/', views.past_games, name='past_games'),
|
|
||||||
]
|
]
|
||||||
@ -1,12 +1,11 @@
|
|||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.contrib.auth.decorators import login_required
|
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 clubs.models import Team
|
||||||
from accounts.models import CustomUser
|
from accounts.models import CustomUser
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
import datetime
|
import datetime
|
||||||
from itertools import chain
|
|
||||||
|
|
||||||
def get_all_child_teams(parent_team):
|
def get_all_child_teams(parent_team):
|
||||||
"""
|
"""
|
||||||
@ -41,7 +40,7 @@ def dashboard(request):
|
|||||||
|
|
||||||
assisted_teams = user.assisted_teams.all()
|
assisted_teams = user.assisted_teams.all()
|
||||||
|
|
||||||
|
from itertools import chain
|
||||||
all_teams = list(set(chain(player_teams, expanded_coached_teams, assisted_teams)))
|
all_teams = list(set(chain(player_teams, expanded_coached_teams, assisted_teams)))
|
||||||
|
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
@ -88,9 +87,6 @@ def dashboard(request):
|
|||||||
'local_start_time_iso': local_start_time.isoformat()
|
'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
|
# Get children's events
|
||||||
if hasattr(user, 'children'):
|
if hasattr(user, 'children'):
|
||||||
print('haschild')
|
print('haschild')
|
||||||
@ -106,9 +102,6 @@ def dashboard(request):
|
|||||||
for event in child_events:
|
for event in child_events:
|
||||||
participation, created = EventParticipation.objects.get_or_create(user=child, event=event)
|
participation, created = EventParticipation.objects.get_or_create(user=child, event=event)
|
||||||
child_events_list.append({'event': event, 'participation': participation})
|
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})
|
children_events.append({'child': child, 'events': child_events_list})
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
@ -137,68 +130,3 @@ def player_list(request):
|
|||||||
'teams': all_teams
|
'teams': all_teams
|
||||||
}
|
}
|
||||||
return render(request, 'dashboard/player_list.html', context)
|
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)
|
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
# Register your models here.
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class PollsConfig(AppConfig):
|
|
||||||
default_auto_field = 'django.db.models.BigAutoField'
|
|
||||||
name = 'polls'
|
|
||||||
@ -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)
|
|
||||||
@ -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')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -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
|
|
||||||
@ -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 %}
|
|
||||||
@ -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 %}
|
|
||||||
@ -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 %}
|
|
||||||
@ -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 %}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
||||||
@ -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'),
|
|
||||||
]
|
|
||||||
128
polls/views.py
128
polls/views.py
@ -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'
|
|
||||||
@ -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>
|
|
||||||
@ -22,11 +22,6 @@
|
|||||||
<div class="col-md-2">
|
<div class="col-md-2">
|
||||||
<button type="submit" class="btn btn-primary">Filter</button>
|
<button type="submit" class="btn btn-primary">Filter</button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@ -72,9 +67,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Luck-O-Meter, Supporter Stats -->
|
<!-- Luck-O-Meter and Inning Heatmap -->
|
||||||
<div class="row mt-4">
|
<div class="row mt-4">
|
||||||
<div class="col-md-6">
|
<div class="col-md-4">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">Luck-O-Meter</div>
|
<div class="card-header">Luck-O-Meter</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@ -83,20 +78,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-8">
|
||||||
<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="card">
|
<div class="card">
|
||||||
<div class="card-header">Inning Heatmap (Runs Scored)</div>
|
<div class="card-header">Inning Heatmap (Runs Scored)</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|||||||
@ -5,5 +5,4 @@ app_name = 'team_stats'
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('team/<int:team_id>/', views.team_statistics, name='team_statistics'),
|
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'),
|
|
||||||
]
|
]
|
||||||
|
|||||||
@ -2,13 +2,13 @@ from django.shortcuts import render, get_object_or_404
|
|||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.http import HttpResponseForbidden
|
from django.http import HttpResponseForbidden
|
||||||
from clubs.models import Team
|
from clubs.models import Team
|
||||||
from calendars.models import Game, EventParticipation
|
from calendars.models import Game
|
||||||
|
|
||||||
def _calculate_statistics(team, season=None):
|
def _calculate_statistics(team, season=None):
|
||||||
"""
|
"""
|
||||||
Calculates statistics for a given team and season.
|
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:
|
if season:
|
||||||
games_query = games_query.filter(season=season)
|
games_query = games_query.filter(season=season)
|
||||||
|
|
||||||
@ -20,25 +20,10 @@ def _calculate_statistics(team, season=None):
|
|||||||
runs_allowed = 0
|
runs_allowed = 0
|
||||||
inning_runs = {i: 0 for i in range(1, 10)}
|
inning_runs = {i: 0 for i in range(1, 10)}
|
||||||
streak_counter = 0
|
streak_counter = 0
|
||||||
|
current_streak_type = None
|
||||||
last_game_result = None
|
last_game_result = None
|
||||||
|
|
||||||
games_with_supporters = 0
|
|
||||||
total_supporter_player_percentage = 0
|
|
||||||
|
|
||||||
for game in games:
|
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
|
result = game.result
|
||||||
|
|
||||||
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]))
|
||||||
@ -100,8 +85,6 @@ def _calculate_statistics(team, season=None):
|
|||||||
else:
|
else:
|
||||||
pythagorean_pct = 0
|
pythagorean_pct = 0
|
||||||
|
|
||||||
avg_supporter_player_percentage = (total_supporter_player_percentage / games_with_supporters) if games_with_supporters > 0 else 0
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'team': team,
|
'team': team,
|
||||||
'wins': wins,
|
'wins': wins,
|
||||||
@ -113,8 +96,6 @@ def _calculate_statistics(team, season=None):
|
|||||||
'pythagorean_pct': pythagorean_pct,
|
'pythagorean_pct': pythagorean_pct,
|
||||||
'inning_runs': inning_runs,
|
'inning_runs': inning_runs,
|
||||||
'total_games': total_games,
|
'total_games': total_games,
|
||||||
'games_with_supporters': games_with_supporters,
|
|
||||||
'avg_supporter_player_percentage': avg_supporter_player_percentage,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@ -147,50 +128,3 @@ def team_statistics(request, team_id):
|
|||||||
}
|
}
|
||||||
|
|
||||||
return render(request, 'team_stats/team_statistics.html', context)
|
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)
|
|
||||||
@ -38,12 +38,6 @@
|
|||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{% url 'edit_profile' %}">Profile</a>
|
<a class="nav-link" href="{% url 'edit_profile' %}">Profile</a>
|
||||||
</li>
|
</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 %}
|
{% if user.coached_teams.all %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="{% url 'player-add' %}">Create New Player</a>
|
<a class="nav-link" href="{% url 'player-add' %}">Create New Player</a>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user