diff --git a/accounts/forms.py b/accounts/forms.py
index 48d7111..c507203 100644
--- a/accounts/forms.py
+++ b/accounts/forms.py
@@ -1,18 +1,5 @@
from django import forms
-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
+from .models import CustomUser
class CustomUserCreationForm(forms.ModelForm):
password = forms.CharField(widget=forms.PasswordInput)
@@ -37,3 +24,22 @@ 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
diff --git a/accounts/migrations/0005_customuser_is_verified_customuser_verification_code_and_more.py b/accounts/migrations/0005_customuser_is_verified_customuser_verification_code_and_more.py
new file mode 100644
index 0000000..b7418c8
--- /dev/null
+++ b/accounts/migrations/0005_customuser_is_verified_customuser_verification_code_and_more.py
@@ -0,0 +1,31 @@
+# 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',
+ ),
+ ]
diff --git a/accounts/models.py b/accounts/models.py
index 1d12871..8b9c39f 100644
--- a/accounts/models.py
+++ b/accounts/models.py
@@ -1,3 +1,4 @@
+import uuid
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.utils import timezone
@@ -8,6 +9,11 @@ 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):
@@ -16,23 +22,6 @@ 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()
diff --git a/accounts/templates/accounts/email/parent_verification.html b/accounts/templates/accounts/email/parent_verification.html
new file mode 100644
index 0000000..e956e4f
--- /dev/null
+++ b/accounts/templates/accounts/email/parent_verification.html
@@ -0,0 +1,15 @@
+
+
+
+ Kontoaktivierung - Baseball Organisator
+
+
+ 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:
+ Konto jetzt aktivieren
+ Ihr Verifizierungscode lautet: {{ verification_code }}
+ 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:
{{ verification_url }}
+ Vielen Dank,
Ihr Baseball Organisator Team
+
+
diff --git a/accounts/templates/accounts/email/parent_verification.txt b/accounts/templates/accounts/email/parent_verification.txt
new file mode 100644
index 0000000..64088a3
--- /dev/null
+++ b/accounts/templates/accounts/email/parent_verification.txt
@@ -0,0 +1,14 @@
+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
diff --git a/accounts/templates/accounts/email/player_verification.html b/accounts/templates/accounts/email/player_verification.html
new file mode 100644
index 0000000..d9047d1
--- /dev/null
+++ b/accounts/templates/accounts/email/player_verification.html
@@ -0,0 +1,15 @@
+
+
+
+ Willkommen beim Baseball Organisator
+
+
+ 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:
+ Konto jetzt aktivieren
+ Dein Verifizierungscode lautet: {{ verification_code }}
+ Wenn du den Link nicht klicken kannst, kopiere bitte die folgende URL und füge sie in die Adresszeile deines Browsers ein:
{{ verification_url }}
+ Vielen Dank,
Dein Baseball Organisator Team
+
+
diff --git a/accounts/templates/accounts/email/player_verification.txt b/accounts/templates/accounts/email/player_verification.txt
new file mode 100644
index 0000000..9c71330
--- /dev/null
+++ b/accounts/templates/accounts/email/player_verification.txt
@@ -0,0 +1,14 @@
+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
diff --git a/accounts/templates/accounts/verify_account.html b/accounts/templates/accounts/verify_account.html
new file mode 100644
index 0000000..d7a4683
--- /dev/null
+++ b/accounts/templates/accounts/verify_account.html
@@ -0,0 +1,51 @@
+{% extends "base.html" %}
+
+{% block content %}
+
+
+
+
+
+
Bitte legen Sie Ihre Zugangsdaten fest.
+
+
+
+
+
+{% endblock %}
diff --git a/accounts/urls.py b/accounts/urls.py
index f1dcba7..a000e99 100644
--- a/accounts/urls.py
+++ b/accounts/urls.py
@@ -3,8 +3,7 @@ from . import views
from django.contrib.auth import views as auth_views
urlpatterns = [
- path('invitation/', views.invitation_code_view, name='invitation_code'),
- path('register/', views.register_view, name='register'),
+ path('verify//', views.verify_account, name='verify_account'),
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'),
diff --git a/accounts/utils.py b/accounts/utils.py
new file mode 100644
index 0000000..61631e1
--- /dev/null
+++ b/accounts/utils.py
@@ -0,0 +1,74 @@
+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)
diff --git a/accounts/views.py b/accounts/views.py
index b886f48..b4c00ed 100644
--- a/accounts/views.py
+++ b/accounts/views.py
@@ -3,53 +3,15 @@ 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
+from django.contrib.auth import views as auth_views, login
+from django.contrib import messages
from django.db.models import Q
from django.http import JsonResponse
-from .forms import InvitationCodeForm, CustomUserCreationForm, CustomUserChangeForm, PlayerCreationForm
-from .models import CustomUser, InvitationCode
+from .forms import CustomUserCreationForm, CustomUserChangeForm, PlayerCreationForm, PlayerVerificationForm, ParentVerificationForm
+from .models import CustomUser
+from .utils import send_verification_email
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':
@@ -74,11 +36,13 @@ class PlayerCreateView(LoginRequiredMixin, HeadCoachCheckMixin, CreateView):
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.is_active = False # Player is inactive until verified
+ player.set_unusable_password() # Password must be set via verification
+ player.verification_code = uuid.uuid4()
player.save()
-
- # Create invitation code for player
- InvitationCode.objects.create(code=str(uuid.uuid4()), user=player)
+
+ # Send verification email to player
+ send_verification_email(player, self.request, is_parent=False)
# Handle parents
for i in ['1', '2']:
@@ -96,7 +60,6 @@ class PlayerCreateView(LoginRequiredMixin, HeadCoachCheckMixin, CreateView):
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)
@@ -106,8 +69,16 @@ class PlayerCreateView(LoginRequiredMixin, HeadCoachCheckMixin, CreateView):
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)
+ parent_user, created = CustomUser.objects.get_or_create(
+ email=new_email,
+ defaults={'username': new_email, 'is_active': False}
+ )
+ if created:
+ 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)
if form.errors:
@@ -127,4 +98,32 @@ def user_search(request):
results = []
for user in users:
results.append(f"{user['last_name']}, {user['first_name']} ({user['username']})")
- return JsonResponse(results, safe=False)
\ No newline at end of file
+ return JsonResponse(results, safe=False)
+
+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('dashboard')
+ else:
+ form = FormClass()
+
+ return render(request, 'accounts/verify_account.html', {'form': form, 'is_parent': is_parent})
\ No newline at end of file
diff --git a/baseball_organisator/settings.py b/baseball_organisator/settings.py
index f8f8767..735c6a7 100644
--- a/baseball_organisator/settings.py
+++ b/baseball_organisator/settings.py
@@ -141,3 +141,6 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
AUTH_USER_MODEL = 'accounts.CustomUser'
LOGIN_REDIRECT_URL = '/'
+
+MTP_EMAIL_SEND = 0
+DEFAULT_FROM_EMAIL = 'webmaster@localhost'