From d8bf0cd82cc33f435ca0066ccc9fc21ce4a6f709 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Thu, 29 Jan 2026 18:46:31 +0000 Subject: [PATCH] 2.0 --- assets/.gitkeep | 0 core/__pycache__/admin.cpython-311.pyc | Bin 101105 -> 100211 bytes core/__pycache__/forms.cpython-311.pyc | Bin 19476 -> 19783 bytes core/__pycache__/models.cpython-311.pyc | Bin 26588 -> 27142 bytes core/__pycache__/urls.cpython-311.pyc | Bin 2748 -> 2846 bytes core/__pycache__/views.cpython-311.pyc | Bin 37303 -> 42701 bytes core/admin.py | 56 ++++----- core/forms.py | 6 +- ..._event_name_alter_event_unique_together.py | 22 ++++ ...ignsettings_twilio_account_sid_and_more.py | 28 +++++ .../migrations/0026_alter_interaction_date.py | 18 +++ ...lter_event_unique_together.cpython-311.pyc | Bin 0 -> 1014 bytes ...wilio_account_sid_and_more.cpython-311.pyc | Bin 0 -> 1286 bytes ...026_alter_interaction_date.cpython-311.pyc | Bin 0 -> 817 bytes core/models.py | 12 +- core/templates/core/index.html | 2 +- .../templates/core/voter_advanced_search.html | 65 ++++++++++- core/templates/core/voter_detail.html | 6 +- core/urls.py | 1 + core/views.py | 108 ++++++++++++++++++ core/views.py.tmp | 4 - core_admin_patch.py | 32 ++++++ patch_voter_detail_events.py | 44 +++++++ 23 files changed, 355 insertions(+), 49 deletions(-) create mode 100644 assets/.gitkeep create mode 100644 core/migrations/0024_alter_event_name_alter_event_unique_together.py create mode 100644 core/migrations/0025_campaignsettings_twilio_account_sid_and_more.py create mode 100644 core/migrations/0026_alter_interaction_date.py create mode 100644 core/migrations/__pycache__/0024_alter_event_name_alter_event_unique_together.cpython-311.pyc create mode 100644 core/migrations/__pycache__/0025_campaignsettings_twilio_account_sid_and_more.cpython-311.pyc create mode 100644 core/migrations/__pycache__/0026_alter_interaction_date.cpython-311.pyc delete mode 100644 core/views.py.tmp create mode 100644 core_admin_patch.py create mode 100644 patch_voter_detail_events.py diff --git a/assets/.gitkeep b/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index 718ca547000f2115cc2ad69271bfe91db4492fe6..8335e4f16501a25e87d7934160d711700673e54d 100644 GIT binary patch delta 14255 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 delta 14821 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 Nh<;E!Dk>Xu{|n@+m)rmV diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc index 422a45738bf649c87e299536739553a7407a306c..dd564835ed6fc0224182c25d957bca1a837cb8a3 100644 GIT binary patch delta 1754 zcmbW1e@s(X6vun3E&Tzd##U%cf$^xNXlW~SFe{}9NX^&=lexI5vCuk83SAz}G>&9H zl6IYvK|GKEW1*Wy*1q}fg1C+D7X z&-dJW-hCYzBR@|N#dKbtOhCu&#k2l%ra48ohV;Ok#Vqq~ofTac3IsDk$lqG%xq zKYlG)+nTI$Cp$bzRaZ*Yl~#2@LfawylfWA}X3nK+BF+IpkDMm*gSBs*_2jPmcWv&E z<-v+XZO{4dIq6!(znNaiK5BXeht?mH^x4k1kd44lQCTkgHjT1X6sCOOikfz1B>uks zkf*QP$9pr9{t$Z-JCw=s_xVG9ParO4+b}5tCsc({psyx!Fv}U}(~llrlIIWB-xm;R z2N~;}JeJm5Rwql0@lt)7vEO2xHyCHK;kgvko@Uyk&4X=YJ5sr&N&a_#3ns2+Y$Uu_ zp@Z_`wg!m^!~dJyXT=>vG(tCj3Ph#PlQ0ZbYPfs0*NHfaCmMyRq7pUUFZLYbJfaRU z2(juyG7Jr677~PD+3&Io=tjH(FEUYbfxF9WC!+VM;|x3~x2}ZfGT+zZLt_`8W&~bn2k6MTt$?O!(HY$Oqi~UKBB$| z9If0#!rV~h)52|GTzQeAljXN@3GVGKf3#$YSODKnA9Py-Q$ zcvY0duo^dPaS`z+7@2`Z>oHNBnxjx>8-Jv$vwTwgW5MLm^WBK$wo&3=v+n0 zpEi^I1)Yc=&`5x_Wgd1m7Gx=VdVh)X`X;h4&?;44;}sEnQ?9M`bsWM?x-9iG&5Uws z6MfABIuU6!5(vJ?!g`TETGUw{G-zbi3R#=DR9@k?R-}`|q|y|`>*;hp z4q-r)z4Vtcu>+zCX#sD3{_8rV$n>*MvYvw2d5c``EU)PjZarc0g=Q0;w0u zV&0Ms%)@akOF-lxauFvebpCuE#`y?;#J^Zfk=4=?+vK-K`|8FOfD(x9%-(06jey(U z=#pEKHk#W9O|gdWZJLNRh=b~}s-0pT&)lAZFnzS%lM@bS{=H*PeK>Il7xY@9XvLWb zJ99D>XCtlXM5v2zAks6KJqvO}ndCGL0k#@iW^LR#x^dAuPd5JUK$1L9hEMWi&+?L8 zicVY%aRf+;gLG0GE-)L_#3_^-wS?Hgp4u$yq7y#w*vd#Sn;JrK-4{OPRx5AENSkoCV*dPSVX(v(kdNiLg1bYqH8^F#UJ1T5% z**>s_4Po6dVg!-ymdT{iw4b@Ex?;Q4lEFU~4i_3pQXs)kre`)=j#f{x+{a&ML<%!2 HR*Js@*uSFg diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 89c49f21ace26f4fe5a8a70e41544d33a2e25d83..411f104be508670ad8099bd733de3947fb033c4a 100644 GIT binary patch delta 2163 zcmZuydu&rx81L!2!gjljJ!uPL-Ns{`z#d(@uFS!pW3ZR(!3JYnd)s@rb#Co)ZUI?{ zU`7ZgzTg8v!9e^2T@(W;M&utsV?ZI&N~2heieO?`j6jHJ0^<2jc_iRX&adD3&iDA; z?|$d&j~B@HC`o-UB_&zKp5xz*`OefFOKmSC6W}QvBmn0M=Mo6rDzv7Ny-;L1sr{VU z(Zle&Wf3_Ynq5(nn6wu)UoZeyxg(Q)$s|S1z+|FEJUR;#Ra?kKFgs>de}jq(2w{W> z!q*5tA$*Ilg+Xjez%wL!g9`lwRp$^c!?cc7vbfaL)yz2;VP6YHj>Fu!{Q-L z`X1Dn5b_vw&|UCDbuDO}?`l&CQ{I7(oO+T5XPoo20xAT!?R-rw5Qt3Gs~rShn$`r@ z>&~xHtLjx!(l(Wpe3+Hx11t@Hy0auk!~pIHG-x-MCQPW`WBJ{&8)=H0fS8;!yoK~g@}K9CdU$$AmdH%!-!~r=H?7i1@7io zr#&*D|1v9fe6M--Vtk)iny4tg0lzG{9#k2V(PD(D_|i%E!fdIQ#JTWtA{2+*E%`)U zz&|eRf9@SJ7phkMOlrZ<+LF!9S}}?hqwxCKU~_9h@?6{_RKxqN<7#%7SaU#K+a#-3 zS$o&?3OsK@&t?YZ1j<{>1_#?4$V#~0-Zq;DINQ{`~~@$&2Y`7hZ#93Tc$`;rKAs&4=1o$!B+g~7St*^VTey@)JSM0JY}VO!b-=i zQl7F(;S(v9O{BCZCQ_<2Et!p^G%1~pqzo{*bGPv6j_opldAFX@)UvmY_YK?AP6R$5 zV+FO#yi!@2==E4jtE^txYpbZNEU&Oi7Q0m}_w<*QS2EXpbl_tGU1KJDmF%@!N~O|% zv9w(Dl#2bL&0A?JwO80}*2=Oh3$0;xW4dR`>{hF-s=``TY5})9Kn_6IotwChRdfT~ zavvdMa7dgD*E(|s=3}X6VIF>OoG4*G#!{q!U&Jxz8VHEP6q_QJ5!~n-_61y`m;Xb! z6rU7Jf2dm@9CQT(&&&QtX8klU;_|N_@yL|V$}Y@>5MWD*K(S}ps!Im*zMQDRe6;

Z#QIRS#J+jN&M$SXK_gORl8{z|lUkN^Z_*mdq zkheQ;!!G#Ln@fg5-+NzB&rc(k8>**a$MRR2ZleD716HpaHLV`CkgjxNf=CM?^a6SgrnSYa=)u>qx9A4aij=Y7f$ zf*Ay2FwPe^h~i6vCd7~^Q5*bV{D5MDf&wG^V4kQ5`a#hs7($GSdjGe)e848pujig~ z?m4IDp6B+dDf;?6WxeHa*evpMZvXahoS zht`j!pE$}t)-=$`MEN6C>6wS1`9y*P)%7#Pr&2OfBs?Bb2-9gkRyR%;*;_wr;g?XH z1I_~%fX{(%fJtCn!pw*bZqbM0hWG%gGl0o^>i5u2mxuV$vVjOo$1Y1Q#%hd)fwY+Hu73A>ZeE~_bUp_V`7+SEe2F(ksH@o+54kK6Neb72D9 zDD#x`;nJ0EUfbrhm0!?mC$-ust(JGRiCL-XHPc*M40zY|dQ=aZ7qh^9g@>BYvlu9W;#aa8wTkZZ{dp zL?w_R?P3}lAs@@5$y!RF1S%4)m8C=_I6&@R^*WXD^;MV1$CI7@66Mv2Al2|!ZLNV+ z1zihA?Jipx)<7L^?Ajx@Bham`pDB-j>h5*UgLwtqJ0!fRLd{eS_x7~YQhu|i>j4#2 zB~hXAS|ptWq$2FEBaYdfw)v2O#azF7g66AK->v@2nFm8A++ReJ6!9|T!+d^Ce?ft? z-A-#-gClr5gp>GS6)k}R=wjc%WA>T*7EEa~C$-`!t(Z>?ET<(2T6dht!SAkr z*wG>VhOwbdMqny*GHnPsK9h?(r{ia_p^diiM;o?js<2JSU7k!t2tCtpc=1QqLNgj40Uzj58=;g3rAw1tr6WP1{1XDiIj)zByXGhv2W?b kitEtdxMwU%9Nj#J>N2VNn&p1HFm`a~b?9&Wk5N_dAL^gO!ThnZ1>%NOrO)M*x>H&>%)2E{>g?&#_iedPe9KS%VGr7g*dcvbbMiao;S; TIfIekRGOcufg1#i6oHBXL8vd` delta 120 zcmbOywntQbIWI340}!Z}Rb~EVXJB{?;=q6;l<`??qq-n7SGs7FWQu+;gQmgcH0Cpm zVv{ehh;7bd$z^7gntX%3b#fAiFP9Qf3nLI0yH4K8v37GE=R`(+V<~>725t~6QUFQ; E0MI@ezyJUM diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index f306c0fcc1968c1b8dae91bc0d449ce293afc0d1..645f02c0b18ab44be520f30048863fd861853d13 100644 GIT binary patch delta 6793 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 G_x}O|jttuX delta 2160 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 T&YL}Vc5(fP7LNb3TG#yz!Q0dz literal 0 HcmV?d00001 diff --git a/core/models.py b/core/models.py index e88c2d4..b9e6253 100644 --- a/core/models.py +++ b/core/models.py @@ -280,13 +280,16 @@ class VotingRecord(models.Model): class Event(models.Model): tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name='events') - name = models.CharField(max_length=255, blank=True) + name = models.CharField(max_length=255, db_index=True) date = models.DateField() start_time = models.TimeField(null=True, blank=True) end_time = models.TimeField(null=True, blank=True) event_type = models.ForeignKey(EventType, on_delete=models.PROTECT, null=True) description = models.TextField(blank=True) + class Meta: + unique_together = ('tenant', 'name') + def __str__(self): if self.name: return f"{self.name} ({self.date})" @@ -339,7 +342,7 @@ class Interaction(models.Model): voter = models.ForeignKey(Voter, on_delete=models.CASCADE, related_name='interactions') volunteer = models.ForeignKey(Volunteer, on_delete=models.SET_NULL, null=True, blank=True, related_name='interactions') type = models.ForeignKey(InteractionType, on_delete=models.SET_NULL, null=True) - date = models.DateField() + date = models.DateTimeField() description = models.CharField(max_length=255) notes = models.TextField(blank=True) @@ -364,10 +367,13 @@ class VoterLikelihood(models.Model): class CampaignSettings(models.Model): tenant = models.OneToOneField(Tenant, on_delete=models.CASCADE, related_name='settings') donation_goal = models.DecimalField(max_digits=12, decimal_places=2, default=170000.00) + twilio_account_sid = models.CharField(max_length=100, blank=True, default='ACcd11acb5095cec6477245d385a2bf127') + twilio_auth_token = models.CharField(max_length=100, blank=True, default='89ec830d0fa02ab0afa6c76084865713') + twilio_from_number = models.CharField(max_length=20, blank=True, default='+18556945903') class Meta: verbose_name = 'Campaign Settings' verbose_name_plural = 'Campaign Settings' def __str__(self): - return f'Settings for {self.tenant.name}' \ No newline at end of file + return f'Settings for {self.tenant.name}' diff --git a/core/templates/core/index.html b/core/templates/core/index.html index 8000f67..e942b7f 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -171,7 +171,7 @@ - {{ interaction.date|date:"M d, Y" }} + {{ interaction.date|date:"M d, Y H:i" }} {{ interaction.description|truncatechars:50 }} diff --git a/core/templates/core/voter_advanced_search.html b/core/templates/core/voter_advanced_search.html index 4bf18c5..82daeb3 100644 --- a/core/templates/core/voter_advanced_search.html +++ b/core/templates/core/voter_advanced_search.html @@ -88,12 +88,15 @@

Search Results ({{ voters.paginator.count }})
- +
-
@@ -199,6 +202,36 @@
+ + + -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/voter_detail.html b/core/templates/core/voter_detail.html index 3e97371..e862f4e 100644 --- a/core/templates/core/voter_detail.html +++ b/core/templates/core/voter_detail.html @@ -231,7 +231,7 @@ {% for interaction in interactions %} - {{ interaction.date|date:"M d, Y" }} + {{ interaction.date|date:"M d, Y H:i" }} {{ interaction.type.name }} {{ interaction.description }} {{ interaction.notes|truncatechars:30 }} @@ -338,6 +338,7 @@ Date + Event Name Event Type Status Description @@ -348,6 +349,7 @@ {% for participation in event_participations %} {{ participation.event.date|date:"M d, Y" }} + {{ participation.event.name|default:"(No Name)" }} {{ participation.event.event_type.name }} {% if participation.participation_status.name|lower == 'attended' %} @@ -599,7 +601,7 @@
- +
diff --git a/core/urls.py b/core/urls.py index 0bcde88..39ae92f 100644 --- a/core/urls.py +++ b/core/urls.py @@ -7,6 +7,7 @@ urlpatterns = [ path('voters/', views.voter_list, name='voter_list'), path('voters/advanced-search/', views.voter_advanced_search, name='voter_advanced_search'), path('voters/export-csv/', views.export_voters_csv, name='export_voters_csv'), + path('voters/bulk-sms/', views.bulk_send_sms, name='bulk_send_sms'), path('voters//', views.voter_detail, name='voter_detail'), path('voters//edit/', views.voter_edit, name='voter_edit'), path('voters//delete/', views.voter_delete, name='voter_delete'), diff --git a/core/views.py b/core/views.py index de09425..43c2add 100644 --- a/core/views.py +++ b/core/views.py @@ -1,3 +1,7 @@ +import base64 +import re +import urllib.parse +import urllib.request import csv import io from django.http import JsonResponse, HttpResponse @@ -540,6 +544,7 @@ def export_voters_csv(request): ]) return response + def voter_delete(request, voter_id): """ Delete a voter profile. @@ -554,3 +559,106 @@ def voter_delete(request, voter_id): return redirect('voter_list') return redirect('voter_detail', voter_id=voter.id) + +def bulk_send_sms(request): + """ + Sends bulk SMS to selected voters using Twilio API. + """ + if request.method != 'POST': + return redirect('voter_advanced_search') + + 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('voter_advanced_search') + + 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('voter_advanced_search') + + voter_ids = request.POST.getlist('selected_voters') + message_body = request.POST.get('message_body') + client_time_str = request.POST.get('client_time') + + interaction_date = timezone.now() + if client_time_str: + try: + from datetime import datetime + interaction_date = datetime.fromisoformat(client_time_str) + if timezone.is_naive(interaction_date): + interaction_date = timezone.make_aware(interaction_date) + except Exception as e: + logger.warning(f'Failed to parse client_time {client_time_str}: {e}') + + if not message_body: + messages.error(request, "Message body cannot be empty.") + return redirect('voter_advanced_search') + + voters = Voter.objects.filter(tenant=tenant, id__in=voter_ids, phone_type='cell').exclude(phone='') + + if not voters.exists(): + messages.warning(request, "No voters with a valid cell phone number were selected.") + return redirect('voter_advanced_search') + + 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" + + # Get or create interaction type for SMS + interaction_type, _ = InteractionType.objects.get_or_create(tenant=tenant, name="SMS Text") + + for voter in voters: + # Format phone to E.164 (assume US +1) + digits = re.sub(r'\D', '', str(voter.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 + # Log interaction + Interaction.objects.create( + voter=voter, + type=interaction_type, + date=interaction_date, + description='Mass SMS Text', + notes=message_body + ) + else: + fail_count += 1 + except Exception as e: + logger.error(f"Error sending SMS to {voter.phone}: {e}") + 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 diff --git a/core/views.py.tmp b/core/views.py.tmp deleted file mode 100644 index 24fc470..0000000 --- a/core/views.py.tmp +++ /dev/null @@ -1,4 +0,0 @@ - if request.GET.get("yard_sign") == "true": - voters = voters.filter(Q(yard_sign="wants") | Q(yard_sign="has")) - if request.GET.get("window_sticker") == "true": - voters = voters.filter(Q(window_sticker="wants") | Q(window_sticker="has")) diff --git a/core_admin_patch.py b/core_admin_patch.py new file mode 100644 index 0000000..1bdd865 --- /dev/null +++ b/core_admin_patch.py @@ -0,0 +1,32 @@ +import sys + +with open('core/admin.py', 'r') as f: + content = f.read() + +old_block = """ defaults = {} + if participation_status_val and participation_status_val.strip(): + defaults = {} + if participation_status_val and participation_status_val.strip(): + status_obj, _ = ParticipationStatus.objects.get_or_create(tenant=tenant, name=participation_status_val.strip()) + defaults['participation_status'] = status_obj + else: + # Default to 'Invited' if not specified + status_obj, _ = ParticipationStatus.objects.get_or_create(tenant=tenant, name='Invited') + defaults['participation_status'] = status_obj""" + +new_block = """ defaults = {} + if participation_status_val and participation_status_val.strip(): + status_obj, _ = ParticipationStatus.objects.get_or_create(tenant=tenant, name=participation_status_val.strip()) + defaults['participation_status'] = status_obj + else: + # Default to 'Invited' if not specified + status_obj, _ = ParticipationStatus.objects.get_or_create(tenant=tenant, name='Invited') + defaults['participation_status'] = status_obj""" + +if old_block in content: + new_content = content.replace(old_block, new_block) + with open('core/admin.py', 'w') as f: + f.write(new_content) + print("Patch applied successfully") +else: + print("Old block not found") diff --git a/patch_voter_detail_events.py b/patch_voter_detail_events.py new file mode 100644 index 0000000..7757837 --- /dev/null +++ b/patch_voter_detail_events.py @@ -0,0 +1,44 @@ + +import sys + +with open('core/templates/core/voter_detail.html', 'r') as f: + content = f.read() + +old_thead = ''' + Date + Event Type + Status + Description + Actions + ''' +new_thead = ''' + Date + Event Name + Event Type + Status + Description + Actions + ''' + +if old_thead in content: + content = content.replace(old_thead, new_thead) +else: + print("Warning: old_thead not found") + +old_tbody = ''' {% for participation in event_participations %} + + {{ participation.event.date|date:"M d, Y" }} + {{ participation.event.event_type.name }}''' +new_tbody = ''' {% for participation in event_participations %} + + {{ participation.event.date|date:"M d, Y" }} + {{ participation.event.name|default:"(No Name)" }} + {{ participation.event.event_type.name }}''' + +if old_tbody in content: + content = content.replace(old_tbody, new_tbody) +else: + print("Warning: old_tbody not found") + +with open('core/templates/core/voter_detail.html', 'w') as f: + f.write(content)