From 2e087bcd88c205bba6118b9db1426353ddc2c7b9 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Mon, 26 Jan 2026 17:50:38 +0000 Subject: [PATCH] Autosave: 20260126-175038 --- core/__pycache__/admin.cpython-311.pyc | Bin 83837 -> 84739 bytes core/__pycache__/forms.cpython-311.pyc | Bin 16169 -> 19392 bytes core/__pycache__/models.cpython-311.pyc | Bin 26091 -> 26385 bytes core/__pycache__/urls.cpython-311.pyc | Bin 2419 -> 2638 bytes core/__pycache__/views.cpython-311.pyc | Bin 27736 -> 35974 bytes core/admin.py | 29 ++- core/forms.py | 53 ++++- core/migrations/0021_voter_phone_type.py | 18 ++ .../0021_voter_phone_type.cpython-311.pyc | Bin 0 -> 932 bytes core/models.py | 11 +- core/templates/core/index.html | 7 +- .../templates/core/voter_advanced_search.html | 197 ++++++++++++++++++ core/templates/core/voter_detail.html | 7 + core/urls.py | 2 + core/views.py | 136 +++++++++++- 15 files changed, 451 insertions(+), 9 deletions(-) create mode 100644 core/migrations/0021_voter_phone_type.py create mode 100644 core/migrations/__pycache__/0021_voter_phone_type.cpython-311.pyc create mode 100644 core/templates/core/voter_advanced_search.html diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index bcabe2c7ac1ec249da8fa56db60777c6adb2e64b..ac9121497bafa660c18e09b444100f67902deb0f 100644 GIT binary patch delta 11280 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 delta 10507 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 EA2x5nkpKVy diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc index d062e201e686e2e2b962305ce4d37c400ce55e02..92d31a34a9b98df7c6269daa1cf3492be57f1b5b 100644 GIT binary patch literal 19392 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 PFOO)CywQ7JZvgARl6J9_ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index fcae47b50e90e322205193790e725dda46c87f6d..7fdfe9b1c1eab6d01fec947813bb13ac26074b63 100644 GIT binary patch delta 2978 zcma)63v5$W7{2GOE#>antzEaWZtWO@%43c(*&3Ypo%y&8bTysBqS>8`A_%qZZY2Ee)-S;f9HRl z|D2njk-Zm5%#rA5NnqdUeVg2K3f_wO+$!ocaMhaWUoFU*S4opj6qHeG^~k|b8O>IF z2d-poK-U!^2*=qU{+c2=KdOH%kCOF)(On|ru)mKJt>Q?5$S%Sd?C)d5h<*$^=w>)2 zR_$bjKyjR7=yxmY87E4WOg!f#3~~a|n9Q6Z*%(!!VnzsMQgI~YfGaJ*KeUY45q6qY zJ4fOqsW@>X_(@iAcm`dpkwaT=;iNGG)f(h@-j#$d4dbyt{57TgPo7m3ba-NZp(#~G zGAcUR6i8G}P8w&~`cq&P@`D(14tABFz6$?UV7i*)AE8xaIqARWB7<|r4WwfZB&mip zQxz&siI9_rmE|8ljHZz-!)OzRv9z*99ur7W9gKtX9vDbVAF!&@nkd`l)a|5c(oSI& zk<*OZ^-Ysy+jgO8%1+@C(v(#%IMU&5`^6$hghLEz)_B^L&C)!EwuNgv>lM_kRNC4$ zOQj44b(=hN-DYVq!>|sXOrNIjWO*DTv=h#z&z!W6EhuW}XoP1NI3lPW6iQ?J>h|WtM zt*@pBm>2p!WaSmbpJk#KpDCM-T2TEl`18h+pW$%cRDa0gTDCB0Jsd zo$mGy5A`{W6t5IbLg+!zBA6J2VqCIJ6|dLn^-)Ff(Fs6y-cxLTIBI5bc)yYuyU2Hy{Dije>X{n))`xf1XpLI zrrgyDapq26w>AG%Zcl88O86?PRw>VCltZo$EVtaP5~ceoy?BNW*|-5$#7bFqvr!mR>} zg_pq_fh`G19XMG|-=iH0Bo^+_T|o!E@JgYD+<~-fH#T=0T6zpEmkcdi7r^tg?pr2nGj9tcz;Xg5%gk`5Sb{H# ze>FUZL2U?W3>xSj__73u30BRnNsDAJKE2D-p=hOzb=~b*@078eEaXSQrP<>pHb4SJ zhy`uw(?lQaENviG^vZl8Z7+jkBAtmczY74Q>@2avM`e%e&^a|hRe6ps5gpxwC(6gg z#-WIi#D4EtD72=)*X3`LIJniA96U7lWnv$^RR!n*a}S3rVv<8-cW>GJud=>JbuVf( z)PeGKgdY%YF!18bZnN38hUdB$TniG|$!}Z`Ouor1Y*YA*nHo;q z#j|k(M*1gXFq2f7!E(&1G&wLP#pj4>SqW5!b+BJ34o9-05+O6#UTG(yl|YX^3654B zWPK^GzD5cmv!>d_X|?F(LEOOGk_}JQq)T>OBjmx!nr)&zyuWVYO%e}9wTsPJXs^Vb zRSejB!;dl>0<{ZB8QiO_8^@h--`U8;%M~V3lq{&5PQVjomU2)=D26?CyTqJutlav` z=6JO5S8YE=5$OxK_eJnm*V>OoRMQGv8X2iCfsoGC?%BBBM@>uzhsqKm&|o0d@NUDB z2?MjnjkFcFPGGc;0Ne)ke_Z$O$hxWk+oMb;h2rX1}oYjHZM+4L2WsEP}tsB*Tv7OUX-cZMjw3 z$Q*fqINu;%IN_XO8js#nk;w;O4obhnU(QKVJz5af!L-&rq#izOoo;-K*+WqqXu delta 2714 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 Iaip&FKWtbbX8-^I delta 5121 zcmb7HYj9L&8Q!zkWUom`APKo_LV#?5kWj9civ^P8_(2&I#ucD48yO=)Y13(&R)Z&*v;nI z@A{tidEe)|oX5`CCnluei-CaGMxRd~J)%~8_EfN5ik_O=x>2&pHrb&$ldh;sl%1M8 z>4|!h714@hWwcVXxioLm7xg9mQNO6WwLmf$4N}=7S7@PRRrJ)8z2l-CA?3rBU~&`CtM=W zAY3Zf6E2e*2$#!^ge&BkgrAa|2=AAr-L^>cO+0&hL^A!G^>iw8UZRhizhWQEtZ>v; zN^|&jf4$`63IE#KUK)AFyFQ&};XYMYjfkCfWM1>Hm!wW^59(5Yj|P`@g=myn8BZjb zl874$tE4)cfyO{WF=A;J>t_l&*7A3Pi^qfLt^m{nYA~=O&18ke4j*N$s5(;VBWyEz zT?CP!>0!#lBZ_XYE$FV%6;0_k6gg%nsd&nWsWRJ&(c1vq0XqPFfCmW7>i&3gIIbpA zF`Yji^10WdXB~eg)F&k}w?pTg?sl}a@w2r}O$jPSDwqc?0l+N4E`XQ+xpvwTHx(le z7Dr1Ppp!s%5QM{FHbB2-$Z|23doI&>r0#$;m^o4RkwnNqmK*^r;c38`vC_5LP3vE$#m(%nrfI1O-Y%~p|qN^RO7W0bQpt? zAa=rLbC4+bSh#nNLJc}xpp0cdR^q&)X;NxTQ%P*6GH-`ZIw)z;j-;bkWI6>y`NW;I zTiSs6vETxsN;RE|$x!=7{^8t~5b_%Fn=WCNf-t-Dms?ANz)r$uX#gzl-$uuS0Kxq< zN@oDy0Xzd};N6k>U1F8E1z)qmB0x)`!2D?p6_oK9i?2o3Z;h4}%C^Ryqu$5pU)T7- zfoA^tqpSE+1GO1nmtCSL1m`{P@QEn2!ye}sx>rlzZiC?buya9=Fg4eUiKj@4{RQNH8-2IW2+mt{rn7NKO%a$|8*Ro!F~ukjd_bd zM(HO2k*@+}p$#WG1UFB4Zs>oR!1SpFcXW@9H!tzd7(hpee2W!zvRC=)C4Z9!`LRO{ znbk{wCe6wd%_+MBR>7AVzJ+opuU_6L@-4E>UP0r}2_jWx^DcJ#DT$WjD98(oJyG% zH0`6gWMWW(ZeVJa$-cwxZ`UJ&nHylO0CCCuLB|GNKN zmf3+t?@2QY%sA|VSx&F~-0J#C>AO?62HG7}Vn+P%Dmk5sL&*9R2^G7tNa)PG?N6xO zIxs6@E8_JsKd`2;R_N3Zo#y;tZ}G3Kxltt2a(QE~lf|*>^;zG;Bg5E&@6Oz>Y1kCJ zaEq~4WI0|%LVXAWkvnUd7Aw`4bJ~(A4G{;=Z!oy5N&`ZrxmoNI53Rd&FNqddPNh%5 zvb;V8%Tj%^U%|a1f;oN>_ZDlX`7K%s;uLhsiZhfIXP(eyn(3BE#^1N`@V;)odBX@D zBo{ZVv$ubYMrjglH&64~n>sD2o~4#ZZP^7^kYf|P6jGlg$0~Fb?h#i|Eyr18a?<7+ z$fhtW(LZi#nl2<7ghVy<3yP)=rs+hx#(kSVqRd^~(2!}{@=yDeB4y(7zk?5N18{OO z!LP0?KjFMsZl(N8f4`>0Q*;L=rN@it2~YQr!z>8VQTau>%jgzkak`YII$|+40|hpv z>b(k6P_J;E4wT$Ro4e`?4Hss!2^x{{?T~S27~Z+N{6^cjH6Oe%Q~K%tA1;-%h>3sd znA}^$xxIDOJ8-_BUqPs>eyg+kEh1y`@m=4|F3DxP$Nn$fUfbI@d5f(n-eQ$eC1vOz zb5XMPp=P*UPSDlpC=S~;LA%A*`U6=`X-X$CS-+Slql@DdtFamBJpYKs=3i5)P zQ2QkEzsKwXWlQ8}1F<6Vg0@ik5IWp|ZHPcl=2gRSW~lw@usGyOm3|$gh0?*{Zl%{l z>EGB_=lv^nZ%2gYBSM)k@26L2>lTHJMO7Q@v;(Zra8Im#k|3A}Lis&f1mWKD7H3dS zK03PD|l| zfoan1%-(_5q#nU{rRnI`M@_q$HhuC?Je5dy4;sd>_3&KF2-9@tk_>xoBr(V z5ksZnt^9i@8^&LP?F2vroB})tI1hLg@CM*bz*~Sz036Tk8UTAXGXXaM5IB1ua2p`B zfFOvw^__~9m%R%d4!sdNM{7u*@9}#~OwvH>M-^}P&gHq3g Z?dX+H^3B|O{E$>->y&m%w*)e{^gqAlz{~&u diff --git a/core/admin.py b/core/admin.py index 40b43cc..54572ac 100644 --- a/core/admin.py +++ b/core/admin.py @@ -35,6 +35,7 @@ VOTER_MAPPABLE_FIELDS = [ ('zip_code', 'Zip Code'), ('county', 'County'), ('phone', 'Phone'), + ('phone_type', 'Phone Type'), ('email', 'Email'), ('district', 'District'), ('precinct', 'Precinct'), @@ -203,7 +204,7 @@ class VolunteerEventInline(admin.TabularInline): @admin.register(Voter) class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin): list_display = ('first_name', 'last_name', 'nickname', 'voter_id', 'tenant', 'district', 'candidate_support', 'is_targeted', 'city', 'state', 'prior_state') - list_filter = ('tenant', 'candidate_support', 'is_targeted', 'yard_sign', 'district', 'city', 'state', 'prior_state') + list_filter = ('tenant', 'candidate_support', 'is_targeted', 'phone_type', 'yard_sign', 'district', 'city', 'state', 'prior_state') search_fields = ('first_name', 'last_name', 'nickname', 'voter_id', 'address', 'city', 'state', 'prior_state', 'zip_code', 'county') inlines = [VotingRecordInline, DonationInline, InteractionInline, VoterLikelihoodInline] readonly_fields = ('address',) @@ -317,12 +318,34 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin): if 'window_sticker' in voter_data: if voter_data['window_sticker'] not in dict(Voter.WINDOW_STICKER_CHOICES): voter_data['window_sticker'] = 'none' + if 'phone_type' in voter_data: + pt_val = str(voter_data['phone_type']).lower() + pt_choices = dict(Voter.PHONE_TYPE_CHOICES) + if pt_val not in pt_choices: + # Try to match by display name + found = False + for k, v in pt_choices.items(): + if v.lower() == pt_val: + voter_data['phone_type'] = k + found = True + break + if not found: + voter_data['phone_type'] = 'cell' + else: + voter_data['phone_type'] = pt_val - Voter.objects.update_or_create( + voter, created = Voter.objects.get_or_create( tenant=tenant, voter_id=voter_id, - defaults=voter_data ) + for key, value in voter_data.items(): + setattr(voter, key, value) + + # Flag that coordinates were provided in the import to avoid geocoding + if "latitude" in voter_data and "longitude" in voter_data: + voter._coords_provided_in_import = True + + voter.save() count += 1 except Exception as e: logger.error(f"Error importing: {e}") diff --git a/core/forms.py b/core/forms.py index d9ca81b..a281af4 100644 --- a/core/forms.py +++ b/core/forms.py @@ -7,7 +7,7 @@ class VoterForm(forms.ModelForm): fields = [ 'first_name', 'last_name', 'nickname', 'birthdate', 'address_street', 'city', 'state', 'prior_state', 'zip_code', 'county', 'latitude', 'longitude', - 'phone', 'email', 'voter_id', 'district', 'precinct', + 'phone', 'phone_type', 'email', 'voter_id', 'district', 'precinct', 'registration_date', 'is_targeted', 'candidate_support', 'yard_sign', 'window_sticker' ] widgets = { @@ -30,6 +30,55 @@ class VoterForm(forms.ModelForm): self.fields['candidate_support'].widget.attrs.update({'class': 'form-select'}) self.fields['yard_sign'].widget.attrs.update({'class': 'form-select'}) self.fields['window_sticker'].widget.attrs.update({'class': 'form-select'}) + self.fields['phone_type'].widget.attrs.update({'class': 'form-select'}) + +class AdvancedVoterSearchForm(forms.Form): + MONTH_CHOICES = [ + ('', 'Any Month'), + (1, 'January'), (2, 'February'), (3, 'March'), (4, 'April'), + (5, 'May'), (6, 'June'), (7, 'July'), (8, 'August'), + (9, 'September'), (10, 'October'), (11, 'November'), (12, 'December') + ] + + first_name = forms.CharField(required=False) + last_name = 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) + zip_code = forms.CharField(required=False) + district = forms.CharField(required=False) + precinct = forms.CharField(required=False) + phone_type = forms.ChoiceField( + choices=[('', 'Any')] + Voter.PHONE_TYPE_CHOICES, + required=False + ) + is_targeted = forms.BooleanField(required=False, label="Targeted Only") + candidate_support = forms.ChoiceField( + choices=[('', 'Any')] + Voter.SUPPORT_CHOICES, + required=False + ) + yard_sign = forms.ChoiceField( + choices=[('', 'Any')] + Voter.YARD_SIGN_CHOICES, + required=False + ) + window_sticker = forms.ChoiceField( + choices=[('', 'Any')] + Voter.WINDOW_STICKER_CHOICES, + required=False + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for field in self.fields.values(): + if isinstance(field.widget, forms.CheckboxInput): + field.widget.attrs.update({'class': 'form-check-input'}) + else: + field.widget.attrs.update({'class': 'form-control'}) + + self.fields['birth_month'].widget.attrs.update({'class': 'form-select'}) + self.fields['candidate_support'].widget.attrs.update({'class': 'form-select'}) + self.fields['yard_sign'].widget.attrs.update({'class': 'form-select'}) + self.fields['window_sticker'].widget.attrs.update({'class': 'form-select'}) + self.fields['phone_type'].widget.attrs.update({'class': 'form-select'}) class InteractionForm(forms.ModelForm): class Meta: @@ -173,4 +222,4 @@ class VolunteerImportForm(forms.Form): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields['tenant'].widget.attrs.update({'class': 'form-control form-select'}) - self.fields['file'].widget.attrs.update({'class': 'form-control'}) + self.fields['file'].widget.attrs.update({'class': 'form-control'}) \ No newline at end of file diff --git a/core/migrations/0021_voter_phone_type.py b/core/migrations/0021_voter_phone_type.py new file mode 100644 index 0000000..81f3f7a --- /dev/null +++ b/core/migrations/0021_voter_phone_type.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2026-01-26 16:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0020_remove_volunteer_name'), + ] + + operations = [ + migrations.AddField( + model_name='voter', + name='phone_type', + field=models.CharField(choices=[('home', 'Home Phone'), ('cell', 'Cell Phone'), ('work', 'Work Phone')], default='cell', max_length=10), + ), + ] diff --git a/core/migrations/__pycache__/0021_voter_phone_type.cpython-311.pyc b/core/migrations/__pycache__/0021_voter_phone_type.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..af9f7b9d6ddf1d226f643262d50e1181a2e64dab GIT binary patch literal 932 zcmZuwOKa6Y6h4#W=H3h~XcYu)ODQ4-p{cHmAQdg5MX4y<&5&kJ@1uF#PLjSB(uE6` z#ZC7v6tRE7|IkGjC0lXPT}r#^$}_q5wv;*(zB!MXGv9oN{1_eOK;!qN@JBnt zQ5zVib7bs+1j#~(Sd&FY(_mo0OOVEAkS4OOIXv{8tX~&i)0EZ{xV2-Qc8!8ZotE;m zP7sM*)H?I0rtsxyE#tY}okP*?df<$3)cXsy4tWX#w>nPTfFpo1tC1O9qqPCu7s0!X| zCozg_tB04PsHleS?ORX7hR`lh$=2b!G)lGd5C0>2(gy?q28-K zi9L)Z#z6;DrJGbVo=N#a*HD&ro%yz}D!<@K4+pLK38$2V^94^o)617hk=(;g4Du{d zo_^8@s>eBNb?JreRUDM*TmOTlJ(Tgs+#6=)+}BiOl^r45c(n#OH==XJ@!8UMl$ z8~2XxOdj2wI2xPYeQ-E7eefvlet_S~T5ow1~sTknmN3ePGm>z2b$fZ$9+Y1(7< z&MQ6D#E`?HYZZ>{`f)3nk;|1Xl^T7P<9b`Qcap60##~?Ar)r8^`eh*fKv0^DF=Ec# c19*7-EA0_xmv9|+`QeTGCpvNVpKNvSKdCeIumAu6 literal 0 HcmV?d00001 diff --git a/core/models.py b/core/models.py index 9198711..e847349 100644 --- a/core/models.py +++ b/core/models.py @@ -126,6 +126,11 @@ class Voter(models.Model): ('wants', 'Wants Sticker'), ('has', 'Has Sticker'), ] + PHONE_TYPE_CHOICES = [ + ('home', 'Home Phone'), + ('cell', 'Cell Phone'), + ('work', 'Work Phone'), + ] tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name='voters') voter_id = models.CharField(max_length=50, blank=True) @@ -143,6 +148,7 @@ class Voter(models.Model): latitude = models.DecimalField(max_digits=12, decimal_places=9, null=True, blank=True) longitude = models.DecimalField(max_digits=12, decimal_places=9, null=True, blank=True) phone = models.CharField(max_length=20, blank=True) + phone_type = models.CharField(max_length=10, choices=PHONE_TYPE_CHOICES, default='cell') email = models.EmailField(blank=True) district = models.CharField(max_length=100, blank=True) precinct = models.CharField(max_length=100, blank=True) @@ -239,9 +245,12 @@ class Voter(models.Model): self.state != orig.state or self.zip_code != orig.zip_code) - # Detect if coordinates were changed in this transaction (e.g., from a form) coords_provided = (self.latitude != orig.latitude or self.longitude != orig.longitude) + # If specifically provided in import, treat as provided even if same as DB + if getattr(self, "_coords_provided_in_import", False): + coords_provided = True + # Auto-geocode if address changed AND coordinates were NOT manually updated if address_changed and not coords_provided: should_geocode = True diff --git a/core/templates/core/index.html b/core/templates/core/index.html index cde8d7b..8000f67 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -30,7 +30,7 @@ -

+
@@ -42,6 +42,11 @@
+
diff --git a/core/templates/core/voter_advanced_search.html b/core/templates/core/voter_advanced_search.html new file mode 100644 index 0000000..731ed40 --- /dev/null +++ b/core/templates/core/voter_advanced_search.html @@ -0,0 +1,197 @@ +{% extends "base.html" %} + +{% block content %} +
+
+

Advanced Voter Search

+ Back to Registry +
+ +
+
+
+
+ + {{ form.first_name }} +
+
+ + {{ form.last_name }} +
+
+ + {{ form.voter_id }} +
+
+ + {{ form.birth_month }} +
+
+ + {{ form.city }} +
+
+ + {{ form.zip_code }} +
+
+ + {{ form.district }} +
+
+ + {{ form.precinct }} +
+
+ + {{ form.phone_type }} +
+
+ + {{ form.candidate_support }} +
+
+ + {{ form.yard_sign }} +
+
+ + {{ form.window_sticker }} +
+
+
+ {{ form.is_targeted }} + +
+
+
+ Clear Filters + +
+
+
+
+ +
+ {% csrf_token %} + + {% for key, value in request.GET.items %} + {% if key != 'csrfmiddlewaretoken' %} + + {% endif %} + {% endfor %} + +
+
+
Search Results ({{ voters.count }})
+
+ +
+
+ +
+
+
+ + + + + + + + + + + + + {% for voter in voters %} + + + + + + + + + {% empty %} + + + + {% endfor %} + +
+ + NameDistrictPhoneTarget VoterSupporter
+ + + + {{ voter.first_name }} {{ voter.last_name }} + +
{{ voter.address|default:"No address provided" }}
+
{{ voter.district|default:"-" }} + {{ voter.phone|default:"-" }} + {% if voter.phone %} +
{{ voter.get_phone_type_display }}
+ {% endif %} +
+ {% if voter.is_targeted %} + Yes + {% else %} + No + {% endif %} + + {% if voter.candidate_support == 'supporting' %} + Supporting + {% elif voter.candidate_support == 'not_supporting' %} + Not Supporting + {% else %} + Unknown + {% endif %} +
+

No voters found matching your search criteria.

+
+
+
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/voter_detail.html b/core/templates/core/voter_detail.html index 0e1f86f..c4d3b35 100644 --- a/core/templates/core/voter_detail.html +++ b/core/templates/core/voter_detail.html @@ -88,6 +88,9 @@
  • {{ voter.phone|default:"N/A" }} + {% if voter.phone %} + {{ voter.get_phone_type_display }} + {% endif %}
  • @@ -436,6 +439,10 @@ {{ voter_form.phone }}
  • + + {{ voter_form.phone_type }} +
    +
    {{ voter_form.email }}
    diff --git a/core/urls.py b/core/urls.py index a1d2ea2..0c1423e 100644 --- a/core/urls.py +++ b/core/urls.py @@ -5,6 +5,8 @@ urlpatterns = [ path('', views.index, name='index'), path('select-campaign//', views.select_campaign, name='select_campaign'), 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//', views.voter_detail, name='voter_detail'), path('voters//edit/', views.voter_edit, name='voter_edit'), path('voters//geocode/', views.voter_geocode, name='voter_geocode'), diff --git a/core/views.py b/core/views.py index bb4a0e2..8003067 100644 --- a/core/views.py +++ b/core/views.py @@ -1,12 +1,12 @@ import csv import io -from django.http import JsonResponse +from django.http import JsonResponse, HttpResponse from django.urls import reverse from django.shortcuts import render, redirect, get_object_or_404 from django.db.models import Q, Sum from django.contrib import messages from .models import Voter, Tenant, Interaction, Donation, VoterLikelihood, EventParticipation, Event, EventType, InteractionType, DonationMethod, ElectionType, CampaignSettings, Volunteer -from .forms import VoterForm, InteractionForm, DonationForm, VoterLikelihoodForm, EventParticipationForm, VoterImportForm +from .forms import VoterForm, InteractionForm, DonationForm, VoterLikelihoodForm, EventParticipationForm, VoterImportForm, AdvancedVoterSearchForm import logging from django.utils import timezone @@ -399,3 +399,135 @@ def voter_geocode(request, voter_id): }) return JsonResponse({'success': False, 'error': 'Invalid request method.'}) + +def voter_advanced_search(request): + """ + Advanced search for voters with multiple filters. + """ + 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) + voters = Voter.objects.filter(tenant=tenant).order_by("last_name", "first_name") + + form = AdvancedVoterSearchForm(request.GET) + if form.is_valid(): + data = form.cleaned_data + if data.get('first_name'): + 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('voter_id'): + voters = voters.filter(voter_id__icontains=data['voter_id']) + if data.get('birth_month'): + voters = voters.filter(birthdate__month=data['birth_month']) + if data.get('city'): + voters = voters.filter(city__icontains=data['city']) + if data.get('zip_code'): + voters = voters.filter(zip_code__icontains=data['zip_code']) + if data.get('district'): + voters = voters.filter(district__icontains=data['district']) + if data.get('precinct'): + voters = voters.filter(precinct__icontains=data['precinct']) + if data.get('phone_type'): + voters = voters.filter(phone_type=data['phone_type']) + if data.get('is_targeted'): + voters = voters.filter(is_targeted=True) + if data.get('candidate_support'): + voters = voters.filter(candidate_support=data['candidate_support']) + if data.get('yard_sign'): + voters = voters.filter(yard_sign=data['yard_sign']) + if data.get('window_sticker'): + voters = voters.filter(window_sticker=data['window_sticker']) + + context = { + 'form': form, + 'voters': voters, + 'selected_tenant': tenant, + } + return render(request, 'core/voter_advanced_search.html', context) + +def export_voters_csv(request): + """ + Exports selected or filtered voters to a CSV file. + """ + 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': + return redirect('voter_advanced_search') + + action = request.POST.get('action') + voters = Voter.objects.filter(tenant=tenant) + + if action == 'export_selected': + voter_ids = request.POST.getlist('selected_voters') + voters = voters.filter(id__in=voter_ids) + else: # export_all + # Re-apply filters from hidden inputs + # These are passed as filter_fieldname + filters = {} + for key, value in request.POST.items(): + if key.startswith('filter_') and value: + field_name = key.replace('filter_', '') + filters[field_name] = value + + # We can use the AdvancedVoterSearchForm to validate and apply filters + # but we need to pass data without the prefix + form = AdvancedVoterSearchForm(filters) + if form.is_valid(): + data = form.cleaned_data + if data.get('first_name'): + 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('voter_id'): + voters = voters.filter(voter_id__icontains=data['voter_id']) + if data.get('birth_month'): + voters = voters.filter(birthdate__month=data['birth_month']) + if data.get('city'): + voters = voters.filter(city__icontains=data['city']) + if data.get('zip_code'): + voters = voters.filter(zip_code__icontains=data['zip_code']) + if data.get('district'): + voters = voters.filter(district__icontains=data['district']) + if data.get('precinct'): + voters = voters.filter(precinct__icontains=data['precinct']) + if data.get('phone_type'): + voters = voters.filter(phone_type=data['phone_type']) + if data.get('is_targeted'): + voters = voters.filter(is_targeted=True) + if data.get('candidate_support'): + voters = voters.filter(candidate_support=data['candidate_support']) + if data.get('yard_sign'): + voters = voters.filter(yard_sign=data['yard_sign']) + if data.get('window_sticker'): + voters = voters.filter(window_sticker=data['window_sticker']) + + voters = voters.order_by('last_name', 'first_name') + + response = HttpResponse(content_type='text/csv') + response['Content-Disposition'] = f'attachment; filename="voters_export_{timezone.now().strftime("%Y%m%d_%H%M%S")}.csv"' + + writer = csv.writer(response) + writer.writerow([ + 'Voter ID', 'First Name', 'Last Name', 'Nickname', 'Birthdate', + 'Address', 'City', 'State', 'Zip Code', 'Phone', 'Phone Type', 'Email', + 'District', 'Precinct', 'Is Targeted', 'Support', 'Yard Sign', 'Window Sticker' + ]) + + for voter in voters: + writer.writerow([ + voter.voter_id, voter.first_name, voter.last_name, voter.nickname, voter.birthdate, + voter.address, voter.city, voter.state, voter.zip_code, voter.phone, voter.get_phone_type_display(), voter.email, + voter.district, voter.precinct, 'Yes' if voter.is_targeted else 'No', + voter.get_candidate_support_display(), voter.get_yard_sign_display(), voter.get_window_sticker_display() + ]) + + return response \ No newline at end of file