From ac80c84fbd95e03677ec6aafd05cd55519b29bc0 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Tue, 3 Feb 2026 13:35:39 +0000 Subject: [PATCH] Autosave: 20260203-133539 --- core/__pycache__/admin.cpython-311.pyc | Bin 116854 -> 116874 bytes core/__pycache__/forms.cpython-311.pyc | Bin 31057 -> 33473 bytes core/__pycache__/urls.cpython-311.pyc | Bin 6501 -> 6646 bytes core/__pycache__/views.cpython-311.pyc | Bin 91994 -> 95319 bytes core/admin.py | 5 +- core/forms.py | 34 +++ core/templates/base.html | 26 ++- core/templates/core/door_visits.html | 52 ++++- core/templates/core/profile.html | 73 ++++++ core/templates/core/volunteer_list.html | 54 ++++- .../templates/core/voter_advanced_search.html | 4 + .../registration/password_change_done.html | 21 ++ .../registration/password_change_form.html | 53 +++++ core/urls.py | 1 + core/views.py | 127 ++++++++--- core/views_new.py | 111 +++++++++ door_views_update.py | 211 ++++++++++++++++++ static/css/custom.css | 1 - staticfiles/css/custom.css | 1 - 19 files changed, 718 insertions(+), 56 deletions(-) create mode 100644 core/templates/core/profile.html create mode 100644 core/templates/registration/password_change_done.html create mode 100644 core/templates/registration/password_change_form.html create mode 100644 core/views_new.py create mode 100644 door_views_update.py diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index 82d9cc3dc0f72f659d757c1447b9801fd6422669..709ba8107988806d606bddea3c5235dcbcaacfe0 100644 GIT binary patch delta 13371 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 AGXMYp diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc index ef9dbe1583e9f69aa7fbc4728d1753044d7e00d9..e5d5174226e762ab4a953b0d717f06ac8ca662c9 100644 GIT binary patch delta 8239 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 diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index f61391bdc4c183551732cc258768a9f89cab1279..c6c4cdfd7ccb52cc331e9435e6113398f6b0b57b 100644 GIT binary patch delta 781 zcmZ{i&ubGw6vuayx6Ka?(lps*mBa*pq-m-}L+m2bY-^Roln{_y5_=F@F|n96B7%sm zx1b(URxl^&L3B~UNysNg!oGibBeEGi{q`#FsIv^vDb;)(F%ss8guh5|iyALGn4pO7{#69W+l7 zP%Tg?sJ2HFjmpS=mjfNq9f`BuE5t{{FU0N;*D{E6Azs%++(SG^Y+=ezgg4AHM-c_Y zWkd&IB0hx2m-)m~|C>0^WliKd@g>)C^gwR2OrT^zi9*Sq7J0@DW^`gbiAKlP;{-Gd zGzyx18WUy*7KcrVj;fot3CvkAM~#*&u`h5#PBJ_~yhVIP{E-t(i${2h7U8>#k;Eb1 zx;Ai5n>m`9CWU|QqwX{}o?zX3#1Di&%FV_RWrQA0p5>lb`^Sm*!c-Ysj}gdQkf)Hh zTX-CO&~#s~N@K@}S|Cugph%%;Ut>)vI_^aIUC0Wbc~VJCaQ{1f|GnMn!+BF7khLI7 RA!{#l`;}enX-_%7b7*eL)2 delta 696 zcmYk4O=#0#7{{CKsZBexx^8`&sQnshTdU3woD;f+$<``lW32?{s`a9W>A^2V2jb>I zDEI*qk>Ek*X*o&95CtzgdVB*fBI03}-Cq#&H1K_111=#y9`gGq&-0M{KDBjHTh+8! zNF3JmlZAJ$G(&m0R(KXs=SQzCTnv?NtZznEqP;VvGwW{#h~lgzremf^E8@E6IO^cB zsu}P34(jq`C(*H=x7-I53O*DV6nL*to2Y2C?Ah$-IeKB9Ld%C1gBA}9%Xc}mM86)v zQVoTp7#&`jVh2oGyF#JuLz_XHN72UupLXxF>?j?drO@!9!JxtW(C-fXKG$aHs>RdzJ@7yNFQXI=YLGq9nX2r43k0@9^SPU#afI1QAqIZxTu545(G<|3?X!1eS zLxC=Nr&+nWag~DYgU!I^Nz^3JBi$rBG5OW}zi`ToXM4E5v-t!)zhh0qbUqGc71Q}9 z`A1SuX2hK_$+;xjMaj34UnGA@ZcAp!5s|S-TJR>x3BDls)CXBpk{9(aNs_|)tmNes zCLhcCeJVGMo6IS}sC(Thwtu91i$cwZ8iN|YB*I%Vtm^oZvgC~99m&^{pCo@sZtFNk x#=!i9k~bHTcgMw#_ZQ)*)jR+I diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 2acb99de2f5259e746e4d755972a2d2dc3c126e6..5afc5c83868894f75686cf4b0cb1d93df1ca39b3 100644 GIT binary patch delta 22467 zcmc(H3wTu3wg1^Olgaxr$>jZ-3FI*W0t65cc?5aCk$^giL&zCma2{dLBtV=vps10G zqUYK}TWy1{$}LtjZ9`jbeO#}_`e?$PN<1w^Z2e!Yt;VZNICmnO6dF9*%<GtWK8TJ{Jl zFQq;&voC|cdG6(&74{WW-sE2CS!G}4S#4kKSz}-0S!-YGS!ZA8S#Mv@1fAeqy7QVS zx1AfFV1n=i_^Ka!tC)X4ux}I#4uk~zCdj;MyC9k$VPfI&q9=5a6~5|6EIuGS0l^~B#l=wDE#eZuptuyUM^gh*dnv`5d#ff6 zhF@wlS_?yoaq4W4ZZwvM_K7Q@p8cBiRbzUxdQ4Al``Y|##^k?UlYgzY{t(VUaov~# z2Q&rNYYR~RYsTciLz92QnEb6{^55xX;zo#F2=!@Z!d+s@2En%JeQd?XlpMEjyUXj~ z&Rv}@4wH*1`Rm&J{@p$=4@ml&F|D z`P`k}fYZs>ao;wV+u5(rSej9`2pRxU6@+6fR9H9(FEk{X3nw|at_h53q-|2R?2b`iq?W)5i zaxdogA#@<@LfC__6+khyw|P3+id@^h4!<)HaCx`;73SdWm_d|&TD&RcZU|QNn@c`7 z7#Cpde92T<5yDto8qdL)B7_!%0D=iXN%eQOw>$lQ#o%r8IC(b2+4TH&OrMXi1i-Hc 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

cQ0NbEfLP)W@<;?EX=3(6ngCv?vrd zE$TH6uWTJO-Sit%ZQu6C+$XOeG%X!6Ee)HNLSWE-Q?K#uOw)by56&OTtO#dA(>ZceI(^NQrm2Qu%^{+H~- z+2^v%;jD@y*ACV#9m-l33TG`F%vyG?p!~@4p@O<_LET_h-NkoJ)lU{5uNX94F=VrA{B8?VFo6yloPYiCz&RvpFDF^%R6P8B5;JsbFgQn-M@ z)p(U)Vk|9eN+!G@;&H+CD0umY&%(m95w1j-gK!nXTsiv&RvJ2w;kyy;MR*C}Hwb@1 z7(w_9;UdD{5WYfSz;&LAfWLLX@n-|L8KD@V6rlm(Dg+n8%?Lk27(jR(VUWQ#gTIME zyctb5a`|5{^clhk!WRf%BH;BRx_ra&E(^!&9y|{L5ANyIn&a6ouRuT-8F>~s?hQCv z&1B(`X+ujY$E9easxXcrO=#DU61kLi6$}nIi01L9AP=-H_>aK?F`{RT4GXge6aNnj zvj!9Y53IY9eZ@jS1MrXif5X=Q7tIf3IM|S`X(n6BMg+u<^@47JF3dtBI*R0M17qe9 zL4!Nx^sTH)e!Y$5R5%%%G$J5s3R2`6z5HAos~Fh6mF4Q?&=!^=r?#`AqP2|GjR=TX zVv+7s0a3o+$;MY2wlX$%L_qvFCE5=)a+z9PvxhNwe+^O7AVuD!zn-y0Kt{r!E}gt< z3oDqqLwiLrH5ta&*VTS3KEP_Y$IY#dSzfW;DHSE$WhpS)Z;mIFvE chp|P1E^kCv%4UxU0GOV4pei0gu{4MN2h{F1kpKVy delta 19988 zcmc(Hd4N>av3H;Do_$|>_MNp^hGjre5g2w**(9hf*fet+=&^fxINgAZGxjKm(WpUA zlIFQYj1l#PxMVbwm_&UVqKSzCC5Bw1ND_@EF5|<*1mowcU)}DxcZNpuz3-22bZX8$ zYgL^(Rdwo|!*AbC(_c)B-Jh42ZKI!D-?f1a*~0cmd?bI;5{Z*zGi>%JkkjMjJV!9ilSXBZV7e!r|7LhH`EOR@Z?-3!zjHh}^f%@R=6Z4| z;SA=5@;&*X0#5;Vlol)u6?uv{ogOR>m3T@*rJmA|%j4p_j9^))+*2N^@Ko@3X0S3; z<*DLyR7=c%J~j+Ps&4>foiLXDorP?M)AG{Q4NrHdKmT$mS}8fx*h zgr<3>g{FI^htBe-F9~;yJ8=YCJ*|xOT+dwkn;)DPn(vv8E;d;6y)7TFq%4U1Diad$3gPwl@}BYRRjoyzko^W0z@-)V)V*B6ztrg5X+hB*Ass zD1ujLqX~MoF$C9JTAQ>dNf~<@20qPAtu|=m2)3DRhjO<`8-H3u&8)0VAT+xjjWPew zyaeNugEybn$3|@u4ZBI3OmMR{g@eT`;3(@NLEf@X7lu%vF?SZ)xG9SbvZwWwTFS7 zx+1+wRpsOtdt$@es9V#kb9*(%#WwfycYsFU&Ws0z$nTq&A zo)OvF*5;2ybZ3Vz==XOZQg**>nE`f-HKtn@=+(m!&0t0t1BSiSfn<#e`s;wZ< zYx`TKEx-P*Z4bB)=S@4J{#83y?CzUg_xnt7o~ju$Djx_*AgGHivQ;e~Q{AutU(Tr9 z>R&DABTYzkc7|nCE=1);>ZUOxr!GNqDVJ^wL;}P+2{R=zs9XlH9AG8D#Q>|+8)J@( zUFzwvlf*S@V61!TZpwZKGQxr)iz)S>UG7K5Hwl~)4a?kiPIBfuQz=t-cKSB?-A3qziR0g}e2smkT3`#pzxq2;nZ`}nmEnlEqjR~%D#{`XR$9Go*^7t0TZ>&lESI|mD?mRt>2O&-^p8A0Jp89ZV-7tR1KJ*NEGkImO>1W6b12CJO zL+W{ep9A~?U=+Y;fH44L0o(v4d(xRZdZs}vt*^6tQGpqC2q|V!I;9N$7`(|fs%!qJ zFA$VRsPy)}LoGXn*sW$iQrWj*`pu3U<_j;eXVgDu%o5M48RuLPL){03JWPMwWyS(y z*)UfgWI#_3`J6w9ucbhL5&~#_Sx6Ln)Qa$+_00^1< zG5|QIr~9Q0%ZQ#vOCc1|$ENYO!BEVRJI4?YAS8PM4ARy4=Z;7}j_(ty^>8V9UDR1Z zB>Y++D&L}vANK7z_bo@Yg;F#C&Jy#xP`;vWn^lnxn&n~j@T_r`$sJ0t)KQ10ikrWN z&({HX5=^3460%Uh*k`219_4=`pyvcSNaTENQ4+sMhK&RwA?67s<8?I52@O_s&3xTNo?28sqE`mc7-S$Bv`(W zu3lE*1*xgaJ2Q2I2=lb+=^AMOnuZj#xebcs0LJPCFsXn@8dPNPq{&^XU|~htN0j{! zHDTd*X^QlP+4j#Bzt0v|44ax$uGtWWZGLamx8Zy(+~H%{IKyO>041I*)*$~I*>nwG zTIljm>N`uSXv0DxnhP0B;vxS*Sq}BulHU_`Pb@tyT*K!Btv^r?U06PhF8OyVIRh1C zXmmy@QcR1n>Z~G{lf=-FH|p;ko~e(~bxL{5V|hz9lb=w=AnWpPXgH#(!nW57Jul)l{nB`{$_Hi#q%MwenigWD@rb`erFgC`kGy zMRG{peeqahLVrg&?$VTN%M?boWT}L)9Q~UPgpC-UlIm2-b6kRIW*Vkc&khEz^alf* z!(ok-r$u+F&ef&a4(fb7X7mF!W#J_Clhr>pXz#n@OYhoKO{n|;*fCC@Atitv8wV!Z zm6B+th-G&u(RyxM(C_OY-#(Ch3N2rzU&3#W?HVLi)$wi;z@^e$@*o)RS zSY|)>%#8SAflg}F3}D?0tW6?PigJ-5%Ie*uT3_`Ark>rfR1#c)9?aRy0~zT6=B)Lh$`?5TEljp4%XytX84a`rIt_h4 zM4YKVy`0prn$qsdDWt*H&~K!kCy(U1N&Ut5Qi3C|Y}jY7H`lXaBY_XgHe;KsZt<@i zuATF#$1^y5OfgINFs7t|Y?Bp+U}G%EwS_6n50-59`b!Q1(*?wXfp*v zMB9l?s2bhgkWDdF+f_jR0PCn&`_&2DORtPLEWVDRmsPyOTXP?n%isY@QOpN=kKIN2 z9aS72E%vH&!lPSugTHB#Vyrevky_g115ljSCNH3()wCDn*)Tox3)J3lb2?|9P(KUT zf58gj#gVG}e(GK?*yInlg*CsI#8%SATtc9A2kq6&y?{IajNY-H9*Mf7-ya>0`x9zM zXPx-6+Sl1s&+bW{vDHZoCr1*2JLxy_h*0~g)&XMTcfd&gq4?G)wX17Z z3+thwTW{WY$*)m8`E`K10PY50H}oDg@#;~PkXH(T6|@DSJG|aNTeySvu^kavrLMes zOvgkj)iXP#zb(+w7NyPVBxGcFZVq>l)4Ze8PgzqaOD>>pjZlnSj1d+AtN>UJa52Ct zfQJDdAwY|Q4QUh^?Ev4~+dV2)VljBE=1mPhY@29m!ydFA5AYbkYXGb;3z7N(fVs=h zHWorJa@j600_@{BHQyNWAAW+OCjp)U;9Y+K z=O=q_s8LVu%2oS!6{zDotK%%1pY5*A8!DPdcDfZr6D31MlS)W5KioY&ufig-l?pBH zYBE|>SxTx=B5JrrjU~U9Tf|C?7Il`AdM?S^QK)`;LzScPU=x2oc0(cIo;Si$aU>Uf zvcu?Nl%-@eO2ilpT0h2;KXz!exyRYcd?wwS06z%^fyv;$4pJT&)=anCO&bO`EN~QoV3vt*D}PSL&owsYy!Q zWPG|D`R&ze{7u{J`$wrGcQ+Mw=eLheHtf#dKL(jj${edcxoJYKv}=<)gf@jrCL~Js zPeelbzB1O{K1n_Ll}0gDz5JEgV^S?s*6xrcJ>7^-P>}PSo6m_Yp%E=Bmlv{S`J!|* z*4`KZwmWX5*zUj?msq{B834;xP6puBIu)reTC!;vU{zn*E$c*V^1rhx*>7QDO3-w0 zed-w$i{#kh8jBijSf&4_V#27PUQWA&N~msfNqdT9ZmHS*_MM@?h3#i|Uv z&@(#%ZC4@&t!Hfr$mnJcDZ=1d4Vjwr)pa?{2ZFVckF54Vs3wOEbJbp^_a;gQJu9TyAV0}Jf8qLAxs)G zsu#ITALEYcOkhkYeK2f^a7?a*sh(jc{P)@{w!A?d+_!+X<^R2}C##-k7Gi3I`G?t> zB6aJx)`=PF-EWN+PE~SmlQ^c%x_5jzBXAB{vE)If$!4rlSKWJKE0^6!Ij8Jv=f49L zd&)k3$~z!O7gJ7&@kYSiq1#u7b>~7E4(ZOhVQmNPk}*Y6S98BTuMX7FSpXR$K0eWN zG=HQ`209VTksfvLx7SW(rZJZ~sj}PxU>cV}4zr;8qU1N7P*d)k?P7|TU?i5pYtYQ8 zuD@?u)vHLB01QgG5u8s~FW)z2L^Vpb5zy_@PoCJ;4YD5HtOEEx3YV(d19c*%W*%sA zuR#VvfqHu4@!QTlc0=8!t~{-zQZ zUR)Vm?}LCI_{@->KhL~fQZr~AKR-^VgC&JKBQLk zZJz@|^eh<;8a|ENZjh4^=m_ZSz+?||CYPl`&os#9fG<^IWb)86cmE@# zeoR2m)%+WMTZ2(AsFOcI{*wSZmG^O1BIsyzl{mmSM5X`Lck|4yKHY0K%$9M0 z0i1YYv{H(~`UHtTJoX`V>W8ah=z*5_v-IZ-yIfd)BL+EPus7EIym95OhHd?^#x1*C z)_Q%&I|0CKX8lL;n@PyQ;O1$hfgP*%0xB4`u|4%fb=vck{Z?PqkERjTean9=?BiKv zKrtyBbgkzZY)Txz8Z3Ae!#ty|esUU9EuW)rcTq~^ZSL%uc@4mln={S4LYOM1^aM(o zDrQEKH01JzmyzQ+2R(XLBwFQ*ZjE3!4`nipz{OA1BxJ-saaDHj>h9V9J{%Ce|SFbN26p2pbxY$vFwcO%g#^A{>z* zhsy@(kd`{uRk|V zj8R$7yJOhsny16^yZB^R4UWpPcS`YHO}L)2H;g?c+_ddsadJleS4BBIz*fO+jd)`J zZfs?s$P9yFD$Nq)4Qhp$aUT3QGJ6P+582sCirokd4I*M}`7S^$7$LWH7xXA{nfpf7_eL8028Eu)QoVOm;unk-XuwpV2y%rjhj_2P(%GA)1T^_`ydZd-ef z@;zIxvbGgCv0+xX{i;lj>?*cL>*A9y6XVsAt|B#Ka=BXnVxcRm+fM&wc00SXy3@3x zJJWlzqmAm}7e}7il9{|>IWc5HiC>Rgna5{rbQL2)J7mq=!bdSr6H;s-5{J0G z$Y8`woQxAtq61Vk{sc9!&D9IGg@aq^aMUj&#+88+nE0JI>@@;E`GXYbA!(Q@=WV^OTaRuZ@pS+B<3gxFf|Aj}}ktz2I0z@qkUF%@xO7^}ltEe#tfZk;@Oe zMjvst9(A>TIa@t-WV+gSWK8Kmx~-)0SasvEvQfv1%8t3}j@6D;nJ+bwCLaIN_4bBT z#`|U9-bw)H%7>3G&0*zk%!3;Bo2Gxq`CzB8869zNuOY?C7~SZZI7|12riP5r-T5eG z{w4yCy~yAJdFaQOW0Yr{QjWBE+7vZQaF+vg?@`Swz`Gg5y9H{}D<9M9z>({m-yX1! zOhxQ77>aTH0I5H#Y`rG-HU@kLfHiI_m5?Md8bC@1@d@-~#jJ6p@oP{~YmJ2M>iwu2j8Qi#+@f0wf1oWASS%z=>7v%OLmpM6zz5D*Br?V0O;zQ^lAWh`#*mFV{v9zk4~>&uZTf%ji%ro-FWyn+%{f= zO#Dfp3SyHEY6=1(!yME?bNV%ZHXnYUq@izDg~y*Kqv))Pr8je<1ZY$3! z8RlrXLH<%*|A%^065PfAchvB743mAH0-jD5s}mp?6PvWxrxU!;I5N<)EprY7mY6T& zaz&ZR1f!ala%GP;Wgd zJEI^sf*>Z6SF|~yiR%=3CKW@^M*c>aN>D%aGKKopTh(c&DEmqE-djJQ`8@FF_rww) zyzrE3S-2s2<*gxU1Da(mdSc&$c@G<6I3-~!SWSDW!PO*hRKI^`szsNvV_+HwmPzh} z{e&j;#I|A^QcPgNLeX-yE;JtVI1Q&~7&}gFC{ku$MwMT{M4h6;lWN)< zrl}wO<@LlQ_5F3e;X*Ey{{nF)g{JBPQLE*rR7`s(bI}c>h)Eb+YROghM|9c?5?D#>7kn?!#6ZlVVQw=hzlriL3{4hGMKMURITQuAy!dVS_Xx`E8I~L#ydhb!tz6 za{c!YiQHfR_p-!E<*=yDGlPhIpl1H9(Xxw45X)mTs_m%iq!ET(EIH#sGxvC@h#y9G z;_BA3^xD8W(L6ZwoT)!et0eaRd#GC5`Z)A1YQF!gFfLh`5SycgeI>b^;-h8R(X*fLk7Fm*3%0XZT8i8b$%c394r?X z*{YbH9s6@V=>B);jr$!8bxIsU8zUqvXD@1+jywB!?6?p5>{6#TLoBD;FrB)~RK`}v zJ&QQl2oClbj{7e^@%}Gj0FSBv^O?^gN2xD1n$p36j$uoK=e-dWn*3p%oenmz$gu$R zp~?vW_L{*n!%j{7oCp9E`Opn=JV%Ji>>MJ07RVo~>VLF6-Xp|o3H62@vgHcrFXBfW zVpL#IF}PFjh8cgRgy*szs522Kfv`!0bvOJ^RAOs47nRuNJRA2qMT6KC?{$irw0y#H zRs0^OxHlae&o&VsxI~opUy&wm%J4E9*~3X;F=Y|eUncz+Vj}^)Bx#bogYpoOn@}7e zKw%HYfkU^vQm%|I$`h?ELDUEVbO3Y$Yypq}5r8PbR)AuVu`PaAo~RJ9_|ZHuDs~K6 zyHI#Nz-|I=w-F8ESu|~fsas5wcLUm-Ko?&|>PCQ@0GReANDW(9U8u~0^Hr3__5j`n zuopl9d<}qS2&*^|p+)NJ0N((J1F#w0htz)n+zx<%i@X)7R~an=?()+@wLLVEK6fsRsf66QBhE@2xo2EAOFH1Q(?!;9G8& zyW&?Bh}yIg5}URCw-<<=&KzhOG7`3vhtsX6k|X>7RVvc#Vtm|QF3uP4^v@|5KNC*P zb!SdIw^A%E-Xm_F7=>UZe!4T%ukRiiFU%78={@;$_EnEFw{m)bb+7{10w?2#-jG$s zd#i-2u^`%-%%mgme64EOF24}xqa)OW-L%Co>Mm5iZ+cty3SJ+EMB zQ515T_=zq}rffE?MoS-hY#zNx9hqM-POH5$v!^7w+^pJd)9SiQhE}FC_WJnus>P)X zQZ1{AL?)XOJ-5Rj*tB^=SmKf#hjtdio#SP65e>m7Zn8PPyhhXsx5~b{W}RE;#fe)R zmc#e1{42(;)yBKmM#hhA9_JouEF-p5G>^7rv~5Xs8N@|}#+lzyjP{%OlQm+7Lsr{W z__>Psf?BcO%~g0cSE6zi0YV`y7~bwDko#RTzQ{F7R7ekL@HTgY|s zId$UwDx-6Zz(`bsem;Gyj4!SiD<;84l6yXAyeR!3YO_58{)1O&DBU$K{#valkH;EB zW?u75oD#N$L!Gn!>5QMO7gOhPbgdd_I_Zgn5APi4y~G9RFG_&0!WEdr5W3hrG)#v9 z>dgV{A35VUHi!puxYvi#$s_TZjiOq#$1iOZw_G~b;+dM-ht-m)f=rWMf*wp0#U?z} z#HjfBO=3iB*!*lq>)!y>VlH6zbtfN_UQL;FnIF$t3Q>*-554W6=$f9UZ4GrsB&$Iv zbU6ipoyXGQN@py(bpwn85YhMtO~RGVA=!2D>=B|Ny%_~n@yR1Z=_EZf=5SC;gofA?E2VPMFQp@m-5p&BrdLn=#wH?XQkM_Gj_xHT(`y^Z?tgTI zxL9P?rOo`%MxZ}`lqeP@1Le6R^9O7MzPx}MvJ=IBbn#DSyr`g_HV1F zyi`5qNOjB6>XsvA(~g!+>s@~AetonM_L;1>`pd_PULmH&_qoM#hmaqsyRRLw_m0B; z+HoRX6gk>qeE6r6UYqt5#qS(1D$7pOCLPryY$ATwcu`)gl?>Go(bZd^0I^F2hvIP(Xa8-%}|7d?dD6O)055U5=2unO{%Zf zz)l@M;S$s14b{SxMfz|ks!mpi|2av-41fB1YHxzT|><{uI$P=aOj{Q6*ddWf%^@bp2vWQ)uP<$mCh=u$pm(^%g0+5cDrmkW;vjPR+krB^yA$14pvDbWpK2P^hu0^Hm z276th+9RKyG|}bxqy~}8sC_;K(IRsl@lRUB$6|K;qiN!+@yDl&kMiEAtRJwIYo)L}aWjqp0|A#kM@x(cGGSmmD6w;s@6dF#bQ1yYgu6%ESDBJZ**;o%6E^rHh2^ zIZ?K#vj40ZqCRtV3ZtI5qr#8W=%|&B$@y-{e8P;RXBN1WjSB}}FF%1~jS9ITbF>L$ z7RMi)BNpZOk!Jh&Q2diQqOmF!;|!}1KdV)=2v5AdRgAY^W{*#vE2dVn_k15ZsiTpT zrWxN2@t(QD-SimP1m)jrdw%8fTYkB^mmF&P#EJDfFg+_iX|8Cgrswa8d#tHDIOi^J9j+f9~5 z(_}scB;kx2H!NmkBJaoC!tqnhU8|0kP{aCWZbUyzE8chY!Fwn9Esf2wFQj(2u#c(5R zFbP$kr$)Na=wt!+pS)U>?n^+jqU^Kl#fdknUen^rE70z*7!1yn?u4sIZrHW+#krQ_ zo7}{N7-u(ZpmEwv2D)ODvBZka<>gJz9=)Kq1u*S1oOF3{VZV2QD0cSWxI}FJ+-Fqr z+zZ7b%L4^#%-QsjST(VZn+oh-24F?^I#P21Xx6OTU`(g(ylBDdR?B8S)8OV|RP6;w zv?LLce~Q0yp=h9g;(^-bMcOv|wXJl2qsx3 zlut|h$F3I73qEw0d(r9j0LAHBQ0h9Pp=hM3pMG}vi{%| zv~xb~sYd*Euc$K^ll=vZISEuuYM7YBoyqHfTQADz0)UX=hYWPHkguVDY(xv)79*z6|2NRxi`qeI1*^&0C{^V5Awo1KnI9ua_;I6Wx;-k?#h;Q7J^B^jvbY zf`JXqMqPxS^!!9t@}GjZ2$}XkSWctS>87JG%;spclOKJQN)ll+pBhKV1vudsWihM> z^KyY+m}shPXb#cS5vs?#*L*b3Pn6*;fDCM4e(A2kOz9^KHwEaiY*-rq)<6;uYOs;R zjPz#R=(Gf*mJ)~uH;NTSdKylkBm8b%jz;#cHj0@sJp-@Wu~mR*(w#L6_YAK%hc0)9 z0(ACC7joq^bUGd2EP%5CW&oT+K+mGf#uplot_Qf&aZP$?e;c;#sI%D%dTS3C ztT+BYB78@MZ@}&sV&Z@e5PfbCX67>bwzfDN-yalt{TX3V;24=~zFdgW12#ZQbI#mg zkKYy)H8tymIDfzfXhE7YuSwLnREVYl8=$2IXGV=0<(CODpD5=)mOAklC8>fWDkUJ7 zfCUj`jT=fuvUa^#PCWuz>T~8?`ZvI=bSGOBiJ1d70+#y7ir*Iz1v`vN$3%V#7LNdV d1O^)pCI*}+TY828v(S!!S>2w@;(Xp4{SQnDafbi^ diff --git a/core/admin.py b/core/admin.py index 58dc59c..2ecb813 100644 --- a/core/admin.py +++ b/core/admin.py @@ -54,6 +54,7 @@ VOTER_MAPPABLE_FIELDS = [ ('longitude', 'Longitude'), ('secondary_phone', 'Secondary Phone'), ('secondary_phone_type', 'Secondary Phone Type'), + ('door_visit', 'Door Visit'), ] EVENT_MAPPABLE_FIELDS = [ @@ -428,7 +429,7 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin): if val == "" and not created: continue # Skip empty updates for existing records unless specifically desired? # Type conversion and normalization - if field_name == "is_targeted": + if field_name in ["is_targeted", "door_visit"]: val = val.lower() in ["true", "1", "yes"] elif field_name in ["birthdate", "registration_date"]: parsed_date = None @@ -794,7 +795,7 @@ class VolunteerAdmin(BaseImportAdminMixin, admin.ModelAdmin): list_filter = ('tenant',) fields = ('tenant', 'user', 'first_name', 'last_name', 'email', 'phone', 'notes', 'interests') search_fields = ('first_name', 'last_name', 'email', 'phone') - inlines = [VolunteerEventInline, InteractionInline] + inlines = [VolunteerEventInline] filter_horizontal = ('interests',) change_list_template = "admin/volunteer_change_list.html" diff --git a/core/forms.py b/core/forms.py index 1b46fd8..f15fd6b 100644 --- a/core/forms.py +++ b/core/forms.py @@ -1,4 +1,5 @@ from django import forms +from django.contrib.auth.models import User from .models import Voter, Interaction, Donation, VoterLikelihood, InteractionType, DonationMethod, ElectionType, Event, EventParticipation, EventType, Tenant, ParticipationStatus, Volunteer, VolunteerEvent, VolunteerRole, ScheduledCall class VoterForm(forms.ModelForm): @@ -44,6 +45,7 @@ class AdvancedVoterSearchForm(forms.Form): first_name = forms.CharField(required=False) last_name = forms.CharField(required=False) + address = forms.CharField(required=False) voter_id = forms.CharField(required=False, label="Voter ID") birth_month = forms.ChoiceField(choices=MONTH_CHOICES, required=False, label="Birth Month") city = forms.CharField(required=False) @@ -310,6 +312,7 @@ class VotingRecordImportForm(forms.Form): super().__init__(*args, **kwargs) self.fields['tenant'].widget.attrs.update({'class': 'form-control form-select'}) self.fields['file'].widget.attrs.update({'class': 'form-control'}) + class DoorVisitLogForm(forms.Form): OUTCOME_CHOICES = [ ("No Answer Left Literature", "No Answer Left Literature"), @@ -337,6 +340,17 @@ class DoorVisitLogForm(forms.Form): widget=forms.Select(attrs={"class": "form-select"}), label="Candidate Support" ) + follow_up = forms.BooleanField( + required=False, + widget=forms.CheckboxInput(attrs={"class": "form-check-input"}), + label="Follow Up" + ) + follow_up_voter = forms.CharField( required=False, widget=forms.Select(attrs={"class": "form-select"}), label="Voter to Follow Up") + call_notes = forms.CharField( + widget=forms.Textarea(attrs={"class": "form-control", "rows": 2}), + required=False, + label="Call Notes" + ) class ScheduledCallForm(forms.ModelForm): class Meta: @@ -356,3 +370,23 @@ class ScheduledCallForm(forms.ModelForm): for field in self.fields.values(): field.widget.attrs.update({'class': 'form-control'}) self.fields['volunteer'].widget.attrs.update({'class': 'form-select'}) + +class UserUpdateForm(forms.ModelForm): + class Meta: + model = User + fields = ['first_name', 'last_name', 'email'] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for field in self.fields.values(): + field.widget.attrs.update({'class': 'form-control'}) + +class VolunteerProfileForm(forms.ModelForm): + class Meta: + model = Volunteer + fields = ['phone'] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for field in self.fields.values(): + field.widget.attrs.update({'class': 'form-control'}) diff --git a/core/templates/base.html b/core/templates/base.html index fd94cde..87933dd 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -47,15 +47,27 @@ {% endif %}

- Admin Panel + Admin Panel {% if user.is_authenticated %} - {{ user.username }} -
- {% csrf_token %} - -
+ {% else %} - Login + Login {% endif %}
diff --git a/core/templates/core/door_visits.html b/core/templates/core/door_visits.html index 36c23ef..8d139ce 100644 --- a/core/templates/core/door_visits.html +++ b/core/templates/core/door_visits.html @@ -72,7 +72,8 @@ data-address="{{ household.address_street }}" data-city="{{ household.city }}" data-state="{{ household.state }}" - data-zip="{{ household.zip_code }}"> + data-zip="{{ household.zip_code }}" + data-voters="{{ household.voters_json_str }}"> Log Visit @@ -217,7 +218,7 @@ {{ visit_form.notes }} -
+
{{ visit_form.candidate_support }} @@ -231,6 +232,28 @@
+ +
+ +
+
+ {{ visit_form.follow_up }} + +
+ +
+
+ + {{ form.address }} +
{{ form.birth_month }} diff --git a/core/templates/registration/password_change_done.html b/core/templates/registration/password_change_done.html new file mode 100644 index 0000000..46b9d72 --- /dev/null +++ b/core/templates/registration/password_change_done.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} +{% load static %} + +{% block content %} +
+
+
+
+
+ +
+

Password Changed!

+

Your password has been successfully updated. You can now use your new password to log in.

+ +
+
+
+
+{% endblock %} diff --git a/core/templates/registration/password_change_form.html b/core/templates/registration/password_change_form.html new file mode 100644 index 0000000..2a23bf2 --- /dev/null +++ b/core/templates/registration/password_change_form.html @@ -0,0 +1,53 @@ +{% extends "base.html" %} +{% load static %} + +{% block content %} +
+
+
+
+
+
+ +
+

Change Password

+

Secure your account by updating your password

+
+
+
+ {% csrf_token %} + {% for field in form %} +
+ + {{ field }} + {% if field.help_text %} +
{{ field.help_text }}
+ {% endif %} + {% for error in field.errors %} +
{{ error }}
+ {% endfor %} +
+ {% endfor %} +
+ + Cancel +
+
+
+
+
+
+
+ + +{% endblock %} diff --git a/core/urls.py b/core/urls.py index a6ac9a4..dd9cc0d 100644 --- a/core/urls.py +++ b/core/urls.py @@ -64,4 +64,5 @@ urlpatterns = [ path('call-queue/', views.call_queue, name='call_queue'), path('call-queue//complete/', views.complete_call, name='complete_call'), path('call-queue//delete/', views.delete_call, name='delete_call'), + path('profile/', views.profile, name='profile'), ] \ No newline at end of file diff --git a/core/views.py b/core/views.py index 084dd40..8364507 100644 --- a/core/views.py +++ b/core/views.py @@ -1,3 +1,5 @@ +from django.contrib.auth.decorators import login_required +from django.contrib.auth.forms import PasswordChangeForm from django.utils.dateparse import parse_date from datetime import datetime, time, timedelta import base64 @@ -15,7 +17,7 @@ from django.contrib import messages from django.core.paginator import Paginator from django.conf import settings from .models import Voter, Tenant, Interaction, Donation, VoterLikelihood, EventParticipation, Event, EventType, InteractionType, DonationMethod, ElectionType, CampaignSettings, Volunteer, ParticipationStatus, VolunteerEvent, Interest, VolunteerRole, ScheduledCall -from .forms import VoterForm, InteractionForm, DonationForm, VoterLikelihoodForm, EventParticipationForm, VoterImportForm, AdvancedVoterSearchForm, EventParticipantAddForm, EventForm, VolunteerForm, VolunteerEventForm, VolunteerEventAddForm, DoorVisitLogForm, ScheduledCallForm +from .forms import VoterForm, InteractionForm, DonationForm, VoterLikelihoodForm, EventParticipationForm, VoterImportForm, AdvancedVoterSearchForm, EventParticipantAddForm, EventForm, VolunteerForm, VolunteerEventForm, VolunteerEventAddForm, DoorVisitLogForm, ScheduledCallForm, UserUpdateForm, VolunteerProfileForm import logging import zoneinfo from django.utils import timezone @@ -446,6 +448,8 @@ def voter_advanced_search(request): voters = voters.filter(first_name__icontains=data['first_name']) if data.get('last_name'): voters = voters.filter(last_name__icontains=data['last_name']) + if data.get('address'): + voters = voters.filter(Q(address__icontains=data['address']) | Q(address_street__icontains=data['address'])) if data.get('voter_id'): voters = voters.filter(voter_id__icontains=data['voter_id']) if data.get('birth_month'): @@ -520,6 +524,8 @@ def export_voters_csv(request): voters = voters.filter(first_name__icontains=data['first_name']) if data.get('last_name'): voters = voters.filter(last_name__icontains=data['last_name']) + if data.get('address'): + voters = voters.filter(Q(address__icontains=data['address']) | Q(address_street__icontains=data['address'])) if data.get('voter_id'): voters = voters.filter(voter_id__icontains=data['voter_id']) if data.get('birth_month'): @@ -580,7 +586,7 @@ def voter_delete(request, voter_id): return redirect('voter_detail', voter_id=voter.id) -@role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin']) +@role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin'], permission='core.view_voter') def bulk_send_sms(request): """ Sends bulk SMS to selected voters using Twilio API. @@ -839,23 +845,23 @@ def volunteer_list(request): ) # Interest filter - interest_id = request.GET.get("interest") - if interest_id: - volunteers = volunteers.filter(interests__id=interest_id) + interest_ids = request.GET.getlist("interest") + if interest_ids: + volunteers = volunteers.filter(interests__id__in=interest_ids).distinct() + interests = Interest.objects.filter(tenant=tenant).order_by('name') + paginator = Paginator(volunteers, 50) page_number = request.GET.get('page') volunteers_page = paginator.get_page(page_number) - interests = Interest.objects.filter(tenant=tenant).order_by('name') - context = { 'tenant': tenant, 'selected_tenant': tenant, 'volunteers': volunteers_page, 'query': query, 'interests': interests, - 'selected_interest': interest_id, + 'selected_interests': interest_ids, } return render(request, 'core/volunteer_list.html', context) @@ -1113,7 +1119,7 @@ def event_remove_volunteer(request, assignment_id): messages.success(request, f"{volunteer_name} removed from event volunteers.") return redirect('event_detail', event_id=event_id) -@role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin']) +@role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin'], permission='core.view_volunteer') def volunteer_bulk_send_sms(request): """ Sends bulk SMS to selected volunteers using Twilio API. @@ -1196,6 +1202,7 @@ def volunteer_bulk_send_sms(request): return redirect('volunteer_list') @role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin'], permission='core.view_voter') + def door_visits(request): """ Manage door knocking visits. Groups unvisited targeted voters by household. @@ -1253,11 +1260,16 @@ def door_visits(request): 'longitude': float(voter.longitude) if voter.longitude else None, 'street_name_sort': street_name.lower(), 'street_number_sort': street_number_sort, - 'target_voters': [] + 'target_voters': [], + 'voters_json': [] } households_dict[key]['target_voters'].append(voter) + households_dict[key]['voters_json'].append({'id': voter.id, 'name': f"{voter.first_name} {voter.last_name}"}) households_list = list(households_dict.values()) + for h in households_list: + h['voters_json_str'] = json.dumps(h['voters_json']) + households_list.sort(key=lambda x: ( (x['neighborhood'] or '').lower(), x['street_name_sort'], @@ -1299,39 +1311,42 @@ def log_door_visit(request): """ selected_tenant_id = request.session.get("tenant_id") if not selected_tenant_id: - return redirect('index') + return redirect("index") tenant = get_object_or_404(Tenant, id=selected_tenant_id) campaign_settings, _ = CampaignSettings.objects.get_or_create(tenant=tenant) # Capture query string for redirecting back with filters - next_qs = request.POST.get('next_query_string', '') - redirect_url = reverse('door_visits') + next_qs = request.POST.get("next_query_string", "") + redirect_url = reverse("door_visits") if next_qs: redirect_url += f"?{next_qs}" # Get the volunteer linked to the current user volunteer = Volunteer.objects.filter(user=request.user, tenant=tenant).first() - if request.method == 'POST': + if request.method == "POST": form = DoorVisitLogForm(request.POST) if form.is_valid(): - address_street = request.POST.get('address_street') - city = request.POST.get('city') - state = request.POST.get('state') - zip_code = request.POST.get('zip_code') + address_street = request.POST.get("address_street") + city = request.POST.get("city") + state = request.POST.get("state") + zip_code = request.POST.get("zip_code") - outcome = form.cleaned_data['outcome'] - notes = form.cleaned_data['notes'] - wants_yard_sign = form.cleaned_data['wants_yard_sign'] - candidate_support = form.cleaned_data['candidate_support'] + outcome = form.cleaned_data["outcome"] + notes = form.cleaned_data["notes"] + wants_yard_sign = form.cleaned_data["wants_yard_sign"] + candidate_support = form.cleaned_data["candidate_support"] + follow_up = form.cleaned_data["follow_up"] + follow_up_voter_id = form.cleaned_data.get("follow_up_voter") + call_notes = form.cleaned_data["call_notes"] # Determine date/time in campaign timezone - campaign_tz_name = campaign_settings.timezone or 'America/Chicago' + campaign_tz_name = campaign_settings.timezone or "America/Chicago" try: tz = zoneinfo.ZoneInfo(campaign_tz_name) except: - tz = zoneinfo.ZoneInfo('America/Chicago') + tz = zoneinfo.ZoneInfo("America/Chicago") interaction_date = timezone.now().astimezone(tz) @@ -1352,16 +1367,21 @@ def log_door_visit(request): messages.warning(request, f"No targeted voters found at {address_street}.") return redirect(redirect_url) + # Get default caller for follow-ups + default_caller = None + if follow_up: + default_caller = Volunteer.objects.filter(tenant=tenant, is_default_caller=True).first() + for voter in voters: # 1) Update voter flags voter.door_visit = True # 2) If "Wants a Yard Sign" checkbox is selected if wants_yard_sign: - voter.yard_sign = 'wants' + voter.yard_sign = "wants" # 3) Update support status if Supporting or Not Supporting - if candidate_support in ['supporting', 'not_supporting']: + if candidate_support in ["supporting", "not_supporting"]: voter.candidate_support = candidate_support voter.save() @@ -1375,14 +1395,26 @@ def log_door_visit(request): description=outcome, notes=notes ) + + # 5) Create ScheduledCall if follow_up is checked and this is the selected voter + if follow_up and follow_up_voter_id and str(voter.id) == follow_up_voter_id: + ScheduledCall.objects.create( + tenant=tenant, + voter=voter, + volunteer=default_caller, + comments=call_notes, + status="pending" + ) - messages.success(request, f"Door visit logged for {address_street}.") + if follow_up: + messages.success(request, f"Door visit logged and follow-up call scheduled for {address_street}.") + else: + messages.success(request, f"Door visit logged for {address_street}.") else: messages.error(request, "There was an error in the visit log form.") return redirect(redirect_url) -@role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin'], permission='core.view_voter') def door_visit_history(request): """ Shows a distinct list of Door visit interactions for addresses. @@ -1474,7 +1506,7 @@ def door_visit_history(request): } return render(request, "core/door_visit_history.html", context) -@role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin']) +@role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin'], permission='core.add_scheduledcall') def schedule_call(request, voter_id): selected_tenant_id = request.session.get("tenant_id") tenant = get_object_or_404(Tenant, id=selected_tenant_id) @@ -1496,7 +1528,7 @@ def schedule_call(request, voter_id): return redirect(referer) return redirect('voter_detail', voter_id=voter.id) -@role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin']) +@role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin'], permission='core.add_scheduledcall') def bulk_schedule_calls(request): if request.method != 'POST': return redirect('voter_advanced_search') @@ -1530,7 +1562,7 @@ def bulk_schedule_calls(request): messages.success(request, f"{count} calls added to queue.") return redirect(request.META.get('HTTP_REFERER', 'voter_advanced_search')) -@role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin']) +@role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin'], permission='core.view_scheduledcall') def call_queue(request): selected_tenant_id = request.session.get("tenant_id") if not selected_tenant_id: @@ -1550,8 +1582,7 @@ def call_queue(request): } return render(request, 'core/call_queue.html', context) -@role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin']) -@role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin']) +@role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin'], permission='core.change_scheduledcall') def complete_call(request, call_id): selected_tenant_id = request.session.get("tenant_id") tenant = get_object_or_404(Tenant, id=selected_tenant_id) @@ -1589,7 +1620,7 @@ def complete_call(request, call_id): return redirect('call_queue') -@role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin']) +@role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin'], permission='core.delete_scheduledcall') def delete_call(request, call_id): selected_tenant_id = request.session.get("tenant_id") tenant = get_object_or_404(Tenant, id=selected_tenant_id) @@ -1600,3 +1631,31 @@ def delete_call(request, call_id): messages.success(request, "Call removed from queue.") return redirect('call_queue') + +@login_required +def profile(request): + try: + volunteer = request.user.volunteer_profile + except: + volunteer = None + + if request.method == 'POST': + u_form = UserUpdateForm(request.POST, instance=request.user) + v_form = VolunteerProfileForm(request.POST, instance=volunteer) if volunteer else None + + if u_form.is_valid() and (not v_form or v_form.is_valid()): + u_form.save() + if v_form: + v_form.save() + messages.success(request, f'Your profile has been updated!') + return redirect('profile') + else: + u_form = UserUpdateForm(instance=request.user) + v_form = VolunteerProfileForm(instance=volunteer) if volunteer else None + + context = { + 'u_form': u_form, + 'v_form': v_form + } + + return render(request, 'core/profile.html', context) diff --git a/core/views_new.py b/core/views_new.py new file mode 100644 index 0000000..552eea6 --- /dev/null +++ b/core/views_new.py @@ -0,0 +1,111 @@ +@role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin'], permission='core.view_voter') +def log_door_visit(request): + """ + Mark all targeted voters at a specific address as visited, update their flags, + and create interaction records. + """ + selected_tenant_id = request.session.get("tenant_id") + if not selected_tenant_id: + return redirect("index") + + tenant = get_object_or_404(Tenant, id=selected_tenant_id) + campaign_settings, _ = CampaignSettings.objects.get_or_create(tenant=tenant) + + # Capture query string for redirecting back with filters + next_qs = request.POST.get("next_query_string", "") + redirect_url = reverse("door_visits") + if next_qs: + redirect_url += f"?{next_qs}" + + # Get the volunteer linked to the current user + volunteer = Volunteer.objects.filter(user=request.user, tenant=tenant).first() + + if request.method == "POST": + form = DoorVisitLogForm(request.POST) + if form.is_valid(): + address_street = request.POST.get("address_street") + city = request.POST.get("city") + state = request.POST.get("state") + zip_code = request.POST.get("zip_code") + + outcome = form.cleaned_data["outcome"] + notes = form.cleaned_data["notes"] + wants_yard_sign = form.cleaned_data["wants_yard_sign"] + candidate_support = form.cleaned_data["candidate_support"] + follow_up = form.cleaned_data["follow_up"] + follow_up_voter_id = form.cleaned_data.get("follow_up_voter") + call_notes = form.cleaned_data["call_notes"] + + # Determine date/time in campaign timezone + campaign_tz_name = campaign_settings.timezone or "America/Chicago" + try: + tz = zoneinfo.ZoneInfo(campaign_tz_name) + except: + tz = zoneinfo.ZoneInfo("America/Chicago") + + interaction_date = timezone.now().astimezone(tz) + + # Get or create InteractionType + interaction_type, _ = InteractionType.objects.get_or_create(tenant=tenant, name="Door Visit") + + # Find targeted voters at this exact address + voters = Voter.objects.filter( + tenant=tenant, + address_street=address_street, + city=city, + state=state, + zip_code=zip_code, + is_targeted=True + ) + + if not voters.exists(): + messages.warning(request, f"No targeted voters found at {address_street}.") + return redirect(redirect_url) + + # Get default caller for follow-ups + default_caller = None + if follow_up: + default_caller = Volunteer.objects.filter(tenant=tenant, is_default_caller=True).first() + + for voter in voters: + # 1) Update voter flags + voter.door_visit = True + + # 2) If "Wants a Yard Sign" checkbox is selected + if wants_yard_sign: + voter.yard_sign = "wants" + + # 3) Update support status if Supporting or Not Supporting + if candidate_support in ["supporting", "not_supporting"]: + voter.candidate_support = candidate_support + + voter.save() + + # 4) Create interaction + Interaction.objects.create( + voter=voter, + volunteer=volunteer, + type=interaction_type, + date=interaction_date, + description=outcome, + notes=notes + ) + + # 5) Create ScheduledCall if follow_up is checked and this is the selected voter + if follow_up and follow_up_voter_id and str(voter.id) == follow_up_voter_id: + ScheduledCall.objects.create( + tenant=tenant, + voter=voter, + volunteer=default_caller, + comments=call_notes, + status="pending" + ) + + if follow_up: + messages.success(request, f"Door visit logged and follow-up call scheduled for {address_street}.") + else: + messages.success(request, f"Door visit logged for {address_street}.") + else: + messages.error(request, "There was an error in the visit log form.") + + return redirect(redirect_url) diff --git a/door_views_update.py b/door_views_update.py new file mode 100644 index 0000000..765c0e7 --- /dev/null +++ b/door_views_update.py @@ -0,0 +1,211 @@ +def door_visits(request): + """ + Manage door knocking visits. Groups unvisited targeted voters by household. + """ + 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) + + # Filters from GET parameters + district_filter = request.GET.get('district', '').strip() + neighborhood_filter = request.GET.get('neighborhood', '').strip() + address_filter = request.GET.get('address', '').strip() + + # Initial queryset: unvisited targeted voters for this tenant + voters = Voter.objects.filter(tenant=tenant, door_visit=False, is_targeted=True) + + # Apply filters if provided + if district_filter: + voters = voters.filter(district=district_filter) + if neighborhood_filter: + voters = voters.filter(neighborhood__icontains=neighborhood_filter) + if address_filter: + voters = voters.filter(Q(address_street__icontains=address_filter) | Q(address__icontains=address_filter)) + + # Grouping by household (unique address) + households_dict = {} + for voter in voters: + # Key for grouping is the unique address components + key = (voter.address_street, voter.city, voter.state, voter.zip_code) + if key not in households_dict: + # Parse street name and number for sorting + street_number = "" + street_name = voter.address_street + match = re.match(r'^(\d+)\s+(.*)$', voter.address_street) + if match: + street_number = match.group(1) + street_name = match.group(2) + + try: + street_number_sort = int(street_number) + except ValueError: + street_number_sort = 0 + + households_dict[key] = { + 'address_street': voter.address_street, + 'city': voter.city, + 'state': voter.state, + 'zip_code': voter.zip_code, + 'neighborhood': voter.neighborhood, + 'district': voter.district, + 'latitude': float(voter.latitude) if voter.latitude else None, + 'longitude': float(voter.longitude) if voter.longitude else None, + 'street_name_sort': street_name.lower(), + 'street_number_sort': street_number_sort, + 'target_voters': [], + 'voters_json': [] + } + households_dict[key]['target_voters'].append(voter) + households_dict[key]['voters_json'].append({'id': voter.id, 'name': f"{voter.first_name} {voter.last_name}"}) + + households_list = list(households_dict.values()) + for h in households_list: + h['voters_json_str'] = json.dumps(h['voters_json']) + + households_list.sort(key=lambda x: ( + (x['neighborhood'] or '').lower(), + x['street_name_sort'], + x['street_number_sort'] + )) + + # Prepare data for Google Map (all filtered households with coordinates) + map_data = [ + { + 'lat': h['latitude'], + 'lng': h['longitude'], + 'address': f"{h['address_street']}, {h['city']}, {h['state']}", + 'voters': ", ".join([f"{v.first_name} {v.last_name}" for v in h['target_voters']]) + } + for h in households_list if h['latitude'] and h['longitude'] + ] + + paginator = Paginator(households_list, 50) + page_number = request.GET.get('page') + households_page = paginator.get_page(page_number) + + context = { + 'selected_tenant': tenant, + 'households': households_page, + 'district_filter': district_filter, + 'neighborhood_filter': neighborhood_filter, + 'address_filter': address_filter, + 'map_data_json': json.dumps(map_data), + 'google_maps_api_key': getattr(settings, 'GOOGLE_MAPS_API_KEY', ''), + 'visit_form': DoorVisitLogForm(), + } + return render(request, 'core/door_visits.html', context) + +@role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin'], permission='core.view_voter') +def log_door_visit(request): + """ + Mark all targeted voters at a specific address as visited, update their flags, + and create interaction records. + """ + selected_tenant_id = request.session.get("tenant_id") + if not selected_tenant_id: + return redirect("index") + + tenant = get_object_or_404(Tenant, id=selected_tenant_id) + campaign_settings, _ = CampaignSettings.objects.get_or_create(tenant=tenant) + + # Capture query string for redirecting back with filters + next_qs = request.POST.get("next_query_string", "") + redirect_url = reverse("door_visits") + if next_qs: + redirect_url += f"?{next_qs}" + + # Get the volunteer linked to the current user + volunteer = Volunteer.objects.filter(user=request.user, tenant=tenant).first() + + if request.method == "POST": + form = DoorVisitLogForm(request.POST) + if form.is_valid(): + address_street = request.POST.get("address_street") + city = request.POST.get("city") + state = request.POST.get("state") + zip_code = request.POST.get("zip_code") + + outcome = form.cleaned_data["outcome"] + notes = form.cleaned_data["notes"] + wants_yard_sign = form.cleaned_data["wants_yard_sign"] + candidate_support = form.cleaned_data["candidate_support"] + follow_up = form.cleaned_data["follow_up"] + follow_up_voter_id = form.cleaned_data.get("follow_up_voter") + call_notes = form.cleaned_data["call_notes"] + + # Determine date/time in campaign timezone + campaign_tz_name = campaign_settings.timezone or "America/Chicago" + try: + tz = zoneinfo.ZoneInfo(campaign_tz_name) + except: + tz = zoneinfo.ZoneInfo("America/Chicago") + + interaction_date = timezone.now().astimezone(tz) + + # Get or create InteractionType + interaction_type, _ = InteractionType.objects.get_or_create(tenant=tenant, name="Door Visit") + + # Find targeted voters at this exact address + voters = Voter.objects.filter( + tenant=tenant, + address_street=address_street, + city=city, + state=state, + zip_code=zip_code, + is_targeted=True + ) + + if not voters.exists(): + messages.warning(request, f"No targeted voters found at {address_street}.") + return redirect(redirect_url) + + # Get default caller for follow-ups + default_caller = None + if follow_up: + default_caller = Volunteer.objects.filter(tenant=tenant, is_default_caller=True).first() + + for voter in voters: + # 1) Update voter flags + voter.door_visit = True + + # 2) If "Wants a Yard Sign" checkbox is selected + if wants_yard_sign: + voter.yard_sign = "wants" + + # 3) Update support status if Supporting or Not Supporting + if candidate_support in ["supporting", "not_supporting"]: + voter.candidate_support = candidate_support + + voter.save() + + # 4) Create interaction + Interaction.objects.create( + voter=voter, + volunteer=volunteer, + type=interaction_type, + date=interaction_date, + description=outcome, + notes=notes + ) + + # 5) Create ScheduledCall if follow_up is checked and this is the selected voter + if follow_up and follow_up_voter_id and str(voter.id) == follow_up_voter_id: + ScheduledCall.objects.create( + tenant=tenant, + voter=voter, + volunteer=default_caller, + comments=call_notes, + status="pending" + ) + + if follow_up: + messages.success(request, f"Door visit logged and follow-up call scheduled for {address_street}.") + else: + messages.success(request, f"Door visit logged for {address_street}.") + else: + messages.error(request, "There was an error in the visit log form.") + + return redirect(redirect_url) \ No newline at end of file diff --git a/static/css/custom.css b/static/css/custom.css index 93db4ac..a764764 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -22,7 +22,6 @@ h1, h2, h3, h4, h5, h6 { } .navbar { - background-color: #ffffff; border-bottom: 1px solid var(--border-color); padding: 1rem 0; } diff --git a/staticfiles/css/custom.css b/staticfiles/css/custom.css index 93db4ac..a764764 100644 --- a/staticfiles/css/custom.css +++ b/staticfiles/css/custom.css @@ -22,7 +22,6 @@ h1, h2, h3, h4, h5, h6 { } .navbar { - background-color: #ffffff; border-bottom: 1px solid var(--border-color); padding: 1rem 0; }