feat: Erweiterung der Spielerstellung um Elternsuche mit Autocomplete

This commit is contained in:
Matthias Nagel 2025-10-01 13:36:58 +02:00
parent 956d7a45e9
commit 4203a696d9
12 changed files with 103 additions and 11 deletions

View File

@ -28,8 +28,10 @@ class CustomUserChangeForm(forms.ModelForm):
fields = ('username', 'first_name', 'last_name', 'email', 'birth_date', 'player_number', 'team')
class PlayerCreationForm(forms.ModelForm):
parent1_email = forms.EmailField(required=False)
parent2_email = forms.EmailField(required=False)
parent1_search = forms.CharField(label="Search Parent 1 by Username/Email", required=False)
parent1_new = forms.EmailField(label="Or create new Parent 1 with Email", required=False)
parent2_search = forms.CharField(label="Search Parent 2 by Username/Email", required=False)
parent2_new = forms.EmailField(label="Or create new Parent 2 with Email", required=False)
birth_date = forms.DateField(input_formats=['%d.%m.%Y', '%Y-%m-%d'], widget=forms.DateInput(format='%d.%m.%Y', attrs={'type': 'date'}))
class Meta:

View File

@ -0,0 +1,19 @@
# Generated by Django 5.2.6 on 2025-10-01 10:00
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0003_absenceperiod'),
]
operations = [
migrations.AddField(
model_name='customuser',
name='parents',
field=models.ManyToManyField(blank=True, related_name='children', to=settings.AUTH_USER_MODEL),
),
]

View File

@ -7,6 +7,7 @@ class CustomUser(AbstractUser):
birth_date = models.DateField(null=True, blank=True)
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')
@property
def age(self):

View File

@ -17,4 +17,39 @@
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const parent1SearchInput = document.getElementById('id_parent1_search');
const parent2SearchInput = document.getElementById('id_parent2_search');
function setupAutocomplete(inputElement) {
const datalist = document.createElement('datalist');
datalist.id = inputElement.id + '_list';
inputElement.setAttribute('list', datalist.id);
document.body.appendChild(datalist); // Append to body instead of parentNode
inputElement.addEventListener('input', function() {
const query = this.value;
if (query.length < 2) {
datalist.innerHTML = '';
return;
}
fetch(`/accounts/user-search/?q=${query}`)
.then(response => response.json())
.then(data => {
datalist.innerHTML = '';
data.forEach(userString => {
const option = document.createElement('option');
option.value = userString;
datalist.appendChild(option);
});
});
});
}
setupAutocomplete(parent1SearchInput);
setupAutocomplete(parent2SearchInput);
});
</script>
{% endblock %}

View File

@ -9,4 +9,5 @@ urlpatterns = [
path('logout/', auth_views.LogoutView.as_view(), name='logout'),
path('profile/', views.edit_profile, name='edit_profile'),
path('player/add/', views.PlayerCreateView.as_view(), name='player-add'),
path('user-search/', views.user_search, name='user-search'),
]

View File

@ -4,6 +4,8 @@ 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.db.models import Q
from django.http import JsonResponse
from .forms import InvitationCodeForm, CustomUserCreationForm, CustomUserChangeForm, PlayerCreationForm
from .models import CustomUser, InvitationCode
import uuid
@ -78,14 +80,38 @@ class PlayerCreateView(LoginRequiredMixin, HeadCoachCheckMixin, CreateView):
# Create invitation code for player
InvitationCode.objects.create(code=str(uuid.uuid4()), user=player)
# Create parent users and invitation codes
for email_field in ['parent1_email', 'parent2_email']:
email = form.cleaned_data.get(email_field)
if email:
parent_user = CustomUser.objects.filter(email=email).first()
if not parent_user:
parent_user = CustomUser.objects.create(email=email, username=email, is_active=False)
# 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)
if form.errors:
return self.form_invalid(form)
return redirect(self.success_url)
@ -94,3 +120,11 @@ class MyLoginView(auth_views.LoginView):
if request.user.is_authenticated:
return redirect('dashboard')
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)

Binary file not shown.