From 6b464385a5095a7fb7c616c0dfe6e02fe081e7f8 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Thu, 29 Jan 2026 21:45:13 +0000 Subject: [PATCH] Autosave: 20260129-214513 --- core/__pycache__/admin.cpython-311.pyc | Bin 100211 -> 100243 bytes core/__pycache__/forms.cpython-311.pyc | Bin 19954 -> 24654 bytes core/__pycache__/urls.cpython-311.pyc | Bin 2846 -> 4812 bytes core/__pycache__/views.cpython-311.pyc | Bin 42894 -> 59066 bytes core/admin.py | 6 +- core/forms.py | 51 +++- core/templates/base.html | 8 +- core/templates/core/event_detail.html | 249 ++++++++++++++++ core/templates/core/event_list.html | 58 ++++ core/templates/core/index.html | 7 +- core/templates/core/volunteer_detail.html | 339 ++++++++++++++++++++++ core/templates/core/volunteer_list.html | 107 +++++++ core/templates/core/voter_detail.html | 148 +++++----- core/urls.py | 18 ++ core/views.py | 287 +++++++++++++++++- 15 files changed, 1193 insertions(+), 85 deletions(-) create mode 100644 core/templates/core/event_detail.html create mode 100644 core/templates/core/event_list.html create mode 100644 core/templates/core/volunteer_detail.html create mode 100644 core/templates/core/volunteer_list.html diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index 8335e4f16501a25e87d7934160d711700673e54d..9619ec46f36d0ea9b0e156508424f627581c7f64 100644 GIT binary patch delta 6969 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 NMJ=cgdVm}~{|`NbbN~PV delta 6867 zcmb_hdsNg_9``%q$U7WR1{7v^$V+6w%J(yYMDd9Nz8FUMsgnbPcSbF=CELx+o^ow( zu03qGbGFB7wzDngbV^G*dDix*RA^3aVeYA1G_mq*(SyC8JHQYQL>7O1&i8k}-+O=G zdw<{0{eI^Qe^~$JL;A44goOnUkbjQ9)!J+?g}oI|gT#lm6erA^DL%?$aBZ^LZ4S$7 z=BTn%Z>U(@uS(oX8saSyKkLCXC}#%z44@VhC`EH=CmPUbBHmC6414 z%(H@KT?#W9|z4jezE9c$XF?Q23jjCSsXvVcq*OciAxR_W??j- zTD#rFF3DBXXS2Jp`0oJfHR^?b!eb~*gOwkaYHO9oj97dmAP|tnvx_q+hp#L)M}%O2 z0KtGD{$Oz>&EsDbAEq(<<&s3o=I@t$O>22$X=I27lL>${ez~+%e_p<=)+i6y=+>4# z8yTUZR>diTk)7soRmp>!A)X%~uG~RJ{RepXfUmAPNFVT#)zx&#y{r0e9eu>3*XKtl z(-4@P1ANS@)*GY#BSmL`1Q{@QdB0FI-@QIA;8Quqr(PWHZd#umHA)5He=$v+hB}Cg zctnu0?GoAsEm@pDdOU08+1v8<-(&b2zGYi{`VSBXU|WL#dO!qV2w)wwRR9A&vMnQQ zjvSOhuU@c-f4OaU-BOq>11txu1grp*0W5%WfK@_m#9V8QId7v@?K0u=P1FcO$`gm3jSGp+23#z(=bY&AX~LONX_YTx)<&) zM9@FEXVEo5U;p?BzcKI5e*E7N=|3Ij>|gMzm5<$*yv7^ka6x&b0=*rN%D4W6yj-g` zY_84ev{kP$YgJa8oz0PB=>xq4P}@7n-`JNx@9^{cCiIsJ`Cc+afeghC`v!S18ta7+ zLpli2Wp@oJ#&(f{@7bSu<2oFGj|&Odus<$%?_6lTgsAJ45Sy@~iv81&YB1-0NPejR zMJP-H0k;DbF)|?aL5w=7?wc9r>t#kyV!%c9?LZFi{lA;UX^q0B?#0MG(p69R#?CKe zskFMR>>toH0D315FF%katAvLS{5~Y&1ory`Kou8FJn7Yx!OeJX?jX=vEOwSxznUcL zp88kob!Uk`6>F45NjSe09n6bg%calwp4V>De*()-c++bc^hy1}bsL_ent@Qyp!6#X_tJx zv`hELyG*#V0-(sJzH3)*sydUEtv)xtv?ti|FAUKm&oZ~W^1^-!`bf&P#!5#uZ#^`ek8R1E zrq<|Ihr#%M37!a>s!c4?SOtxf`QwxO>6Wy}x_)OWtgZt@0MzLWf>b6UUnbLUv59s- z*D$MvfiJtYN^P4ySJo*L&9%Xq<{4Fve3?@wn)_AOVd*OXWHtsux9N)&5!n~~q4tD? z6LLs8qP~>RhwfZF*&f}=#Y^qUbAJ{WBf-VojuZ7m!$-?#SWg;G?4aSJ_tWT}wciMw zO&0_Gb8dOBoKqyeii1#e8;Zqzbn`K?Kg3JLm3xMZ^K!(iC1o*E!5UQ!ud12 z1cd5SfE5#zYs;k68T7tU7^Umk_pto|(B9A-L0d_$$U9twrqKv-a-wW8Y&X!DYf_&$ zW{^Dxe_U<&&OqmWIa%{%2JXRAKNMvHCC_UEGtIf0vqF*+du2$JQ?HY!EI=;Ez7T&* zrBNdkO9ysiX*Yt3bEy>D!B9bIl$zdeii(BuY$x^jkrvrelNO+&w^ks5UQ?BJa#TE< zM%g_{8i%d_S|p9aV%JO3G)(>_B)#tJnA~f2R7~i`&ya5X6eSrn%JfrwR4FmE;k^th z4AVDbi=kdR1|nhtMc#O-(*^1{9W<(}C}Jj%F~!HqN=(rtbOv_U0Q;_?WC9h*DpR~L zk(TAF#r-8qLr1c-+tz7z+ggW1c7^;=P`;!#P8=wqp$+AeXiDrb|Cv)X+!V*Y#N#7?uO$4aq4huULJ8&4*P^wA3Io59 zYV29$meQCEq+L&>X4h1RTU>Y4Xcq6SsOnoE$6LkTQW}}=`$-u9AA4`5h|f#u`Tp!? zNRnPI6pgR;0Njryv9p|v!nBNL_1ZM_p2MMKG$`up2Ur(pG zesm5Ur?+-Fy&Ina??+4MJYlo9^E5^d)lR|3$`LzO&=9(@;qeu8BN=bTgz=a#P`;w8 zQ#BEuj1%!?6i$=G=rTH%3QKvwaDnAW*`t?zmI{Zxw~0Zb%tDVvPr(bO;sx?3##13+czgp?$Snq|3cd23scPdjI z1EcuXN=at5a2FRAvjP~*1{4D3im9t8J$63i1%QQsMSvo}VzG4=k6}SvTF zv6)PCf;T2a>L+8?WKp}BenXSR^P8zgufE8=4dyL0j7+z|k0!uZfI%=12YigjRI%hv za+Eh>-EJ*+JgRPs>a-t)rE0RM{z(fSRXwV*pmJMzp%R0-UU$d^+5#ube`Rd2Yg5_0 KhUDLpBk2F_S!8Sg diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc index fdd70b94a7bf44d5acd87a7f079083ca2b017d0e..48580fa055833bee71964e05a31d87f06e3bb325 100644 GIT binary patch delta 6656 zcmb_g3vgS-6@9CpB}@J&Se6z4%K;z705?r8!ih*fq2SVE+<X;*u^t6hdbZ6FI-a4Fs^ePC^rg(oax;`)Qlo9N-{oUs3(2w%NXSieGWTi1(^hRo36Gk70F#8j3;q+N z?cVz#9Y8rB`ntA68f?aN7lI8n)c3miz;^qX&l}jz--cowZpsKvnS3`&vA=u^fS?$6 zcy_w{{05Y4M35r%A*@E&1YnZ${g~d2a67^lt^kk?P!4$`e0#+~_Jo>r2gx!Vps6U) zS@fO!iUvJahN>K210bk7w)@;8t}eIBq&3MBowvRds-HQ|t zy;E2$2|fy{g39f;J4OQTaTm|}##n)b5IOJ=rqHMDQSTn*XwZC!reNn&+9;bbpe~|?RK} z^#8ddbaTUT+p4<4#( zlms8aW=;ZZ;h8(0!hEtjOqv8@Xs|q;Cvvgii?O4$b3Am@s=B3?({XGS;UQQ~diq0) zN2JB^sCRDAvm>_O!3W$U?i~(qpxsGVR<(Sk=yJJ}v2dd+;1J~FKHM;Hw0-;x8c9c< z=b(TQ@<*tVxRj7YlFub{bNQ|0b^3d4xzR~Z5RQ8q{={#GVS*c#MRU_qdb&nemZGyq zHX-911Gv~0Kf2jw?3y4GvS%bGB+wlEi61nEY-gX77@|qejyoayy!D9umcA7b#}nSP8bmEHeQLmYzqz8?LPo5|DE)C zbwyCEUEma|J!D>AICcN`_B^&{dT3T}j_A!{eb1@97mCg`w9hrPzgj)p&=YCs*?-%7 zj(&lYW_6PHEzK`vowHcyEY>q!;URn2Vx6_z6S3TLNH(QDVvFQfMsm&JT=O4Az4ddJ z^|Kah#9~b>f1f_J>g6TQ!)I}%2;W^?z$I$kY&|tTB$Ibx1 z4(qoeuq}jI>f|}`Q~0M)Fhld34kqot2`BN-v1ZDKn|KoA8cIjMFSyq(V~mO)!@waD zq8lsg>8H(ymzcko&^*FZES@=i<6=DX61e{-^pl&4OAeuiAK+5X0K1bII|c@g5Rtjy z0^tvxZOb5;Gm1X84Ebi-w5GDh2HJ3}#f?{*Rb@L2){!3HH=e9YF@|mZxG%9@TYVZM7s;vJWz-AhE`_(O;lzl%UhV* ziq-H8S-fz~p%8&t31f~a&#{iIU0agABNbQg$O^h{Z4uS1SGCS!t5@mAyQqWkHYVcz zc$f9#>9)M|{U}+wAM~?!W2@;7X{>oc$GE^Gbl8q9x$ItUk%&3^>u*_|>o zEB4kw>#FMLbb~VJl)zJo^wP3eNi6PyYq%a8DnO`4r~rT~AiSIKE`Pw^XS(cpLW^qg zJ-8jSYz@RW**}5o-|*)z0oYs1?OpsgpKI7JpV#(=*>ArB|A}?2pa?f22;+vbox~~e zNXDdlWQ3cL(QmXE^C!qN(&TUs;vEH@y*V%2o&iA5Q&>i(FrE;f&o?Wdc zsHmfc`dW}=&_kK}pt6sbK^=HFQg$y6m45^T{3i&_2sVJ-`P<@8JH~)7`pT0SfeVT; zpVQ^>^XIWvx=4H%79?$^cX6hSw802#<7&Z3e)2p`HLYDcg1{e2`r9fh9ftIwdMO2+H{MKf@k%15M9rwn*rMziWQ^qHDkBBk0j1wj*>@r=_au}d*dvsav?%k&km zke3seTjFh@Fxg*uNwE#QS#$mu896hMoJ7@6_pPb`s%;B~l|ArVfWv07{s4qj5Wz^1g*<66sKgc9%?Tux*hw;&~i%yR|dAKA}#7wg-Uy?NFxfhtSA+axSsG`U}YB*HL5}#8;+7)%W_zy^mwRqYSRS{ d%Pulp5V2fMkXq^Si0+Bf3#{zo6*SoN{SU+@<}LsL delta 4096 zcmc&%Z){sv70*i?$BC0Tb(7}5I@PYTCo%l8(MB!tF5a)MZYR8$E z6!?H6|8mdoo_pW9zkBX^*T4IP@{dbO<##J8$_nW7-nHj-U+4MCI%V>F$8eQWP*^Zr zFy%U3FjcH6Py6JTm0E8Vb8g~FWJqcB`Puf6UHZVsx0bmw;=CEInz?e~Dl%LRbCtyT z9&pm9xJ#>uS@VGL*0PtMxV7}e*5Ybv)!^CQdhS_Ab}ictY}b)p&vql*4P-a6-Nbei z+0AS>vmGG21$I+lDmY%CwtfIDRK+eno)Xi>2MVlmSzq*u+|sbl`b*KR!kRkbgbHW@ z1Oc1mE%$;_El-qK?huFpE9hx0@@74{w&PS>5z87g7nI=;wHMrCJE%_pdH@4}5dym+ z5{V_Eh7r>eiAZEY5#z)fSXM~zJbt+PrK@tz3EXeZS6(et9+T~Bt|>F}y*29tGjKUc zpcaZJ>7QNHpL~|u0{o1%)*mQJccZ-vGQe$Rm;RMSOh9| z64>s!Xku0~#1mjX1t_=Lb%1sP+dCLf=`n4Ot|g|#Ea(P6GoS z0`iD;tnN38voS~H^`1%j#fBd?Z${S^0^2*K88M;Hn0hKHHi6zOKi$}#hE}#4X%;ob z$H5;3Y$c$5!%+4P(+(`l95?!Q0I(q}zg@9AYHAbuv^JWYnKeZ(I{E;g1cU&bN}P3{ zCsY2j@wgN8fE90QF5G^WNV`-wBGH(s&uM~Jz$t@laT4S-S_n<`EvALM8E8mzUU*K< zYT{W#GsPUdI5RT&V0-kWVMdcN%@M_8nBure;o2oQJbE-i2ZS>QVdSI@nS36wYGh3& zZODGEZZv6X)IGKydi$yF?eNGJIuFdOT=^36vOLkznWKa}u&ds>(Q$iGjc;O( zTs6eY#N|lhh135oNz>t!sfoS%Gg?BAr=+Lz{t9Yb!ZQJ%Wi@s^x~Rq_R->ZpY@hm} z%3pyoD+}^%aydg^L*DLM|6ySoH*PCTqhDMlm=K&X-+`U0ky^eaW6IH=Po64t{AX#? zGBhWvx~i@BH`ggEsB;A~1HQoO^!99&lUw_pe7Ry>?-^Fy2jH?r{%c2j`V^X(!pW7; zcA-dQ%cJUYj_0dzeGPy?VM>E#Nym%LYRs=FoKd^ntE~4^eT&(+ zK-pL?|JHXSa1I{0C*1dq^W`QuIDR;ITu*3p?>K4AjFc#43+}MsGTbfy5NeB~{8M!v z#>f!>*P7h=zTjH^z8@z~!$q*XV)N-5ot{YvGh1`$`t+*&O#k!#5RF)P6 zsAv~O6A2E?F5N=`JFoShQI8W=-dD7RZov2C*uZHg^Q=WRz9C;6tg8AEJbnzg0eGD> z@F?k@qJ0za20`A149#gtGtWYJu3H!y0{k3sm|*G6;nyGm*U|VTAOrvvqp$o)kDvDI zC3$x!kUzrpt4Fv&zCY9?UmGp$in?;R*pqp91ZsJ z^s9vMHbMaW3XN5f#!QIfV+Z-#^lSRwY~4+39E$j4txN=MI#%| zJDG4?`x^6yvzj7@!X3STdWaA>Ou!#=LwGk6!ZZ78{<{!vjI|#5^FxGi8$N(LxgmfG zt1Y&B>gj0mM9R5_x~Ef9TEdXcM;}kyrTbvukPAOK+b(YMrFkB6@lAqXpjTjW@xoE% zQ^2Xn$-rA6SP70L_X2U>^Ef?oHlg(i2#Q}###Ji!_vGJ?Hl)qMd&K?Uu;bw#?~=bi Yw$8eHtX;8oOkP&x=O(?yB19|sH&7#rTL1t6 diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 14756530b68fc5114cda9b788e2f742091a1fe05..be74b0a776a7f5bde93e9a4633a93232c74324fe 100644 GIT binary patch literal 4812 zcmb7{Nl+Zg6^5(fwE(e$kl2OT5o#L5pkX9H3yT&IBQ(OoFjBkds(_YyA+j3;WlNr; z4s)GIztarO2qm>tXgw6tj1r?XS&}MUFAZs1 zT8U zRU=idsG0z(7O4tF)df)XNXhXc(r5^v8j-40eO(Bknvkke)Wrbm5>hxX?qxIwP%TK+ zD5^DpYD214QSAX#2U2y4>I|SRBUP`ct^n!^QVojg4xoCFYE)Ej0M&=o1x57-Pyg7jMgpiPQZ0%a4WP!5YE{(r0BRhmHbs3FKusXk zuBaOU)aOWbC~7i*`U0s=McoXbZXtD9QBwibG*VrPnhBt8BXvblcLJ!pNOddfUH~LzK_|R%9VlQbUhIQ+r2S~SN+4Z{0EgNP|Pb96j z;*@D5aw-g}SRKVd0|zN*M)Rsys#=}J;Vm;0&lpHoyFPeztG_so<83lMo=arXQQa_b z!0{d{uv+~E6?c9L3^H6dkUIK0({iu?EZ)+OtidguY28!YVIz~)Pra=oZ+(;1<(J&2 z`LzNktEH2^V0HVYA`N`sYCYe4)aonlSTgb2OeVH78N(gO8glK_X!V|!-KU+_3mmMK z4xUO@|7p2sGvCd6&&^ZZ8Y%9mxo4(xksY1o67dA?QZ@CL$YZI-&ZyRCu_kiOpWzC^ zXqI6V>+Aae(U%`vs~20b{>r40&I4DK=fOqKD1CJ8@-(S(%ig~1FHWhL(m8h~o7`43 zIt7{)^_myh+!;~^8rp(EalMMkp;1gaXN~>u9r+{D?7HNQOJQe?-lTgc;HxZd?0mg6 zdZ%BEY*vljoMtwcb%U((>oJVe>hWt?HzdC*qw$@nRaVzxRqkbyyLh0;^SMS<+%vjX z)x9U;0p-#1k$9e|#@>1A%X04UF!t--{NYCYsTRJQXBx_*bNHtl>e(#*7DeO`NAq~b z`_FX+cZ*nK%8v2K7Af3U?r@#f<$y6WmD$51xu7f8!KSaHjMHXMrjG7tb@NB^!mvr_|+V^PEez+Fh&ZNxf?k290-Ke>5#&>fW7JZXpud_RP+>FLE z3}qaFk)4Ad%BNBpV>fBeurX{*iA45q_|E2(hC-oFT01}c`J^@SvmXZ{wI!dR{F}M= z4ewXK-8g*1VZep~0Rv7cO8+E@R}3GmipV|waGk@34I2VBoG_}+IW1g)OR zO>vmCVN$@PQ$}jJRCz`Bhk4O^n=icJ@Y05t0$w^5q@VZnj>BDX`39eU!ePaR6#*+w zCFxb3-gdYuI!2Fw&EdWc_XXT{s!6W# zio=o(O9GahdXgJFdGM%DT%Gzj!{Mn7PX#=68cDw3$wNmIVtD3bjKiu8s{&S?CX$;y zdF*&Xj6dLC{(^&UgDyaKT1am#)F1LEn;hac#0A8iHqzTYef)S;Og!e#wmBqhNC-$c z9i(@8`p9uujNbcuCxf=S~mFy`CI7+7;0`zW6nV z7dE^Q@WSaMebCcKkNd>+1^&2TVunb+>gmzrCNYNkOAav`Vgh2$FzMGkT}JG5VzKNY@fq{4F>`aoH5d`d-|=DRWZH6H*s#>+VEDuTW6f~&pdtl zWLMmdIhwW=nk~Vv@Lb8i<^4bM?o1GTQ^G~o;4M<8ysmz{NTx}i@njjvGG9${NZF7Q zkaBL5ekbGsH&5op6o!KPW8a2-0sGF~5E1u$V(P>cGhg%gTMqARcqibUGb@QXpXmG$ z7G0n7nU&MEHZO@sKGAYGA=)CxxRhsYm=!SVEJ|R>2jqH~=Su}M_E-|jF7dwOFf5v{ z@$p#>b2iKgm~+0Az!SfV*28(x9zD)*Sg>J1z=E?Pfu}wom;7D+pkT9qC5dM~A=eGA zdY!|j4VwZsomENv%qMP~gvI1DzLDjSvmqxS=RB9hnor2#TIc!!2g?RafaUz0h;J(C Jk45n)`!7;9b;JMw delta 629 zcmXw$&1+LZ6vflzM4Hw-ic zEaIZi2jW6p*0*r+&`p-Xl|mPH;4Zol5!|>F^dE2%#ykADGxu=r9cFeT?}D-Se*c6^ z#_NyITA6W|>*w(BKf-GB_pHZ^9WgVyc~3WA>1JOyzv~vpbZbVEmpqodm3)@`l4x$- zIxSh3^dviyUCAFw$fH|nNyDR0+zGCS8;KjP_~NG>?|pc##iy-r`(Ice{JF{Fa{8F$ zVcBg#I~m&N^7?wKTL-s{mm6=M5TXK6Ms&~0YoYKS#-(<6`ywCG?G-{wAjL?@vbC)R zJ;Us%Oj=-c{;dVNBOCcn*j|pM`p*ejfh;4d>M=XQGb@8Mp&(FT6jVKKCwVqKs1ghT zgJJv_jwfHjN%GPqx_+Bb6R0t2$}kcxQ+|_B7pODpBcV&aCCA`oj&9y1v;V11p6XwwJWy~*Kva7=#YKNu|6_y7O^ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 0f3b89b43ff6cffdd1f6b1c0e2a5d755f88d0197..86959305481addee854aa151a6b99d5ec855fab7 100644 GIT binary patch delta 16310 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

&Xr@edTu;8(Ru~K@Sj|oCf;ZBk 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 Voters +

+
Admin Panel @@ -105,4 +111,4 @@ }); - \ No newline at end of file + diff --git a/core/templates/core/event_detail.html b/core/templates/core/event_detail.html new file mode 100644 index 0000000..3058175 --- /dev/null +++ b/core/templates/core/event_detail.html @@ -0,0 +1,249 @@ +{% extends "base.html" %} +{% load static %} + +{% block content %} +
+
+ +
+

{{ event.name|default:event.event_type }}

+
+ Edit Event Info + +
+
+
+ +
+ +
+
+
+
Event Details
+
+ + {{ event.event_type }} +
+
+ + {{ event.date|date:"F d, Y" }} +
+
+ + + {% if event.start_time %} + {{ event.start_time|time:"g:i A" }} + {% if event.end_time %} - {{ event.end_time|time:"g:i A" }}{% endif %} + {% else %} + Not specified + {% endif %} + +
+
+ +

{{ event.description|default:"No description provided." }}

+
+
+
+
+ + +
+
+
+
Participants ({{ participations.count }})
+
+
+ + + + + + + + + + {% for p in participations %} + + + + + + {% empty %} + + + + {% endfor %} + +
Voter NameStatusActions
+ + {{ p.voter.first_name }} {{ p.voter.last_name }} + + + + {{ p.participation_status.name }} + + +
+ + +
+
+ No participants yet. +
+
+
+
+
+
+ + + + + +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/event_list.html b/core/templates/core/event_list.html new file mode 100644 index 0000000..369fd5d --- /dev/null +++ b/core/templates/core/event_list.html @@ -0,0 +1,58 @@ +{% extends "base.html" %} + +{% block content %} +
+
+

Campaign Events

+ +
+ +
+
+ + + + + + + + + + + + {% for event in events %} + + + + + + + + {% empty %} + + + + {% endfor %} + +
Event NameTypeDateTimeActions
+ + {{ event.name|default:event.event_type }} + + {{ event.event_type }}{{ event.date|date:"M d, Y" }} + {% if event.start_time %} + {{ event.start_time|time:"g:i A" }} + {% if event.end_time %} - {{ event.end_time|time:"g:i A" }}{% endif %} + {% else %} + - + {% endif %} + + View Details +
+

No events found for this campaign.

+
+
+
+
+{% endblock %} diff --git a/core/templates/core/index.html b/core/templates/core/index.html index e942b7f..bb77b59 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -191,13 +191,14 @@
-
+
Upcoming Events
+ View All
{% for event in upcoming_events %} -
+
{{ event.name|default:event.event_type }}
@@ -207,7 +208,7 @@
{{ event.date|date:"M d, Y" }}
-
+
{% empty %}
No upcoming events. diff --git a/core/templates/core/volunteer_detail.html b/core/templates/core/volunteer_detail.html new file mode 100644 index 0000000..f31a821 --- /dev/null +++ b/core/templates/core/volunteer_detail.html @@ -0,0 +1,339 @@ +{% extends "base.html" %} + +{% block head %} + + + + +{% endblock %} + +{% block content %} +
+
+ +
+

{% if volunteer %}{{ volunteer.first_name }} {{ volunteer.last_name }}{% else %}New Volunteer{% endif %}

+ {% if volunteer %} +
+ {% csrf_token %} + +
+ {% endif %} +
+
+ +
+ +
+
+
+
Volunteer Information
+
+ {% csrf_token %} +
+
+ + {{ form.first_name }} +
+
+ + {{ form.last_name }} +
+
+ + {{ form.email }} +
+
+ + {{ form.phone }} +
+
+
+ +
+ + +
+
+ {{ form.interests }} +
+
+
+ Cancel + +
+
+
+
+
+ + {% if volunteer %} + +
+
+
+
Event Assignments
+ +
+
+ + + + + + + + + + {% for assignment in assignments %} + + + + + + {% empty %} + + + + {% endfor %} + +
EventRoleAction
+ + {{ assignment.event.name|default:assignment.event.event_type }} + +
{{ assignment.event.date|date:"M d, Y" }}
+
{{ assignment.role }} +
+ {% csrf_token %} + +
+
+ No events assigned yet. +
+
+
+
+ {% endif %} +
+
+ + + + + + + +{% if volunteer %} + + +{% endif %} + + +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/volunteer_list.html b/core/templates/core/volunteer_list.html new file mode 100644 index 0000000..a40995a --- /dev/null +++ b/core/templates/core/volunteer_list.html @@ -0,0 +1,107 @@ +{% extends "base.html" %} + +{% block content %} +
+
+

Volunteer Directory

+ +
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ + + + + + + + + + + + {% for volunteer in volunteers %} + + + + + + + + {% empty %} + + + + {% endfor %} + +
NameEmailPhoneInterestsActions
+ + {{ volunteer.first_name }} {{ volunteer.last_name }} + + {{ volunteer.email }}{{ volunteer.phone|default:"-" }} + {% for interest in volunteer.interests.all %} + {{ interest.name }} + {% empty %} + No interests listed + {% endfor %} + + View & Edit +
+

No volunteers found matching your search.

+ Add the first volunteer +
+
+ + {% if volunteers.paginator.num_pages > 1 %} + + {% endif %} +
+
+{% endblock %} diff --git a/core/templates/core/voter_detail.html b/core/templates/core/voter_detail.html index 3e4c973..4e4c1d2 100644 --- a/core/templates/core/voter_detail.html +++ b/core/templates/core/voter_detail.html @@ -204,13 +204,13 @@ @@ -260,77 +260,6 @@
- -
-
-
- - - - - - - - - - {% for record in voting_records %} - - - - - - {% empty %} - - {% endfor %} - -
Election DateDescriptionParty
{{ record.election_date|date:"M d, Y" }}{{ record.election_description }}{{ record.primary_party|default:"-" }}
No voting records found.
-
-
-
- - -
-
-
-
Donation History
- -
-
- - - - - - - - - - - {% for donation in donations %} - - - - - - - {% empty %} - - {% endfor %} - -
DateMethodAmountActions
{{ donation.date|date:"M d, Y" }}{{ donation.method.name }}${{ donation.amount }} - - -
No donations recorded.
-
-
-
-
@@ -385,6 +314,77 @@
+ + +
+
+
+
Donation History
+ +
+
+ + + + + + + + + + + {% for donation in donations %} + + + + + + + {% empty %} + + {% endfor %} + +
DateMethodAmountActions
{{ donation.date|date:"M d, Y" }}{{ donation.method.name }}${{ donation.amount }} + + +
No donations recorded.
+
+
+
+ + +
+
+
+ + + + + + + + + + {% for record in voting_records %} + + + + + + {% empty %} + + {% endfor %} + +
Election DateDescriptionParty
{{ record.election_date|date:"M d, Y" }}{{ record.election_description }}{{ record.primary_party|default:"-" }}
No voting records found.
+
+
+
@@ -1085,4 +1085,4 @@ } }); -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/core/urls.py b/core/urls.py index 39ae92f..aa0c522 100644 --- a/core/urls.py +++ b/core/urls.py @@ -28,4 +28,22 @@ urlpatterns = [ path('voters//event-participation/add/', views.add_event_participation, name='add_event_participation'), path('event-participation//edit/', views.edit_event_participation, name='edit_event_participation'), path('event-participation//delete/', views.delete_event_participation, name='delete_event_participation'), + + # Event Detail and Participant Management + path('events/', views.event_list, name='event_list'), + path('events//', views.event_detail, name='event_detail'), + path('events//participant/add/', views.event_add_participant, name='event_add_participant'), + path('events/participant//edit/', views.event_edit_participant, name='event_edit_participant'), + path('events/participant//delete/', views.event_delete_participant, name='event_delete_participant'), + path('voters/search/json/', views.voter_search_json, name='voter_search_json'), + + # Volunteer Management + path('interests/add/', views.interest_add, name='interest_add'), + path('interests//delete/', views.interest_delete, name='interest_delete'), + path('volunteers/', views.volunteer_list, name='volunteer_list'), + path('volunteers/add/', views.volunteer_add, name='volunteer_add'), + path('volunteers//', views.volunteer_detail, name='volunteer_detail'), + path('volunteers//delete/', views.volunteer_delete, name='volunteer_delete'), + path('volunteers//assign-event/', views.volunteer_assign_event, name='volunteer_assign_event'), + path('volunteers/assignment//remove/', views.volunteer_remove_event, name='volunteer_remove_event'), ] diff --git a/core/views.py b/core/views.py index 7013db4..f902649 100644 --- a/core/views.py +++ b/core/views.py @@ -10,8 +10,8 @@ from django.shortcuts import render, redirect, get_object_or_404 from django.db.models import Q, Sum from django.contrib import messages from django.core.paginator import Paginator -from .models import Voter, Tenant, Interaction, Donation, VoterLikelihood, EventParticipation, Event, EventType, InteractionType, DonationMethod, ElectionType, CampaignSettings, Volunteer -from .forms import VoterForm, InteractionForm, DonationForm, VoterLikelihoodForm, EventParticipationForm, VoterImportForm, AdvancedVoterSearchForm +from .models import Voter, Tenant, Interaction, Donation, VoterLikelihood, EventParticipation, Event, EventType, InteractionType, DonationMethod, ElectionType, CampaignSettings, Volunteer, ParticipationStatus, VolunteerEvent, Interest +from .forms import VoterForm, InteractionForm, DonationForm, VoterLikelihoodForm, EventParticipationForm, VoterImportForm, AdvancedVoterSearchForm, EventParticipantAddForm, EventForm, VolunteerForm, VolunteerEventForm import logging from django.utils import timezone @@ -661,4 +661,285 @@ def bulk_send_sms(request): fail_count += 1 messages.success(request, f"Bulk SMS process completed: {success_count} successful, {fail_count} failed/skipped.") - return redirect('voter_advanced_search') \ No newline at end of file + return redirect('voter_advanced_search') + +def event_list(request): + selected_tenant_id = request.session.get("tenant_id") + if not selected_tenant_id: + messages.warning(request, "Please select a campaign first.") + return redirect("index") + + tenant = get_object_or_404(Tenant, id=selected_tenant_id) + events = Event.objects.filter(tenant=tenant).order_by('-date') + + context = { + 'tenant': tenant, + 'events': events, + 'selected_tenant': tenant, + } + return render(request, 'core/event_list.html', context) + +def event_detail(request, event_id): + selected_tenant_id = request.session.get("tenant_id") + if not selected_tenant_id: + messages.warning(request, "Please select a campaign first.") + return redirect("index") + + tenant = get_object_or_404(Tenant, id=selected_tenant_id) + event = get_object_or_404(Event, id=event_id, tenant=tenant) + participations = event.participations.all().select_related('voter', 'participation_status').order_by('voter__last_name', 'voter__first_name') + + # Form for adding a new participant + add_form = EventParticipantAddForm(tenant=tenant) + participation_statuses = ParticipationStatus.objects.filter(tenant=tenant, is_active=True) + + context = { + 'tenant': tenant, + 'selected_tenant': tenant, + 'event': event, + 'participations': participations, + 'add_form': add_form, + 'participation_statuses': participation_statuses, + } + return render(request, 'core/event_detail.html', context) + +def event_add_participant(request, event_id): + tenant_id = request.session.get("tenant_id") + tenant = get_object_or_404(Tenant, id=tenant_id) + event = get_object_or_404(Event, id=event_id, tenant=tenant) + + if request.method == 'POST': + form = EventParticipantAddForm(request.POST, tenant=tenant) + if form.is_valid(): + participation = form.save(commit=False) + participation.event = event + if not EventParticipation.objects.filter(event=event, voter=participation.voter).exists(): + participation.save() + messages.success(request, f"{participation.voter} added to event.") + else: + messages.warning(request, "Voter is already a participant.") + else: + messages.error(request, "Error adding participant.") + + return redirect('event_detail', event_id=event.id) + +def event_edit_participant(request, participation_id): + tenant_id = request.session.get("tenant_id") + tenant = get_object_or_404(Tenant, id=tenant_id) + participation = get_object_or_404(EventParticipation, id=participation_id, event__tenant=tenant) + + if request.method == 'POST': + status_id = request.POST.get('participation_status') + if status_id: + status = get_object_or_404(ParticipationStatus, id=status_id, tenant=tenant) + participation.participation_status = status + participation.save() + messages.success(request, f"Participation updated for {participation.voter}.") + else: + messages.error(request, "Invalid status.") + + return redirect('event_detail', event_id=participation.event.id) + +def event_delete_participant(request, participation_id): + tenant_id = request.session.get("tenant_id") + tenant = get_object_or_404(Tenant, id=tenant_id) + participation = get_object_or_404(EventParticipation, id=participation_id, event__tenant=tenant) + event_id = participation.event.id + voter_name = str(participation.voter) + participation.delete() + messages.success(request, f"{voter_name} removed from event.") + return redirect('event_detail', event_id=event_id) + +def voter_search_json(request): + """ + JSON endpoint for voter search, used by autocomplete/search UI. + """ + selected_tenant_id = request.session.get("tenant_id") + if not selected_tenant_id: + return JsonResponse({"results": []}) + + query = request.GET.get("q", "").strip() + if len(query) < 2: + return JsonResponse({"results": []}) + + tenant = get_object_or_404(Tenant, id=selected_tenant_id) + voters = Voter.objects.filter(tenant=tenant) + + search_filter = Q(first_name__icontains=query) | Q(last_name__icontains=query) | Q(voter_id__icontains=query) + + if "," in query: + parts = [p.strip() for p in query.split(",") ] + if len(parts) >= 2: + search_filter |= Q(last_name__icontains=parts[0], first_name__icontains=parts[1]) + + results = voters.filter(search_filter).order_by("last_name", "first_name")[:20] + + data = [] + for v in results: + data.append({ + "id": v.id, + "text": f"{v.last_name}, {v.first_name} ({v.voter_id})", + "address": v.address, + "phone": v.phone + }) + + return JsonResponse({"results": data}) + +def volunteer_list(request): + selected_tenant_id = request.session.get("tenant_id") + if not selected_tenant_id: + messages.warning(request, "Please select a campaign first.") + return redirect("index") + + tenant = get_object_or_404(Tenant, id=selected_tenant_id) + volunteers = Volunteer.objects.filter(tenant=tenant).order_by('last_name', 'first_name') + + # Simple search + query = request.GET.get("q") + if query: + volunteers = volunteers.filter( + Q(first_name__icontains=query) | Q(last_name__icontains=query) | Q(email__icontains=query) + ) + + paginator = Paginator(volunteers, 50) + page_number = request.GET.get('page') + volunteers_page = paginator.get_page(page_number) + + context = { + 'tenant': tenant, + 'selected_tenant': tenant, + 'volunteers': volunteers_page, + 'query': query, + } + return render(request, 'core/volunteer_list.html', context) + +def volunteer_add(request): + selected_tenant_id = request.session.get("tenant_id") + if not selected_tenant_id: + messages.warning(request, "Please select a campaign first.") + return redirect("index") + tenant = get_object_or_404(Tenant, id=selected_tenant_id) + + if request.method == 'POST': + form = VolunteerForm(request.POST, tenant=tenant) + if form.is_valid(): + volunteer = form.save(commit=False) + volunteer.tenant = tenant + volunteer.save() + form.save_m2m() # Save interests + messages.success(request, f"Volunteer {volunteer} added successfully.") + return redirect('volunteer_detail', volunteer_id=volunteer.id) + else: + form = VolunteerForm(tenant=tenant) + + context = { + 'form': form, + 'tenant': tenant, + 'selected_tenant': tenant, + } + return render(request, 'core/volunteer_detail.html', context) + +def volunteer_detail(request, volunteer_id): + selected_tenant_id = request.session.get("tenant_id") + if not selected_tenant_id: + messages.warning(request, "Please select a campaign first.") + return redirect("index") + tenant = get_object_or_404(Tenant, id=selected_tenant_id) + volunteer = get_object_or_404(Volunteer, id=volunteer_id, tenant=tenant) + + if request.method == 'POST': + form = VolunteerForm(request.POST, instance=volunteer, tenant=tenant) + if form.is_valid(): + form.save() + messages.success(request, f"Volunteer {volunteer} updated successfully.") + return redirect('volunteer_detail', volunteer_id=volunteer.id) + else: + form = VolunteerForm(instance=volunteer, tenant=tenant) + + assignments = volunteer.event_assignments.all().select_related('event') + assign_form = VolunteerEventForm(tenant=tenant) + + context = { + 'volunteer': volunteer, + 'form': form, + 'assignments': assignments, + 'assign_form': assign_form, + 'tenant': tenant, + 'selected_tenant': tenant, + } + return render(request, 'core/volunteer_detail.html', context) + +def volunteer_delete(request, volunteer_id): + selected_tenant_id = request.session.get("tenant_id") + if not selected_tenant_id: + messages.warning(request, "Please select a campaign first.") + return redirect("index") + tenant = get_object_or_404(Tenant, id=selected_tenant_id) + volunteer = get_object_or_404(Volunteer, id=volunteer_id, tenant=tenant) + + if request.method == 'POST': + volunteer.delete() + messages.success(request, "Volunteer deleted.") + return redirect('volunteer_list') + return redirect('volunteer_detail', volunteer_id=volunteer_id) + +def volunteer_assign_event(request, volunteer_id): + selected_tenant_id = request.session.get("tenant_id") + if not selected_tenant_id: + messages.warning(request, "Please select a campaign first.") + return redirect("index") + tenant = get_object_or_404(Tenant, id=selected_tenant_id) + volunteer = get_object_or_404(Volunteer, id=volunteer_id, tenant=tenant) + + if request.method == 'POST': + form = VolunteerEventForm(request.POST, tenant=tenant) + if form.is_valid(): + assignment = form.save(commit=False) + assignment.volunteer = volunteer + assignment.save() + messages.success(request, f"Assigned to {assignment.event}.") + else: + messages.error(request, "Error assigning to event.") + + return redirect('volunteer_detail', volunteer_id=volunteer.id) + +def volunteer_remove_event(request, assignment_id): + selected_tenant_id = request.session.get("tenant_id") + if not selected_tenant_id: + messages.warning(request, "Please select a campaign first.") + return redirect("index") + tenant = get_object_or_404(Tenant, id=selected_tenant_id) + assignment = get_object_or_404(VolunteerEvent, id=assignment_id, volunteer__tenant=tenant) + volunteer_id = assignment.volunteer.id + assignment.delete() + messages.success(request, "Assignment removed.") + return redirect('volunteer_detail', volunteer_id=volunteer_id) + +def interest_add(request): + selected_tenant_id = request.session.get("tenant_id") + if not selected_tenant_id: + return JsonResponse({'success': False, 'error': 'No campaign selected.'}) + + tenant = get_object_or_404(Tenant, id=selected_tenant_id) + if request.method == 'POST': + name = request.POST.get('name', '').strip() + if name: + interest, created = Interest.objects.get_or_create(tenant=tenant, name=name) + if created: + return JsonResponse({'success': True, 'id': interest.id, 'name': interest.name}) + else: + return JsonResponse({'success': False, 'error': 'Interest already exists.'}) + return JsonResponse({'success': False, 'error': 'Invalid request.'}) + +def interest_delete(request, interest_id): + selected_tenant_id = request.session.get("tenant_id") + if not selected_tenant_id: + return JsonResponse({'success': False, 'error': 'No campaign selected.'}) + + tenant = get_object_or_404(Tenant, id=selected_tenant_id) + interest = get_object_or_404(Interest, id=interest_id, tenant=tenant) + + if request.method == 'POST': + interest.delete() + return JsonResponse({'success': True}) + return JsonResponse({'success': False, 'error': 'Invalid request.'})