From 4203a696d91ebdcdd8aa30e6f89518392110e97e Mon Sep 17 00:00:00 2001 From: Matthias Nagel Date: Wed, 1 Oct 2025 13:36:58 +0200 Subject: [PATCH] feat: Erweiterung der Spielerstellung um Elternsuche mit Autocomplete --- accounts/__pycache__/forms.cpython-313.pyc | Bin 3690 -> 3984 bytes accounts/__pycache__/models.cpython-313.pyc | Bin 3171 -> 3299 bytes accounts/__pycache__/urls.cpython-313.pyc | Bin 1010 -> 1102 bytes accounts/__pycache__/views.cpython-313.pyc | Bin 6092 -> 8278 bytes accounts/forms.py | 6 ++- .../migrations/0004_customuser_parents.py | 19 +++++++ .../0004_customuser_parents.cpython-313.pyc | Bin 0 -> 921 bytes accounts/models.py | 1 + accounts/templates/accounts/player_form.html | 37 ++++++++++++- accounts/urls.py | 1 + accounts/views.py | 50 +++++++++++++++--- db.sqlite3 | Bin 266240 -> 282624 bytes 12 files changed, 103 insertions(+), 11 deletions(-) create mode 100644 accounts/migrations/0004_customuser_parents.py create mode 100644 accounts/migrations/__pycache__/0004_customuser_parents.cpython-313.pyc diff --git a/accounts/__pycache__/forms.cpython-313.pyc b/accounts/__pycache__/forms.cpython-313.pyc index 36bb4d7dc53b6a134cb3348d46d572325c6225ba..0165fa4efe257be9ab59ff6644ffe086da1bc52c 100644 GIT binary patch delta 587 zcmaDQGeMs3GcPX}0}v?vy_3Phv5`-JoAJkFd2U^!hxlDuqC9w$tmmmMm?1r!kj>);6H)Mo2UJp}7@V3| zl$@aukXV$OSE693kW{GSx9U#!gl delta 275 zcmbOr|4N4MGcPX}0}xcazLW8ZZ6lumH{*uM^4z*CmW)MglMT3KIDicH7?vP`$$^Zb zoGiht`bdT5X^1L1TvL@p@=(}r-&z= zSCenD438wQn^<|bz5 zxMim1q}<{yNGwXtD=~~u1&P79MiB1g_q^t6IY4{b7;cD2UKdfhB%-n+^`eMLgD*ON mat(+5yEdVlYX~~Nfi^!EacFGWmUq_ zHCc(0w`l#e%i4x{ujumCTHVyK(Nz4dV7D3#!@>2cW@7jY%M`mw-xCzXQFzD6YyVvo zArbZg8X-^FA{rxI_R5r8)|#!G7O+@1Ox+D`XtbtdyZi>WHVo{v@FYoa$>SL)lLn*! z!QTi_!)_3jTRPj5M)#y#SIX_&dN=l7@7%6_mh|oAZ=87OW4SHV_cH;h@0mDF-f)K~ zw!0IUL1>(O3{Iw?QCD;}t;(ihtlJfB&9Z&em&xE1@$u7Wf+&0erHRR3PL9%mf?T$0 z>4t*5A#DU=s%`VRa+cb5gkqcN$+2gx$E^+@ANHT2-WBB-1_?d^I0?uBSPD0Ey{2Jr z9rzkJXGlJDYH=Pyk%DEJhSnTL=ODTOC;-j@GJrpO%EJ>7Bd<$(^$YNmBZ@os}$e;2%&xE;y#nw aXR==ydB6&23q4N_7`O)svKUFTu!di7kh&fK delta 704 zcmZ{hO=}ZT6o%&{lj&qW8k$&}riq=j5{(31NU?}*G-^O0by095beMFmZAk3YJ5x#) zwcWblSs$WMxq&lKP>gjs=56TgE8w(A5e zRN00bMgP=Lrx^mx`4yb8p7F5Bt4ov@?oKR y>7M6qb=V@mCECe)zbf!E4gfvrd{4^vq~aGTHIOyff#>;wME4+z?~G}gZ1@9=(45@> diff --git a/accounts/__pycache__/urls.cpython-313.pyc b/accounts/__pycache__/urls.cpython-313.pyc index be27102747e763e4457fe8a8b1d0afcf973610d5..f018a9f8b6aad9d38b6494ba15248249cba95bb0 100644 GIT binary patch delta 191 zcmeywevU)^GcPX}0}$N$b0?#WiGkrUhyw#WP{!w+jq2`TNKK>2ux{N#H~ wF7cZOK$GnK=11~#2QzLhgBv1$dW55-G diff --git a/accounts/__pycache__/views.cpython-313.pyc b/accounts/__pycache__/views.cpython-313.pyc index ee2fd6a2a801a9c0ba9c157aca936c007f628df0..bb6cd9e453ba2bcd68737497bcfbd1c2e8b7f52c 100644 GIT binary patch delta 3572 zcmb6cTTC0-_0IT>#}6>Jheu!_U>FBOLY7T{kc5Z;F^@E!IQxLzsbde|$k^nLfo#&& z(Q3O&ewsw{QPTZLq)IE=FVy_-(GRNbM_N_d8BxV(HrcOLKUV!n1l30EPtP6Od3C#L zujDgl?z!hV_dNWUo<}F$yDnE1g7w__WZ`rF9XCs})Sb?F0SUGVPt=RO91@UVpQw)3 z;2OXkLe+#XT8nF=e(aA1a3C7QL1W)J!A9$FU9=w88$8TJ18xvp^$0f#Zh-q{?SiKs z^$~5ba1)DKk>IUILiMb(&s5%RHg;=TVcR!bHQPLE?X#3goI$DuihtG}JEbJ5e7*`> zs}KMc`{x-xsD!AW`guyH&H-SeaxNQ_?K|py52ME!xT(KRUA?J86-UkoeG%w|`nb3UX)t0+>NlNBr+(6!#k(?w? z=klkoq~$!WC#O6F_zBT0Csrh#k)^!2l#u1s98Tdz#c6MxY$9Y|I=hn2C-UiBR!rtn zk_Zwou>~7uMgWsZC^)ikGhuOIX&w|wSX@a5G)ud4>m8Ft%Cc6fnk z*nx>8!(1FDnBfbFBTr*uh@noV0V}J)y=0ewJ|*pxsTSpl^DK2zi7;PU4Tgw9yN*!J z%780E4J!+-6L4hRb$oIVSaiP#j(}H5F_F*X^u^`8B#R;*A~IVDAfDj2D&Qm_vK_#u z=pQs?d7mmBdQ0hbFHj@OXYRM1gi7q)O<9!3?uX_vYW!p6b5D1zV2PXDNU$C@^-(~F zl{G8B^@O|11lubFOWfR9I)f6M2yVE5CaSX5xK;6bYdOIYH}{bcz{=Xwx1XDlFp&xQ^@O8~UN63kQgNl(W_r|JNl&A*#P%J_MR7X`j zY(q2TUK6z>NakgrCKBKgeI zI(0~=dUfkk0!!JvZWCoGfs+fmS+GQEa7ZoQM&4h z%TfVz*<7BR%PnV9-9WWSi-~mRYWLJ~CZAr)NE|sWm+9P(GEzd8xQi0En8Ok$FGI`nolVr9@u&O?7R73bm zYuO9SJkBjI;Tp1UE4e7^rbH@*$+guTfQb^uIgH^#BRyC#`E02=$E5r+&PplW1=U;3 zVbRbB;td{IXXbKvQG^1VC}h$pqa^B1c{!PsWLaFs8QzHpK(0|5A;g))M-tYlv|e2@ zPfVvE=HNZ-Iw0Jl5jcFFm||H>9R#( zTaM9+STj-Bv!a^D8^b$XTai1YafgarpT_lVS~o9mCbxp>+h?}9sLJfyVH=Aqud#fQ zjc9CS!@6;KBe_|tb|2qnPuw{3HxKufw{C~6zvsB?xZkB79NuP6-L`z~V|SX{?p?ck zZT-+^Kf68tls5a%QjbFii=m@h=x8x?te}OCZ7rywV`}K^nsbM3y63*@F0x^b4X+PA zVvjtnvih^HaR9^b&#B|@X+!V-^@9&pX+`^BRU2B}4qf>V{vvziI|kJ>+^Kuqaj4kQ zuXXenJI1t*v4`oW2#wQI^s`!olGHSanRJJFT+QPfg^&zxLdOrSQM;JZ@+& zHVkPELqvM^d!=WJ?4rsp66sm$pYj3t_HxWN@`|#n;yPA=#cI;1mIGG~*Gn zN5F{kg|EFTYzdc|@QAYOi&HDgncBPdQ6Tdc98tclZFP}!g%=}5nv^$S%z zXmeD|NqR?%S+EG!%8;6+dr;OCr+sJ~s(RKOH^p3}X%);TTgtc{aB~@V z9JW>(8+3lCeWWeUkQvcfag$I5EuU^fJTt9RmvvI41K@sMBvo1zVcJXJyu6yumrF|3 zTpE|DdcpDHnEI2pB!*B;Kx@#!jC%imP!a%C&bcoq-Wjx2_~I%aW{H z6H7}{Hia3m2tKMY;jIF$Ta5Cr+pr{)Qf|zVuT7){IOHUU%Do4F0&G8lpZp5|*U^sM zbIWG3}e@RHj{JP869@jTzm#stTf-$ZRu< z*H7=bz1PRS^7?O>i5&M$_gdT9HLd=DTGzFHYCXR(sU7YaR=ZDawQik%7}ie5)sYM8 zg?H58yN~SeJu{)|z&Fm|n(e-%IlEMImvJxnHpLy-DtJ?u6507&cj{udktK4ei#W^M z3;Fz#5f0r#`h<*)zYxY?Z%oxNw?GRkbCP5$b+#D6I0DE3Q<|cUIhUlC?s$UIrLOj>Zv7Cdd#V+)T*_G6jvM6-l{6MibSn?>U*;!2h=%y^L}UEd-LW! zp8M&wz>y^R2&{{b54YFkdx5ayUve**-Sg<)B{`!|CDTsZGeH{6bkL4IqI>jHOQB3B z?aXx1F5rpoTk59Wx<5*2SZ@Q2!+kNdhw~lt2LSwL5&Ml)xU8rkE)>Hjp$0*iJ8bPjYp=y|ytF^@& zC9_7uXeA+ZpweX)t{GOFY1Hyp3#NI!N;hbfrM+<>&epw=alDuEkcNRWF{eIUv4%Y5J{}zwYYu_0FCBKv6Njx-~M2g3aWz-xrjso;D z9RSDI&@q%EOtAZYQ|M(%+Z#ffU1|Hp%^ikWN}3XS*se4p%(Bm=SulAbrG;r04aCM~ z(7P7_W2WOTAPt1+1-MO~I|Ok}u#Lu<5!e>y+0TKqeh+9dvZFNzljR>Am%d?7hTdVG_7DAmo)nLe@j9Aos*DixVsB(#2_EHNw6Sqrr-p6<@l-OLMm|-+K$X zpu4A?6_K3@?)HefbK22IR`#J2NFQ0p=jJO*R^#lxBf58*Xr9Zj(^o(W1>_Ba1=bvv zbhSW@O6^SE*e#UGFW7OL)dWlAa?MFEfxYFen+B~Eb`3g&?y~(jNb{zm18C$fQln5a zEGKl5<*L_98+(?B+a!&mWj}(#EjLSLXbel-EKxAVN4ETCy;w9%Ghe4=%}vjtF_y`4 zUo97Ig0Y`Rl;12FDN%W73yz>`dcBb8PdKQJ_wz?v-rClx>G+?lPz_usgAYOsir#hozYOI8tOtz%{0}_ zfx3Lx|66zBF!5?DG1*K^wh}YV#LT1qMsd55m}w+Rcb5(owWSO+m4TKr(o{wsOgyMP zy!Gv^{f`bN7MKziGY87Me@PI=GleMOPhG8WvKdYu$Xdgz*-eH$d0CU_1=#6D1nfpS zi@?h#?p8O9GL_k4xzEnT>!Nv z(qZ;6nvn33fZ^qjvp=GPG5#@7Y{@G(m#bT)$~*9%M`5bG&yul=d;Is{wVLODL*~yu zhyGGNUo68QDH`Q+J`WSC08$fw%bflF)_|l4IaJ^v`sX(J+u;;bM+CqQ9raqTrEKgJ zDqGc2wxvGY6XCIUP0A-FpI>~S$Y5!?d9h4hIlEbxK}>Oc@|k205dQJ8PS&ZHDe_28 z6x8xwfb;5QBTXw{jj@|I2>Y=&!k+fd4u*xHMz_`&PCxQJBfvbHbP1`uL#^OIGdS>^ I0K;?m599%QY+IIqD@FnbQ8-XdD2aL?F(xEn%<@Co!hvLy$!t56LU#sdW)Iv< zOgwwxf6)KJKS8)G8BIKR;>7@?gO=YzrP)RH30ab zozb#~V9-$D3`8I@HbBE*#+=5LjY`90X2W9E9GC_VKxEE<$ZDC*f^kGr?oZ8fXLf1_ z&XsloL%JLbk&%=K73p`flp&E(!ll!p3C?+3)|7TcIuOb~11-Re2(U_IFf*!z2D4gr zv-WpEiFI45scLibzd1W{T3&PN-&|B(F(Y@yRLoki^wK*m+KEW)biS*+x7qrvmn91r1VptvAm2P~P<%x0vQ`+qukz-k7I7b2} zkf08h6+uf%5z9r zib%$hpe!888Qy#ip%V59yipPkgYp{DG+U>xRW62RB zDP4*wIbekx-Y}B13k?h9%2xEH%j!9Wr2DL#%yTxcT=jbauBrQ`VHnpAFz$Z`i@%(j S;e7CWlMCmQ3)et71;HQkD)g5C literal 0 HcmV?d00001 diff --git a/accounts/models.py b/accounts/models.py index fad17c1..1d12871 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -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): diff --git a/accounts/templates/accounts/player_form.html b/accounts/templates/accounts/player_form.html index c6fa7b1..a014ced 100644 --- a/accounts/templates/accounts/player_form.html +++ b/accounts/templates/accounts/player_form.html @@ -17,4 +17,39 @@ -{% endblock %} \ No newline at end of file + + +{% endblock %} diff --git a/accounts/urls.py b/accounts/urls.py index 2d0f7c6..f1dcba7 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -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'), ] \ No newline at end of file diff --git a/accounts/views.py b/accounts/views.py index d407eaf..b886f48 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -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) @@ -93,4 +119,12 @@ class MyLoginView(auth_views.LoginView): def get(self, request, *args, **kwargs): if request.user.is_authenticated: return redirect('dashboard') - return super().get(request, *args, **kwargs) \ No newline at end of file + 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) \ No newline at end of file diff --git a/db.sqlite3 b/db.sqlite3 index 7fee6b0962aa976d6af1ce3035d8bd8f50422ba0..dcd41b2ee22cf29fdb90ab1903c26c99d98b4cab 100644 GIT binary patch delta 1940 zcmbu9U2M}<7>3WWlh`h?PcpSlyN)&uAliXb+p*&$XsDq?{EUvDf>?{D;5bQ3f0B?N z226rd*d1dX)n+8nT5fhRkS3I=o?sVjTp~7!0fT`=LE5AVX#y@rHBGRSkcz@KR%u7F z<$UpZzvq2@^bJoqh9{h(4bDCQKm=bi>&=gr`e5Ig?H{98Lbz7D1m9|0EWXz>XU8aq zNHAxMVaG7jzP#QF<}V9tx}Gki@;N~eU1Ac!%3KpkQ3!#HXCjm0iMf3x@hfblBMMvfXRLhXAryl8fWJBqx zghCa~{hbrF3_A{R*dLn5qP1UiT*zURHt4~S=#7Qe_ib7m z-lN90MtU}e2RHO4QzIFM|K5u*@3s*h!GgdU zU~t=sS_fLf9VWWK8CC;F*gCcjM|;p*cbp89=0lyhPFuKrpc@yrmM6;Uzek|QgkVKf-ifls`ATd?}3Un znN1Hxjm8MLmal6fum$KV*vnOE`}Z7!KwpW*HW%`JKzX39(2L_=88lVZ<#eg zcf_NTE{g$OlFGwxrfCYwz$6wIuIvfB1 delta 585 zcmXw$OK1~O6o&7)lgvyWbI(lBCelh;tEAQD)v6O(6teIE8ieu@sinXv;3 zixL5UfJc$CMRtuvg;|jCXbe7rs;~~h>saA>0%lk#tDokbgCH9cgIP<-!giI~&?c4? zchdT8;Ik*-jt7=XX}!Y#OddR1UG!fNcb zn0$%xS_cLV6|hdQj&%KtbS3aRlnSn hO~HOif6XfG*Nn1R8Zi1#IelJh2!ZTYc`~^z