feat: Erweiterung der Spielerstellung um Elternsuche mit Autocomplete
This commit is contained in:
parent
956d7a45e9
commit
4203a696d9
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -28,8 +28,10 @@ class CustomUserChangeForm(forms.ModelForm):
|
|||||||
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 PlayerCreationForm(forms.ModelForm):
|
class PlayerCreationForm(forms.ModelForm):
|
||||||
parent1_email = forms.EmailField(required=False)
|
parent1_search = forms.CharField(label="Search Parent 1 by Username/Email", required=False)
|
||||||
parent2_email = forms.EmailField(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'}))
|
birth_date = forms.DateField(input_formats=['%d.%m.%Y', '%Y-%m-%d'], widget=forms.DateInput(format='%d.%m.%Y', attrs={'type': 'date'}))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
19
accounts/migrations/0004_customuser_parents.py
Normal file
19
accounts/migrations/0004_customuser_parents.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
||||||
Binary file not shown.
@ -7,6 +7,7 @@ class CustomUser(AbstractUser):
|
|||||||
birth_date = models.DateField(null=True, blank=True)
|
birth_date = models.DateField(null=True, blank=True)
|
||||||
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')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def age(self):
|
def age(self):
|
||||||
|
|||||||
@ -17,4 +17,39 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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 %}
|
{% endblock %}
|
||||||
@ -9,4 +9,5 @@ urlpatterns = [
|
|||||||
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'),
|
||||||
path('player/add/', views.PlayerCreateView.as_view(), name='player-add'),
|
path('player/add/', views.PlayerCreateView.as_view(), name='player-add'),
|
||||||
|
path('user-search/', views.user_search, name='user-search'),
|
||||||
]
|
]
|
||||||
@ -4,6 +4,8 @@ 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
|
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 .forms import InvitationCodeForm, CustomUserCreationForm, CustomUserChangeForm, PlayerCreationForm
|
||||||
from .models import CustomUser, InvitationCode
|
from .models import CustomUser, InvitationCode
|
||||||
import uuid
|
import uuid
|
||||||
@ -78,14 +80,38 @@ class PlayerCreateView(LoginRequiredMixin, HeadCoachCheckMixin, CreateView):
|
|||||||
# Create invitation code for player
|
# Create invitation code for player
|
||||||
InvitationCode.objects.create(code=str(uuid.uuid4()), user=player)
|
InvitationCode.objects.create(code=str(uuid.uuid4()), user=player)
|
||||||
|
|
||||||
# Create parent users and invitation codes
|
# Handle parents
|
||||||
for email_field in ['parent1_email', 'parent2_email']:
|
for i in ['1', '2']:
|
||||||
email = form.cleaned_data.get(email_field)
|
search_identifier = form.cleaned_data.get(f'parent{i}_search')
|
||||||
if email:
|
new_email = form.cleaned_data.get(f'parent{i}_new')
|
||||||
parent_user = CustomUser.objects.filter(email=email).first()
|
|
||||||
if not parent_user:
|
if search_identifier:
|
||||||
parent_user = CustomUser.objects.create(email=email, username=email, is_active=False)
|
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)
|
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)
|
return redirect(self.success_url)
|
||||||
|
|
||||||
@ -94,3 +120,11 @@ class MyLoginView(auth_views.LoginView):
|
|||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
return redirect('dashboard')
|
return redirect('dashboard')
|
||||||
return super().get(request, *args, **kwargs)
|
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)
|
||||||
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user