From 181163257f9491e5b05d6c4e31de0e22b5239731 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Fri, 30 Jan 2026 04:40:56 +0000 Subject: [PATCH] Autosave: 20260130-044056 --- core/__pycache__/admin.cpython-311.pyc | Bin 100243 -> 101789 bytes core/__pycache__/forms.cpython-311.pyc | Bin 24654 -> 26742 bytes core/__pycache__/models.cpython-311.pyc | Bin 27380 -> 27966 bytes core/__pycache__/urls.cpython-311.pyc | Bin 4812 -> 5563 bytes core/__pycache__/views.cpython-311.pyc | Bin 59066 -> 69651 bytes core/admin.py | 81 ++++--- core/forms.py | 26 +- core/migrations/0028_volunteer_notes.py | 18 ++ ...ress_event_city_event_latitude_and_more.py | 43 ++++ core/migrations/0030_event_location_name.py | 18 ++ .../0028_volunteer_notes.cpython-311.pyc | Bin 0 -> 826 bytes ...ty_event_latitude_and_more.cpython-311.pyc | Bin 0 -> 1586 bytes .../0030_event_location_name.cpython-311.pyc | Bin 0 -> 855 bytes core/models.py | 10 +- core/templates/core/event_detail.html | 188 ++++++++++++++- core/templates/core/event_edit.html | 135 +++++++++++ core/templates/core/event_list.html | 14 +- core/templates/core/volunteer_detail.html | 4 + core/templates/core/volunteer_list.html | 120 +++++++++- core/templates/core/voter_detail.html | 15 ++ core/urls.py | 8 +- core/views.py | 222 +++++++++++++++++- 22 files changed, 855 insertions(+), 47 deletions(-) create mode 100644 core/migrations/0028_volunteer_notes.py create mode 100644 core/migrations/0029_event_address_event_city_event_latitude_and_more.py create mode 100644 core/migrations/0030_event_location_name.py create mode 100644 core/migrations/__pycache__/0028_volunteer_notes.cpython-311.pyc create mode 100644 core/migrations/__pycache__/0029_event_address_event_city_event_latitude_and_more.cpython-311.pyc create mode 100644 core/migrations/__pycache__/0030_event_location_name.cpython-311.pyc create mode 100644 core/templates/core/event_edit.html diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index 9619ec46f36d0ea9b0e156508424f627581c7f64..4bd051848a14c2d4d83c9a0572e47bc181db811f 100644 GIT binary patch delta 18944 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 delta 18033 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 SHa6+)NPDAg_B&dzcl{67nWk3& delta 2399 zcmbVN3rv$&6z(nd2U;FwP)g-dUZIrND0Hg`!N8=7! z86uIqPyfY-+6FyrTmABA(<-U({Ca;&<$#nwM8%W-cA>GUxz+xfjW@uxlu*S%D&?~% z=!2n8n+XL~q41kFg&cylvBg9I27}Ir_}jr1n+)eu%@7|K2llv2oBU*DFg5SSEBUWS?c3@A@JCqIsQgq_f{T?KJEfgESNzw18a-lOu@C19ut zh2i9A(kV_S-z6L)>&692>Y)aD1-}_5*@A$(^+fi()M#!$H4JM4bw&P)tl88OwvE|cbA&T^_}cc+llZuPhoYf4!QI827H(m}AAFp-X#D!EfsFVtW*iOO}`h5FTm#OHr$AN^ zMO;XZ?AyTfL-+Z)1zN!^b?@ z>u!dVP`4>&wJhTENy=+D#Y&cO6e5gb;}Od^V|jY>;OZE%m6P6PpFknPjpegXcw&g# zs$Ly~8u${e@<(qhH&KW%y`1Hyho#LDxmuR-(gfvQZ!A+NMEH3*%an)Z_BPdOShVFv zAPQx13>yGBm(!c_g%CN5Vq9JdfU4J`bL%aynk!-D zm~gDr-GbBOF^6ec~i%ZE4!Q!^!*5 zKK~2&H@6hBD$TBvUKFw#%IXm-AndTSv&fDN%V?HxEGO8~+=Zd$X!$%HOz1Dvh%?P* zVhka8m3v2*t;;#bbn{CslAy^JF1})`l^TOdEgkJ0T}54`b4)kC)FK`hc4UKmr$WY( F;y-LfoM-?5 diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 5bcaf3e5f8f5ac3c378d94efe450ff93bc8f4cea..438a44334f5017c84077ba5f00304521469d373f 100644 GIT binary patch delta 2212 zcma)7eN0nV6z?sxQ~GED3zUx*BIqbXL}V}vLUFWEK~TQLqCnxj&T47ddli)}qed24 zvShf!+=sfDEK`_nVd=KGe{^xnmbvMu>yoB1vMqBaGe(_enc3oY&n>WxnQS+?Kh8b( zcYo*fp7SnuzaeKLM13MYK2Ao5;mT94Gu7{^ujP=*z%Mza3CfSDS3Cj5CFSH?z*FK> z#C}3OFj;I`B7RCONlqa{_|(KRL+@IB^fqNm;;AG0yNmgQYGYaSssE zF;3cCf_yx$=3^;Ob3B$NPG@x3)Bg`IUna9>$Yr%MK4C=0CocHynS9cS%&zB`w8)HF zIA5Fn=n*PDDHqG>9}y775ne>dLO6s_iSRPQD+sT`9~s3^Soeq`omQj(y6P^Gm9Waz zNDSb&tybPck{&*^t+p5_LHv!bBPw2y`b5`&&*k+*;@#dp2U^`8N52s5+4+8pHuHTH zVJ#ejM7=&C1(gso>68u;sMKe`bLMzxT|Y7$*5-~`L)zS_nA0WGT60iq4r$H)4Pmum z)EQD6CUT})r`4vQ+7wco{Iy|K#%O6sm2pg(%AHn~1yyAsRhhpg%w>*RLtN$rnbJ;k zr9rMV#FhFjVNKTPU`UfSQA|ZNrl7_Y(wO{pVa-x>FP-T9C=t$97r@!1e2~}cNeX1w zrz!k2%L5Q>x50t>-z!+1>v@amC8_B38YOO!q;+e7Rb zv`h(bwBe9k9EYL}hadle(#5M3jIp8-?H>`YA>5!KVY6gsXZxd11rmqU?Or$iRs}&U zlpKRX6{s5n={M;Pm56@#)Q%`5AFYijC-6eE4(>E2ks44om6J?b&^*}DWK5XniiD;u z%HfTs+-9stMB|Vo*ApIr?-m9H4}E2@-eMfGFawdcq)b%dlA|EeDKkeCT#inJ65Pn6 z@GF@0X)wd7p>*R!@?1rZl}vMaK`t-E<@qgeWuuKOhbOIRP`&9W$%mOuGo%5|HaBK5 zy-gTy4JxyiF`=~*eHQR=y10B?Bh*4;%YJ$sg|4?;m+J_8+ogkBt(B>dA*TV)Y@lF_ z5!o|k0!!O^(h6g3&C8fHi^TNtm5q`_w3gy|GXXD`s;NL5p@~lEnb`-;O)6O6i|yf5 z9WoZM2Zf=c8AtN z&AIro>_wlAE_7T3Si7=1zIqL23!`+r^RZKgTC25!E1`Mp#z}@3bvjj zHoTp{qHQk`k_fMNtx&Ymtz9tPwLktmRhlbF8}xMB$R2pBd$)?kcVd_}DCj9vHKHQI zP67uxE$r=?B#lhA!cj<+vzI*3drf&14-GF)tUp*W*cGs>J0a-Q6Hnl#bC0|yg{;0M`!|eQpKrL0{=0u|7A2B;a!dAL98->S Rx7ps^zcZExK69-ge*)ECB#r<8 delta 1615 zcmZuxdrX^E6z?tcbF>u7%a-z3$GSiSbft=DT+?AthZEb>v?dpWnZ6Px_U_C|wniD~IssBdT?7v*?ESd+QP zG_X)_FQA_>Nm4VgSg09!pTiFONpcn5x0ml(Ld6w?ZxEIdzC`#Q;ah~K87L1V0*{Ho zlte#3)mI4D!O${IR^<7X@(f`OHS6$UYn_L?|A71Z!4+MSMiO*PO8HYF&!h;!#F2Pv zO37{)gOQj&+8Gg}p>)El=xbu2Xp<9(xHy)YN{aL*w6(n@Tu0jq{MKeBw`GlEP9qoy z)7*j69VU_o7dk2hJu39@W5s+#SsX|#Zs*v_9Ysfil3je%? zA>EA1TNKK6t898$+Hc8!jTsiyw1xdpK$wTWO6p;-`yp*HORxizcwwvG3~zSJqzXJe z{p3N2xvJnwk50IQ9;I-zXIF6DILMdC)D(o#Q zg*STV;M4qS5c~ubY@>Rbdj+&usA;LpiKm)2!+!@fNd^b78Un6g2qq zAlaya)PZOB{e%UqFtBA(8_GW;{DN?sft1O>VzC@gPXrR5$T^gVvMUiqS}XY{#U|K& zP&WV0EG$I&dYP>B?KH{6O_C1$sXA0Y9VHJc9-EkkE-xs1eRRZc08Vv>zAa+ z;kXzY6DP$u8zjt0s}ZzF)#3TsQaREkSy72lgN6cd3|xgOcemQguN0nlzs{O&d-Mir zhpfT=B2IT2{_Xut9^5C?(D((Nsp;cmpZ9_xGMzlN8vyXu-gYtoD zh7&_Q#0kpKfRzX1v73>L?;%a1C~ZSsD;)Hga_uN0bg?Bp1*;v^aK_`q`D0@Urr~Qw zBT}-^?-XWJ(^pV_6?XbuTNg6=X)kU|7-_Q*MexV)q0rKF!%ukg$RM2YdMeX%k8~Uz zI*w8ulN2E)rX~_0dKWeAyf6nc5Du^<9fxD1r4Ss65r@1y(o2L&G#jD9=P%sH#CxeT z`McXp{I0K%jKgK0r>P0&}cLgs7?B*B+~N`m>3{m!-a{xHvT`vXNNb7 z&mo@>ejB_Iy!2rxiIkD3ygPDKJ)TQ;t*QPCvz;$E*HORmkFml)ddZsV|M8?SpS{j) N8~~Y{pW#!J_}u`Po5P2aU9u)x^ib;PgHGs(rRYEp~ zR}k-4s!FM>33*u-n&+ohKW&ks92FF~SQeUnErD&`B>SFr)oT~4 zJ6n}fSy5C?(3HGdcqly5c1l9i)d5mVmzZ9=(2+G^qq6llsBLRP^W7;D6KO?BmME!> z{{Pps50TYY3m>$;i?$=Rky;CwwH-y^J>@m6xLMk!{Y$JdS=my`iqQP9b%d&H?^Km{ zr!4pc{qJ+gqp}NE*%hl(EC?)wSeVvd4kY_$XlkX8_Jd6ad+<`&(e)Sf9wZJ255;F| z$KN3sH((sYIOz`S^Y#&ISbxmpQ~PC_zvS@`yVphwiCuR(hr1kMy~p9D@MDL!&g2Ug z+u4HzI_o=$V1M+jgg`Vv#2^wE!$T&XZbWeS4qAMGAa6h(L!JyX?tY)&Mac~W1p^8g z3dFN<&BXo_5eH*$ z>j=^Yq%ow)M+}deICxUQLIQogjv#A57DJYdF+O4PP-6^Vzm4Xz2yzDGFyx3YT&IRf z(;zflc$yk=2qXg}3=;9P0|F-J{eOj)WCV%<3I>G)84sEK#%To4W{~tff?Wf4G3=6Q z#;==v_Ef~5t)Y!42=)xv!>~ufbPB(n577AUc{aKMr+1W_13jal`6$8hf#R<^li)k98qPn>TOdeZ5s&++`W)qL;GQw2xYQDG0kz5)9f_Q{vr zai{I@KDXg3Gbp-*AZ0)bLyFwxj)k^k_&9<+!NvlDxB+ntak9t_$+m&dZVr7zZ#UGw z-X(5HwJfb>0w^3ukT4*DAwib8VMTWjx_d;P-_vIYJw0ijXY?%1@AG_BuhV>u=j-}U zgPz_j&vRD&N4vW!@y&zw$6S8y!dIhc?!8Ioxnra4;IpQ){1(Bc0h<^$Nr5}$wqxeh bg>Q&xy^Nq@Km|jED9mxt&wgrz{wMzfXwA*E delta 611 zcmYk4F=!J}7{_zzccw8UO=A$-Qbn{TX*{$_B*YkPZ6KQFYHed{ZqNn=MKfrF#yGTY zp*Xa>#Zg=gI#}rv=;S71@jLm3c5`qL7vJvUdmjyW$Nl*8{=dijc-&U<>qzKrFu32N zo;Tai?{5XyLx;Rg^JDyp*;|R{ zw8VON*s#Qh@Dast%s=77Du?l7VJb!>#93q^vWyrApcUOl#v zqO&%ZA0pITs1elUMcrRA_Jm9)PuT^$rS7ERLW7_o#|={}6Jqzkknggg^Q8Eza-Q>5 zF&rKA6**6dTy)4+;%tlCYJHmXHSr=kxIDx8diSVAJ4MNHEc|!DvuwLJ%`Q`LfwTrL0!(XvCuWLox7B);NmkJfTjygf~LHwm6d>g JAD>eH#XsFWyl?;j diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 86959305481addee854aa151a6b99d5ec855fab7..2d879ddb425815f05ff943d78e13f5e50056bb63 100644 GIT binary patch delta 16064 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

9{E66HX3t3p|QBxNft6vad*ct*08Xq*!oed}`j z=`)qWRPpVS+m09IMt`mmrn{5RJKk@O;fSCNU5RknJu@`j_eT{Do`adGlMpOuC$6$|>aKZ`V{ue0afNa^sg$li$cW)CDxJmCglXX-d>(yizi0mg+eZz&wlrMt8kC{G0OM6JMib^v7O*l z4iK{$E*#TP#Orl-gq%cvQc$I`T zGPwG^e2GF9_5D!D5`6Xj{Ryk$5jGC4-(e*$(c^)F@Yj#1K-j&hiv1dD3j%~=gP17B zIE;?>_-iKzl<9K9JinpNm>RobHl>{y5KlhB(+}q(KkTcl#2Aj5hoTh~La>+7Ye-3d zJqEpo8?OW6nG!|_0)XpbPHV}WX7JZPuH}QfULk3H(p5{YH0RVlsjzoR@ve{hmBAtn zY0$UiRhWG>z~H%{6W8YeAV&484QR(%e$PxV+fb=qJZ5ju3)k_DAHPEjoY1{GVO;ME z#5;@_B#=H;Rq5|b==Ab=Y-~6G@N{W$M345|xGSIEiAjXudswg#wosF>0C6BkAH#9T zk!g6(-~iilra0*s@n{RaaHb?&%m4DMSLz#w48=~vxm&C3FxL9B(g9>Dx5B*}*Xh!qf*Dx2B+CreE>xpxf$eWo5U74Y- z@Sk#xKD;(Xs+BslWVfdoe@UgK%j=!3&7La1&*fG{GOwjl$!jj%R+yHe|Amsn!H$8= zMnD_DC=vY@mmdMn05rgPz~_KJ04@T)1>oxt`>Uyg@JBTN1`wrSR1g_zRHFDJQf`s2 z>cJ?2b%ubJAxspZ2@{SAM}qFH;6@nsps-yA?J@{WFTq1tBFVavGO5LrBm3RT6n36& z)Bh&*_lbw!PLuXZ9V^VVLh1hv`}Owv?dciP$SJkb*h{7exTR#NgtECX*2daoYnK(&gMy(k$tcDGJwbzavu`GawTWN5EEqXCp9j`{_*S+A)RFwe)Nh hj_W5)qn;?NEfzgdIBEO5EU7-*WR>R7P#}Xc{ukUkG-3b% diff --git a/core/admin.py b/core/admin.py index 1350ddf..78b901e 100644 --- a/core/admin.py +++ b/core/admin.py @@ -39,6 +39,7 @@ VOTER_MAPPABLE_FIELDS = [ ('zip_code', 'Zip Code'), ('county', 'County'), ('phone', 'Phone'), + ('notes', 'Notes'), ('phone_type', 'Phone Type'), ('email', 'Email'), ('district', 'District'), @@ -50,6 +51,8 @@ VOTER_MAPPABLE_FIELDS = [ ('window_sticker', 'Window Sticker'), ('latitude', 'Latitude'), ('longitude', 'Longitude'), + ('secondary_phone', 'Secondary Phone'), + ('secondary_phone_type', 'Secondary Phone Type'), ] EVENT_MAPPABLE_FIELDS = [ @@ -59,6 +62,13 @@ EVENT_MAPPABLE_FIELDS = [ ('end_time', 'End Time'), ('event_type', 'Event Type (Name)'), ('description', 'Description'), + ('location_name', 'Location Name'), + ('address', 'Address'), + ('city', 'City'), + ('state', 'State'), + ('zip_code', 'Zip Code'), + ('latitude', 'Latitude'), + ('longitude', 'Longitude'), ] EVENT_PARTICIPATION_MAPPABLE_FIELDS = [ @@ -76,6 +86,7 @@ DONATION_MAPPABLE_FIELDS = [ INTERACTION_MAPPABLE_FIELDS = [ ('voter_id', 'Voter ID'), + ('volunteer_email', 'Volunteer Email'), ('date', 'Date'), ('type', 'Interaction Type (Name)'), ('description', 'Description'), @@ -88,6 +99,7 @@ VOLUNTEER_MAPPABLE_FIELDS = [ ('last_name', 'Last Name'), ('email', 'Email'), ('phone', 'Phone'), + ('notes', 'Notes'), ] VOTER_LIKELIHOOD_MAPPABLE_FIELDS = [ @@ -318,7 +330,7 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin): valid_fields = {f.name for f in Voter._meta.get_fields()} mapped_fields = {f for f in mapping.keys() if f in valid_fields} # Ensure derived/special fields are in update_fields - update_fields = list(mapped_fields | {"address", "phone", "longitude", "latitude"}) + update_fields = list(mapped_fields | {"address", "phone", "secondary_phone", "secondary_phone_type", "longitude", "latitude"}) if "voter_id" in update_fields: update_fields.remove("voter_id") def chunk_reader(reader, size): @@ -412,7 +424,7 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin): if val_lower in window_sticker_choices: val = val_lower elif val_lower in window_sticker_reverse: val = window_sticker_reverse[val_lower] else: val = "none" - elif field_name == "phone_type": + elif field_name in ["phone_type", "secondary_phone_type"]: val_lower = val.lower() if val_lower in phone_type_choices: val = val_lower @@ -431,6 +443,11 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin): if voter.phone != old_phone: changed = True + old_secondary_phone = voter.secondary_phone + voter.secondary_phone = format_phone_number(voter.secondary_phone) + if voter.secondary_phone != old_secondary_phone: + changed = True + if voter.longitude: try: new_lon = Decimal(str(voter.longitude)[:12]) @@ -527,9 +544,9 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin): return render(request, "admin/import_csv.html", context) @admin.register(Event) class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin): - list_display = ('id', 'name', 'event_type', 'date', 'start_time', 'end_time', 'tenant') - list_filter = ('tenant', 'date', 'event_type') - search_fields = ('name', 'description') + list_display = ('id', 'name', 'event_type', 'date', 'location_name', 'city', 'state', 'tenant') + list_filter = ('tenant', 'date', 'event_type', 'city', 'state') + search_fields = ('name', 'description', 'location_name', 'address', 'city', 'state', 'zip_code') change_list_template = "admin/event_change_list.html" def changelist_view(self, request, extra_context=None): @@ -585,7 +602,7 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin): preview_data.append({ 'action': action, 'identifier': f"{event_name or 'No Name'} ({date} - {event_type_name})", - 'details': row.get(mapping.get('description', '')) or '' + 'details': f"{row.get(mapping.get('city', '')) or ''}, {row.get(mapping.get('state', '')) or ''}" }) context = self.admin_site.each_context(request) context.update({ @@ -625,9 +642,16 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin): date = row.get(mapping.get('date')) if mapping.get('date') else None event_type_name = row.get(mapping.get('event_type')) if mapping.get('event_type') else None description = row.get(mapping.get('description')) if mapping.get('description') else None + location_name = row.get(mapping.get('location_name')) if mapping.get('location_name') else None name = row.get(mapping.get('name')) if mapping.get('name') else None start_time = row.get(mapping.get('start_time')) if mapping.get('start_time') else None end_time = row.get(mapping.get('end_time')) if mapping.get('end_time') else None + address = row.get(mapping.get('address')) if mapping.get('address') else None + city = row.get(mapping.get('city')) if mapping.get('city') else None + state = row.get(mapping.get('state')) if mapping.get('state') else None + zip_code = row.get(mapping.get('zip_code')) if mapping.get('zip_code') else None + latitude = row.get(mapping.get('latitude')) if mapping.get('latitude') else None + longitude = row.get(mapping.get('longitude')) if mapping.get('longitude') else None if not date or not event_type_name: row["Import Error"] = "Missing date or event type" @@ -643,12 +667,26 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin): defaults = {} if description and description.strip(): defaults['description'] = description + if location_name and location_name.strip(): + defaults['location_name'] = location_name if name and name.strip(): defaults['name'] = name if start_time and start_time.strip(): defaults['start_time'] = start_time if end_time and end_time.strip(): defaults['end_time'] = end_time + if address and address.strip(): + defaults['address'] = address + if city and city.strip(): + defaults['city'] = city + if state and state.strip(): + defaults['state'] = state + if zip_code and zip_code.strip(): + defaults['zip_code'] = zip_code + if latitude and latitude.strip(): + defaults['latitude'] = latitude + if longitude and longitude.strip(): + defaults['longitude'] = longitude defaults['date'] = date defaults['event_type'] = event_type @@ -677,10 +715,6 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin): except Exception as e: self.message_user(request, f"Error processing file: {e}", level=messages.ERROR) return redirect("..") - except Exception as e: - print(f"DEBUG: Voter import failed: {e}") - self.message_user(request, f"Error processing file: {e}", level=messages.ERROR) - return redirect("..") else: form = EventImportForm(request.POST, request.FILES) if form.is_valid(): @@ -724,7 +758,7 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin): class VolunteerAdmin(BaseImportAdminMixin, admin.ModelAdmin): list_display = ('first_name', 'last_name', 'email', 'phone', 'tenant', 'user') list_filter = ('tenant',) - fields = ('tenant', 'user', 'first_name', 'last_name', 'email', 'phone', 'interests') + fields = ('tenant', 'user', 'first_name', 'last_name', 'email', 'phone', 'notes', 'interests') search_fields = ('first_name', 'last_name', 'email', 'phone') inlines = [VolunteerEventInline, InteractionInline] filter_horizontal = ('interests',) @@ -845,10 +879,6 @@ class VolunteerAdmin(BaseImportAdminMixin, admin.ModelAdmin): except Exception as e: self.message_user(request, f"Error processing file: {e}", level=messages.ERROR) return redirect("..") - except Exception as e: - print(f"DEBUG: Voter import failed: {e}") - self.message_user(request, f"Error processing file: {e}", level=messages.ERROR) - return redirect("..") else: form = VolunteerImportForm(request.POST, request.FILES) if form.is_valid(): @@ -1048,10 +1078,6 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin): except Exception as e: self.message_user(request, f"Error processing file: {e}", level=messages.ERROR) return redirect("..") - except Exception as e: - print(f"DEBUG: Voter import failed: {e}") - self.message_user(request, f"Error processing file: {e}", level=messages.ERROR) - return redirect("..") else: form = EventParticipationImportForm(request.POST, request.FILES) if form.is_valid(): @@ -1241,10 +1267,6 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin): except Exception as e: self.message_user(request, f"Error processing file: {e}", level=messages.ERROR) return redirect("..") - except Exception as e: - print(f"DEBUG: Voter import failed: {e}") - self.message_user(request, f"Error processing file: {e}", level=messages.ERROR) - return redirect("..") else: form = DonationImportForm(request.POST, request.FILES) if form.is_valid(): @@ -1388,6 +1410,7 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin): date = row.get(mapping.get('date')) type_name = row.get(mapping.get('type')) + volunteer_email = row.get(mapping.get('volunteer_email')) description = row.get(mapping.get('description')) notes = row.get(mapping.get('notes')) @@ -1397,6 +1420,12 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin): errors += 1 continue + volunteer = None + if volunteer_email and volunteer_email.strip(): + try: + volunteer = Volunteer.objects.get(tenant=tenant, email=volunteer_email.strip()) + except Volunteer.DoesNotExist: + pass interaction_type = None if type_name and type_name.strip(): interaction_type, _ = InteractionType.objects.get_or_create( @@ -1405,6 +1434,8 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin): ) defaults = {} + if volunteer: + defaults['volunteer'] = volunteer if interaction_type: defaults['type'] = interaction_type if description and description.strip(): @@ -1437,10 +1468,6 @@ class InteractionAdmin(BaseImportAdminMixin, admin.ModelAdmin): except Exception as e: self.message_user(request, f"Error processing file: {e}", level=messages.ERROR) return redirect("..") - except Exception as e: - print(f"DEBUG: Voter import failed: {e}") - self.message_user(request, f"Error processing file: {e}", level=messages.ERROR) - return redirect("..") else: form = InteractionImportForm(request.POST, request.FILES) if form.is_valid(): diff --git a/core/forms.py b/core/forms.py index 74dc8ed..9072933 100644 --- a/core/forms.py +++ b/core/forms.py @@ -85,7 +85,7 @@ class AdvancedVoterSearchForm(forms.Form): class InteractionForm(forms.ModelForm): class Meta: model = Interaction - fields = ['type', 'date', 'description', 'notes'] + fields = ['type', 'volunteer', 'date', 'description', 'notes'] widgets = { 'date': forms.DateTimeInput(attrs={'type': 'datetime-local'}, format='%Y-%m-%dT%H:%M'), 'notes': forms.Textarea(attrs={'rows': 2}), @@ -95,9 +95,11 @@ class InteractionForm(forms.ModelForm): super().__init__(*args, **kwargs) if tenant: self.fields['type'].queryset = InteractionType.objects.filter(tenant=tenant, is_active=True) + self.fields['volunteer'].queryset = Volunteer.objects.filter(tenant=tenant) for field in self.fields.values(): field.widget.attrs.update({'class': 'form-control'}) self.fields['type'].widget.attrs.update({'class': 'form-select'}) + self.fields['volunteer'].widget.attrs.update({'class': 'form-select'}) if self.instance and self.instance.date: self.initial['date'] = self.instance.date.strftime('%Y-%m-%dT%H:%M') @@ -168,7 +170,7 @@ class EventParticipantAddForm(forms.ModelForm): class EventForm(forms.ModelForm): class Meta: model = Event - fields = ['name', 'date', 'start_time', 'end_time', 'event_type', 'description'] + fields = ['name', 'date', 'start_time', 'end_time', 'event_type', 'description', 'location_name', 'address', 'city', 'state', 'zip_code', 'latitude', 'longitude'] widgets = { 'date': forms.DateInput(attrs={'type': 'date'}), 'start_time': forms.TimeInput(attrs={'type': 'time'}), @@ -250,7 +252,8 @@ class VolunteerImportForm(forms.Form): class VolunteerForm(forms.ModelForm): class Meta: model = Volunteer - fields = ['first_name', 'last_name', 'email', 'phone', 'interests'] + fields = ['first_name', 'last_name', 'email', 'phone', 'notes', 'interests'] + widgets = {'notes': forms.Textarea(attrs={'rows': 3})} def __init__(self, *args, tenant=None, **kwargs): super().__init__(*args, **kwargs) @@ -276,3 +279,20 @@ class VolunteerEventForm(forms.ModelForm): for field in self.fields.values(): field.widget.attrs.update({'class': 'form-control'}) self.fields['event'].widget.attrs.update({'class': 'form-select'}) + +class VolunteerEventAddForm(forms.ModelForm): + class Meta: + model = VolunteerEvent + fields = ['volunteer', 'role'] + + def __init__(self, *args, tenant=None, **kwargs): + super().__init__(*args, **kwargs) + if tenant: + volunteer_id = self.data.get('volunteer') or self.initial.get('volunteer') + if volunteer_id: + self.fields['volunteer'].queryset = Volunteer.objects.filter(tenant=tenant, id=volunteer_id) + else: + self.fields['volunteer'].queryset = Volunteer.objects.none() + for field in self.fields.values(): + field.widget.attrs.update({'class': 'form-control'}) + self.fields['volunteer'].widget.attrs.update({'class': 'form-select'}) diff --git a/core/migrations/0028_volunteer_notes.py b/core/migrations/0028_volunteer_notes.py new file mode 100644 index 0000000..c02db8a --- /dev/null +++ b/core/migrations/0028_volunteer_notes.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2026-01-29 21:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0027_voter_secondary_phone_voter_secondary_phone_type'), + ] + + operations = [ + migrations.AddField( + model_name='volunteer', + name='notes', + field=models.TextField(blank=True), + ), + ] diff --git a/core/migrations/0029_event_address_event_city_event_latitude_and_more.py b/core/migrations/0029_event_address_event_city_event_latitude_and_more.py new file mode 100644 index 0000000..3c0d514 --- /dev/null +++ b/core/migrations/0029_event_address_event_city_event_latitude_and_more.py @@ -0,0 +1,43 @@ +# Generated by Django 5.2.7 on 2026-01-29 22:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0028_volunteer_notes'), + ] + + operations = [ + migrations.AddField( + model_name='event', + name='address', + field=models.CharField(blank=True, max_length=255), + ), + migrations.AddField( + model_name='event', + name='city', + field=models.CharField(blank=True, max_length=100), + ), + migrations.AddField( + model_name='event', + name='latitude', + field=models.DecimalField(blank=True, decimal_places=9, max_digits=12, null=True), + ), + migrations.AddField( + model_name='event', + name='longitude', + field=models.DecimalField(blank=True, decimal_places=9, max_digits=12, null=True), + ), + migrations.AddField( + model_name='event', + name='state', + field=models.CharField(blank=True, max_length=2), + ), + migrations.AddField( + model_name='event', + name='zip_code', + field=models.CharField(blank=True, max_length=20), + ), + ] diff --git a/core/migrations/0030_event_location_name.py b/core/migrations/0030_event_location_name.py new file mode 100644 index 0000000..b4c5122 --- /dev/null +++ b/core/migrations/0030_event_location_name.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2026-01-29 22:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0029_event_address_event_city_event_latitude_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='event', + name='location_name', + field=models.CharField(blank=True, max_length=255), + ), + ] diff --git a/core/migrations/__pycache__/0028_volunteer_notes.cpython-311.pyc b/core/migrations/__pycache__/0028_volunteer_notes.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..730fd1a957eae859922493913859eb04065b3105 GIT binary patch literal 826 zcmZ`$zi-qq6t?s0lBg9BRkQ-86GCLUR7ePh3WYWO8n2OW{!+`nCfs3vhet)9 zrMa+%j6#yfO{My`7GxGmaD)Jbn4(alcn62NhQJs!qU6z3%~*q`22tbGJeDR-ZKi_l)0>RNa66Mutkq7)x>3KSK|%zm>IwtqNTa RhwkL&{d2W&@sHK|+~1ps)^7j+ literal 0 HcmV?d00001 diff --git a/core/migrations/__pycache__/0029_event_address_event_city_event_latitude_and_more.cpython-311.pyc b/core/migrations/__pycache__/0029_event_address_event_city_event_latitude_and_more.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8172ae5bfb90b7aced174cef4a368aa0d654e7c8 GIT binary patch literal 1586 zcmb_b&rcIU6rSB~m)*5h7@J^(XhcEkff5rFO-d6Z(ZnbhjWLa6v+RuRmhEoc9|(Hk z;K3s|{{zIpAHdP0$2RGq8IPWND*&$!KH}Ac9pH6EC#<$O} zt*;V7UpR9jwg*R7?=*5y~dp zd^syJt!kA#Bd}b@XL8jg)b`ETlYi4au)8gAN{WgCK}C@WC83xQ5tS;jD4-_%AGtCR z2OttjZprH+2!OXcwg!F|)E7hPi=iGnh8D$8VouO;ilm6Nm0pLGZo>PIT2B9`IA{9K zGo@f!*AAV4Qcri>fch*+m#7o4 z0fP{a`u^kgcgPj^Bo}PMS%iKumUWvtrJ$Tm0OB2^J4TfrB&iP!-w~?3Av*2o0Mnkr)QF*D3^9`JD;{4HMpz%1r%`9BF z?5_WyDi+uNj0&?DvT)<7yOimw8$CCZRHfdVoP1yId3@(2ADc~+;JW_fo%fXz@>7s{ zsTG*5BG@HBfchk$T^dMTi4uuCCLp~hAagpD>!3YEUrt$92IVQN<8YK;75&Bou)6 zeC;>>6@+X85yWPIqt_SdUn=7O3Si_8^ z5g>VqW_-E}jTmdRkflX!{IoEMvGl>5=vSr}u1?Qin_9~|w

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

- Edit Event Info + Edit Event Info + @@ -24,7 +27,7 @@
-
+
Event Details
@@ -46,12 +49,75 @@ {% endif %}
+
+ + {% if event.location_name %}{{ event.location_name }}
{% endif %} + + {% if event.address %} + {{ event.address }}
+ {{ event.city }}{% if event.city and event.state %}, {% endif %}{{ event.state }} {{ event.zip_code }} + {% elif event.city or event.state or event.zip_code %} + {{ event.city }}{% if event.city and event.state %}, {% endif %}{{ event.state }} {{ event.zip_code }} + {% else %} + No location provided. + {% endif %} +
+ {% if event.latitude and event.longitude %} +
+ {{ event.latitude }}, {{ event.longitude }} +
+ {% endif %} +

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

+ + +
+
+
Volunteers ({{ volunteers.count }})
+
+
+ + + + + + + + + + {% for v in volunteers %} + + + + + + {% empty %} + + + + {% endfor %} + +
VolunteerRole
+ + {{ v.volunteer.first_name }} {{ v.volunteer.last_name }} + + {{ v.role }} +
+ {% csrf_token %} + +
+
+ No volunteers assigned. +
+
+
@@ -171,8 +237,55 @@
+ + + diff --git a/core/templates/core/event_edit.html b/core/templates/core/event_edit.html new file mode 100644 index 0000000..e0815ef --- /dev/null +++ b/core/templates/core/event_edit.html @@ -0,0 +1,135 @@ +{% extends "base.html" %} +{% load static %} + +{% block content %} +
+
+ +

{% if is_create %}Create New Event{% else %}Edit Event{% endif %}

+
+ +
+
+
+
+
+ {% csrf_token %} +
+
+ + {{ form.name }} + {% if form.name.errors %} +
{{ form.name.errors }}
+ {% endif %} +
+
+ + {{ form.event_type }} + {% if form.event_type.errors %} +
{{ form.event_type.errors }}
+ {% endif %} +
+
+ + {{ form.date }} + {% if form.date.errors %} +
{{ form.date.errors }}
+ {% endif %} +
+
+ + {{ form.start_time }} + {% if form.start_time.errors %} +
{{ form.start_time.errors }}
+ {% endif %} +
+
+ + {{ form.end_time }} + {% if form.end_time.errors %} +
{{ form.end_time.errors }}
+ {% endif %} +
+
+ + {{ form.description }} + {% if form.description.errors %} +
{{ form.description.errors }}
+ {% endif %} +
+ +
+
Location Information
+ +
+ + {{ form.location_name }} + {% if form.location_name.errors %} +
{{ form.location_name.errors }}
+ {% endif %} +
+
+ + {{ form.address }} + {% if form.address.errors %} +
{{ form.address.errors }}
+ {% endif %} +
+
+ + {{ form.city }} + {% if form.city.errors %} +
{{ form.city.errors }}
+ {% endif %} +
+
+ + {{ form.state }} + {% if form.state.errors %} +
{{ form.state.errors }}
+ {% endif %} +
+
+ + {{ form.zip_code }} + {% if form.zip_code.errors %} +
{{ form.zip_code.errors }}
+ {% endif %} +
+
+ + {{ form.latitude }} + {% if form.latitude.errors %} +
{{ form.latitude.errors }}
+ {% endif %} +
+
+ + {{ form.longitude }} + {% if form.longitude.errors %} +
{{ form.longitude.errors }}
+ {% endif %} +
+ +
+ + Cancel +
+
+
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/core/templates/core/event_list.html b/core/templates/core/event_list.html index 369fd5d..f865152 100644 --- a/core/templates/core/event_list.html +++ b/core/templates/core/event_list.html @@ -5,7 +5,7 @@ @@ -18,6 +18,7 @@ Type Date Time + Location Actions @@ -39,13 +40,20 @@ - {% endif %} + + {% if event.city or event.state %} + {{ event.city }}{% if event.city and event.state %}, {% endif %}{{ event.state }} + {% else %} + - + {% endif %} + View Details {% empty %} - +

No events found for this campaign.

@@ -55,4 +63,4 @@
-{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/volunteer_detail.html b/core/templates/core/volunteer_detail.html index f31a821..f4c582c 100644 --- a/core/templates/core/volunteer_detail.html +++ b/core/templates/core/volunteer_detail.html @@ -89,6 +89,10 @@ {{ form.interests }} +
+ + {{ form.notes }} +
Cancel diff --git a/core/templates/core/volunteer_list.html b/core/templates/core/volunteer_list.html index a40995a..f37eb25 100644 --- a/core/templates/core/volunteer_list.html +++ b/core/templates/core/volunteer_list.html @@ -12,22 +12,43 @@
-
+
+
+ +
- +
+
+
Volunteers ({{ volunteers.paginator.count }})
+
+ +
+
- + + @@ -38,6 +59,9 @@ {% for volunteer in volunteers %} + {% empty %} - @@ -73,12 +97,12 @@
    {% if volunteers.has_previous %}
  • - +
  • - +
  • @@ -88,12 +112,12 @@ {% if volunteers.has_next %}
  • - +
  • - +
  • @@ -104,4 +128,84 @@ {% endif %} + + + + + {% endblock %} diff --git a/core/templates/core/voter_detail.html b/core/templates/core/voter_detail.html index 4e4c1d2..3081e2a 100644 --- a/core/templates/core/voter_detail.html +++ b/core/templates/core/voter_detail.html @@ -230,6 +230,7 @@
+ @@ -240,6 +241,7 @@ +
Name + + Name Email Phone Interests
+ + {{ volunteer.first_name }} {{ volunteer.last_name }} @@ -57,7 +81,7 @@
+

No volunteers found matching your search.

Add the first volunteer
Date TypeVolunteer Description Notes Actions
{{ interaction.date|date:"M d, Y H:i" }} {{ interaction.type.name }}{% if interaction.volunteer %}{{ interaction.volunteer }}{% else %}-{% endif %} {{ interaction.description }} {{ interaction.notes|truncatechars:30 }} @@ -572,6 +574,10 @@ {{ interaction_form.type }} +
+ + {{ interaction_form.volunteer }} +
{{ interaction_form.date }} @@ -614,6 +620,15 @@ {% endfor %}
+
+ + +
diff --git a/core/urls.py b/core/urls.py index aa0c522..c436512 100644 --- a/core/urls.py +++ b/core/urls.py @@ -32,6 +32,8 @@ urlpatterns = [ # Event Detail and Participant Management path('events/', views.event_list, name='event_list'), path('events//', views.event_detail, name='event_detail'), + path('events/add/', views.event_create, name='event_create'), + path('events//edit/', views.event_edit, name='event_edit'), 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'), @@ -46,4 +48,8 @@ urlpatterns = [ 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'), -] + path('volunteers/search/json/', views.volunteer_search_json, name='volunteer_search_json'), + path('volunteers/bulk-sms/', views.volunteer_bulk_send_sms, name='volunteer_bulk_send_sms'), + path('events//volunteer/add/', views.event_add_volunteer, name='event_add_volunteer'), + path('events/volunteer//delete/', views.event_remove_volunteer, name='event_remove_volunteer'), +] \ No newline at end of file diff --git a/core/views.py b/core/views.py index f902649..74d8d4b 100644 --- a/core/views.py +++ b/core/views.py @@ -11,7 +11,7 @@ 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, ParticipationStatus, VolunteerEvent, Interest -from .forms import VoterForm, InteractionForm, DonationForm, VoterLikelihoodForm, EventParticipationForm, VoterImportForm, AdvancedVoterSearchForm, EventParticipantAddForm, EventForm, VolunteerForm, VolunteerEventForm +from .forms import VoterForm, InteractionForm, DonationForm, VoterLikelihoodForm, EventParticipationForm, VoterImportForm, AdvancedVoterSearchForm, EventParticipantAddForm, EventForm, VolunteerForm, VolunteerEventForm, VolunteerEventAddForm import logging from django.utils import timezone @@ -62,6 +62,7 @@ def index(request): recent_interactions = Interaction.objects.filter(voter__tenant=selected_tenant).order_by('-date')[:5] upcoming_events = Event.objects.filter(tenant=selected_tenant, date__gte=timezone.now().date()).order_by('date')[:5] + context = { 'tenants': tenants, 'selected_tenant': selected_tenant, @@ -132,6 +133,7 @@ def voter_list(request): page_number = request.GET.get('page') voters_page = paginator.get_page(page_number) + context = { "voters": voters_page, "query": query, @@ -152,6 +154,7 @@ def voter_detail(request, voter_id): tenant = get_object_or_404(Tenant, id=selected_tenant_id) voter = get_object_or_404(Voter, id=voter_id, tenant=tenant) + context = { 'voter': voter, 'selected_tenant': tenant, @@ -455,6 +458,7 @@ def voter_advanced_search(request): page_number = request.GET.get('page') voters_page = paginator.get_page(page_number) + context = { 'form': form, 'voters': voters_page, @@ -672,6 +676,7 @@ def event_list(request): tenant = get_object_or_404(Tenant, id=selected_tenant_id) events = Event.objects.filter(tenant=tenant).order_by('-date') + context = { 'tenant': tenant, 'events': events, @@ -689,16 +694,25 @@ def event_detail(request, event_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') + # Get assigned volunteers + volunteers = event.volunteer_assignments.all().select_related('volunteer').order_by('volunteer__last_name', 'volunteer__first_name') + # Form for adding a new participant add_form = EventParticipantAddForm(tenant=tenant) + # Form for adding a new volunteer + add_volunteer_form = VolunteerEventAddForm(tenant=tenant) + participation_statuses = ParticipationStatus.objects.filter(tenant=tenant, is_active=True) + context = { 'tenant': tenant, 'selected_tenant': tenant, 'event': event, 'participations': participations, + 'volunteers': volunteers, 'add_form': add_form, + 'add_volunteer_form': add_volunteer_form, 'participation_statuses': participation_statuses, } return render(request, 'core/event_detail.html', context) @@ -801,15 +815,24 @@ def volunteer_list(request): Q(first_name__icontains=query) | Q(last_name__icontains=query) | Q(email__icontains=query) ) + # Interest filter + interest_id = request.GET.get("interest") + if interest_id: + volunteers = volunteers.filter(interests__id=interest_id) + 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, } return render(request, 'core/volunteer_list.html', context) @@ -832,6 +855,7 @@ def volunteer_add(request): else: form = VolunteerForm(tenant=tenant) + context = { 'form': form, 'tenant': tenant, @@ -859,6 +883,7 @@ def volunteer_detail(request, volunteer_id): assignments = volunteer.event_assignments.all().select_related('event') assign_form = VolunteerEventForm(tenant=tenant) + context = { 'volunteer': volunteer, 'form': form, @@ -943,3 +968,198 @@ def interest_delete(request, interest_id): interest.delete() return JsonResponse({'success': True}) return JsonResponse({'success': False, 'error': 'Invalid request.'}) + +def event_create(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 = EventForm(request.POST, tenant=tenant) + if form.is_valid(): + event = form.save(commit=False) + event.tenant = tenant + event.save() + messages.success(request, "Event created successfully.") + return redirect("event_detail", event_id=event.id) + else: + form = EventForm(tenant=tenant) + + + context = { + "form": form, + "tenant": tenant, + "selected_tenant": tenant, + "is_create": True, + } + return render(request, "core/event_edit.html", context) + +def event_edit(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) + + if request.method == 'POST': + form = EventForm(request.POST, instance=event, tenant=tenant) + if form.is_valid(): + form.save() + messages.success(request, "Event updated successfully.") + return redirect('event_detail', event_id=event.id) + else: + form = EventForm(instance=event, tenant=tenant) + + + context = { + 'form': form, + 'event': event, + 'tenant': tenant, + 'selected_tenant': tenant, + } + return render(request, 'core/event_edit.html', context) + +def volunteer_search_json(request): + """ + JSON endpoint for volunteer 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) + volunteers = Volunteer.objects.filter(tenant=tenant) + + search_filter = Q(first_name__icontains=query) | Q(last_name__icontains=query) | Q(email__icontains=query) + + results = volunteers.filter(search_filter).order_by("last_name", "first_name")[:20] + + data = [] + for v in results: + data.append({ + "id": v.id, + "text": f"{v.first_name} {v.last_name} ({v.email})", + "phone": v.phone + }) + + return JsonResponse({"results": data}) + +def event_add_volunteer(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 = VolunteerEventAddForm(request.POST, tenant=tenant) + if form.is_valid(): + assignment = form.save(commit=False) + assignment.event = event + if not VolunteerEvent.objects.filter(event=event, volunteer=assignment.volunteer).exists(): + assignment.save() + messages.success(request, f"{assignment.volunteer} added as volunteer.") + else: + messages.warning(request, "Volunteer is already assigned to this event.") + else: + messages.error(request, "Error adding volunteer.") + + return redirect('event_detail', event_id=event.id) + +def event_remove_volunteer(request, assignment_id): + tenant_id = request.session.get("tenant_id") + tenant = get_object_or_404(Tenant, id=tenant_id) + assignment = get_object_or_404(VolunteerEvent, id=assignment_id, event__tenant=tenant) + event_id = assignment.event.id + volunteer_name = str(assignment.volunteer) + assignment.delete() + messages.success(request, f"{volunteer_name} removed from event volunteers.") + return redirect('event_detail', event_id=event_id) + +def volunteer_bulk_send_sms(request): + """ + Sends bulk SMS to selected volunteers using Twilio API. + """ + if request.method != 'POST': + return redirect('volunteer_list') + + 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) + settings = getattr(tenant, 'settings', None) + if not settings: + messages.error(request, "Campaign settings not found.") + return redirect('volunteer_list') + + account_sid = settings.twilio_account_sid + auth_token = settings.twilio_auth_token + from_number = settings.twilio_from_number + + if not account_sid or not auth_token or not from_number: + messages.error(request, "Twilio configuration is incomplete in Campaign Settings.") + return redirect('volunteer_list') + + volunteer_ids = request.POST.getlist('selected_volunteers') + message_body = request.POST.get('message_body') + + if not message_body: + messages.error(request, "Message body cannot be empty.") + return redirect('volunteer_list') + + volunteers = Volunteer.objects.filter(tenant=tenant, id__in=volunteer_ids).exclude(phone='') + + if not volunteers.exists(): + messages.warning(request, "No volunteers with a valid phone number were selected.") + return redirect('volunteer_list') + + success_count = 0 + fail_count = 0 + + auth_str = f"{account_sid}:{auth_token}" + auth_header = base64.b64encode(auth_str.encode()).decode() + url = f"https://api.twilio.com/2010-04-01/Accounts/{account_sid}/Messages.json" + + for volunteer in volunteers: + # Format phone to E.164 (assume US +1) + digits = re.sub(r'\D', '', str(volunteer.phone)) + if len(digits) == 10: + to_number = f"+1{digits}" + elif len(digits) == 11 and digits.startswith('1'): + to_number = f"+{digits}" + else: + # Skip invalid phone numbers + fail_count += 1 + continue + + data_dict = { + 'To': to_number, + 'From': from_number, + 'Body': message_body + } + data = urllib.parse.urlencode(data_dict).encode() + + req = urllib.request.Request(url, data=data, method='POST') + req.add_header("Authorization", f"Basic {auth_header}") + + try: + with urllib.request.urlopen(req, timeout=10) as response: + if response.status in [200, 201]: + success_count += 1 + else: + fail_count += 1 + except Exception as e: + logger.error(f"Error sending SMS to volunteer {volunteer.phone}: {e}") + fail_count += 1 + + messages.success(request, f"Bulk SMS process completed: {success_count} successful, {fail_count} failed/skipped.") + return redirect('volunteer_list')