From d756ed7a8afd67fd4bf54dc8d92c1d577d469078 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sun, 25 Jan 2026 05:02:55 +0000 Subject: [PATCH] Autosave: 20260125-050254 --- core/__pycache__/admin.cpython-311.pyc | Bin 19044 -> 46823 bytes core/__pycache__/forms.cpython-311.pyc | Bin 12269 -> 15106 bytes core/__pycache__/models.cpython-311.pyc | Bin 21651 -> 21728 bytes core/admin.py | 533 +++++++++++++++++- core/forms.py | 40 +- ...012_voter_prior_state_alter_voter_state.py | 23 + ...or_state_alter_voter_state.cpython-311.pyc | Bin 0 -> 989 bytes core/models.py | 3 +- .../templates/admin/donation_change_list.html | 9 + .../admin/eventparticipation_change_list.html | 7 + .../admin/interaction_change_list.html | 9 + .../admin/voterlikelihood_change_list.html | 9 + core/templates/core/voter_detail.html | 14 +- 13 files changed, 636 insertions(+), 11 deletions(-) create mode 100644 core/migrations/0012_voter_prior_state_alter_voter_state.py create mode 100644 core/migrations/__pycache__/0012_voter_prior_state_alter_voter_state.cpython-311.pyc create mode 100644 core/templates/admin/donation_change_list.html create mode 100644 core/templates/admin/eventparticipation_change_list.html create mode 100644 core/templates/admin/interaction_change_list.html create mode 100644 core/templates/admin/voterlikelihood_change_list.html diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index ccb9d815387e818e99188eccd1aa4e0029773033..257d10335fed23df5a02915ff8d56672be0c0fcd 100644 GIT binary patch literal 46823 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 delta 3677 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 delta 2152 zcmbtVL2MLN7-qJ+)7=hRwv;VPWp&H0&~4dLTcA|hf`Jw-uqhN)jlyKu9kN5GJN30wQ?j81$_=Zn%*Wk?1ZJ|_bH#3)|%LbXun5wSkEwTrT?Q|@( zUFf03P_M9;ei<4T55YZT{~0%th9S)5d@qET=}Wzv`~S+0y+XGok)hibK~#|F>Pk{qCsWK77@ zNb||fd?#FgmgMaF$-Bbbn@B4F*3k`xP2K{$4d9n`0oFx;S2-dxpZSzL%sCT1Zj=<% z%?PXFE?L$}8l8#$xQ=QH-Vf;Is=e1za_?W^Vx8W;&{|X4K_s{;aN1XBU97`Ho8C|E zu+PSRu2P>sf|okcna@)<5`s3)e`k($3pu4tsJd1(=vS@FiT^K{BK9oOAD+A6QZ91J zm7Sl~QVt_<4dr*J8t;6X^7rwdgt;7I3803u$$TEln-R2e$}bF;)si))5KGHzQ`Db$ zybfgpdm1#}R;6rk%E4WWI&X;v74MR*Yf~yYgHZ3-A>;6!W3V!H5cG_x!>PG~A!Bvw zkn?zS+vO_lN)|jK@^&fmcDaT)uYexy8nM1H?daz({<=D1_9mc%CClYz-Q z-P_+4zdIS93`IT<{Va9uG`y3(w-v=*39lVgbf6<~1j;ydimvmmozMt7(uk(&1#+FO zxRd%MtlAkm3+F8TqT{C960@A`qt2$t`&jt^Fc0{Uu?q_A3$Q;1e8Rx2IU;k-#CUnq zGy9wAkxXFd24Z n=iq8Oz8}lMfk~qyBJ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index d5fb4535a8c7bfd62cf473eca7b4d2619e6a7570..e220ca6ac4c7450d1e0889eb2e8acaae6d067ba6 100644 GIT binary patch delta 1492 zcma)5ZA@Eb6z;h#g_Z)PEtK02w$T6rrG^cHXY;IB zXe-4OJ|DJWi_OIsD6H#|f;n!P1a=p|xQ$QE64gRZJQu&WZ5hfEHG(ysJE>+Y+sPFy zdi(}rieMG;;{0PB6)Sm9ZPu}#ZAqWaNU%i67wn@DEFDuF2f;C#7AzB-qpF~LO!W!` z%Qfw)k}YzxJrlSkPHuO6poa;WD;6~n$Ct`{Fof@yjl+4|=`PpCnW!{@-R^3*gp=;Q za9Y0Wen+DlqWEP7_;cxQv-BZ5Md=M~-6}bV`Ur1VxZnn^R#ZJVLjmejI?o`H84?6Z zj7E<}W0EMwq|b=CO7J1V#nP(GjX=|1z2HH#j<~IEoj#% z?KDp7c%0hT4a;cu9RV#ye1kcPLJRR~_IWC^m6mbcSD{~{Ai+9%>)wMkx*7gfw_trh z#2CRz1|^(yiq0drxIdWlCq>i}wVBX{`e>HYD*l+kqgbB}SvXkVnVy^h)vRKo-mY7u zqk*~ON>h$LjjlnULubS5TpD1uDGz5G{(%R0qcLPoXVj*HCU&A<+|WPNWC0$JH+9++ z{Z6uchSzmEyw-9g zLuQH_#*!0V{%4^C`}~8+cDl&UiTC^-eJ+&}IIy_&OD;Frz{A!HcI7vrnZ3xiYWTXw z$MjB7^XQDb|4?f3ei!&XftiwLKTqg|oz+qB&)GriB;y zp0X_s)}-|r@7-d7Xq$vU`IO=~i1x|sK!xa-R0Jxg6t6*`s#{TBC`au&kWV=lv@k7K zmW9=@fafb)U>tv}JO{IQtjZ}YFbioKv8n++!0)P>;f$mayJF1(ba9=*z90uVpN}s`N+pM^;F9K_LtmPn81YOi1;BiR1q}zEz&hr)_3hBI)K&@V#OovBOaXYQ%?K8h+j?!> z(@T=Mq-7w;oH3TRyR>>*6XavCeO9UG2D0VQ1z@LlwZoZZXSq_m-7%5(5fj<;k{meF zc?fpn&CanL?l3?O4s7(gv?h`yuw%mexzd!W+56HJJAd=pEb|IuE0SNTTbN#wcAiF^ zZww~ztk0RXixFJ7==%`dc--F$tN4Y#N?S~-1Qod9pMhe0>+mV#1-3VxJ1Rw^gZ-|N zJ}DlS-a|`Q0epgWU9K1TqvIFP@0cGZZ 0: + self.message_user(request, f"Failed to import {errors} rows.", level=messages.WARNING) + return redirect("..") + except Exception as 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(): + csv_file = request.FILES['file'] + tenant = form.cleaned_data['tenant'] + + if not csv_file.name.endswith('.csv'): + self.message_user(request, "Please upload a CSV file.", level=messages.ERROR) + return redirect("..") + + with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp: + for chunk in csv_file.chunks(): + tmp.write(chunk) + file_path = tmp.name + + with open(file_path, 'r', encoding='UTF-8') as f: + reader = csv.reader(f) + headers = next(reader) + + context = self.admin_site.each_context(request) + context.update({ + 'title': "Map Participation Fields", + 'headers': headers, + 'model_fields': EVENT_PARTICIPATION_MAPPABLE_FIELDS, + 'tenant_id': tenant.id, + 'file_path': file_path, + 'action_url': request.path, + 'opts': self.model._meta, + }) + return render(request, "admin/import_mapping.html", context) + else: + form = EventParticipationImportForm() + + context = self.admin_site.each_context(request) + context['form'] = form + context['title'] = "Import Participations" + context['opts'] = self.model._meta + return render(request, "admin/import_csv.html", context) + +@admin.register(Donation) +class DonationAdmin(admin.ModelAdmin): + list_display = ('id', 'voter', 'date', 'amount', 'method') + list_filter = ('voter__tenant', 'date', 'method') + search_fields = ('voter__first_name', 'voter__last_name', 'voter__voter_id') + change_list_template = "admin/donation_change_list.html" + + def get_urls(self): + urls = super().get_urls() + my_urls = [ + path('import-donations/', self.admin_site.admin_view(self.import_donations), name='import-donations'), + ] + return my_urls + urls + + def import_donations(self, request): + if request.method == "POST": + if "_import" in request.POST: + file_path = request.POST.get('file_path') + tenant_id = request.POST.get('tenant') + tenant = Tenant.objects.get(id=tenant_id) + + mapping = {} + for field_name, _ in DONATION_MAPPABLE_FIELDS: + mapping[field_name] = request.POST.get(f'map_{field_name}') + + try: + with open(file_path, 'r', encoding='UTF-8') as f: + reader = csv.DictReader(f) + count = 0 + errors = 0 + for row in reader: + try: + voter_id = row.get(mapping.get('voter_id')) if mapping.get('voter_id') else None + if not voter_id: + errors += 1 + continue + + try: + voter = Voter.objects.get(tenant=tenant, voter_id=voter_id) + except Voter.DoesNotExist: + errors += 1 + continue + + date = row.get(mapping.get('date')) + amount = row.get(mapping.get('amount')) + method_name = row.get(mapping.get('method')) + + if not date or not amount: + errors += 1 + continue + + method = None + if method_name: + method, _ = DonationMethod.objects.get_or_create( + tenant=tenant, + name=method_name + ) + + Donation.objects.create( + voter=voter, + date=date, + amount=amount, + method=method + ) + count += 1 + except Exception as e: + logger.error(f"Error importing donation row: {e}") + errors += 1 + + if os.path.exists(file_path): + os.remove(file_path) + self.message_user(request, f"Successfully imported {count} donations.") + if errors > 0: + self.message_user(request, f"Failed to import {errors} rows.", level=messages.WARNING) + return redirect("..") + except Exception as 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(): + csv_file = request.FILES['file'] + tenant = form.cleaned_data['tenant'] + + if not csv_file.name.endswith('.csv'): + self.message_user(request, "Please upload a CSV file.", level=messages.ERROR) + return redirect("..") + + with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp: + for chunk in csv_file.chunks(): + tmp.write(chunk) + file_path = tmp.name + + with open(file_path, 'r', encoding='UTF-8') as f: + reader = csv.reader(f) + headers = next(reader) + + context = self.admin_site.each_context(request) + context.update({ + 'title': "Map Donation Fields", + 'headers': headers, + 'model_fields': DONATION_MAPPABLE_FIELDS, + 'tenant_id': tenant.id, + 'file_path': file_path, + 'action_url': request.path, + 'opts': self.model._meta, + }) + return render(request, "admin/import_mapping.html", context) + else: + form = DonationImportForm() + + context = self.admin_site.each_context(request) + context['form'] = form + context['title'] = "Import Donations" + context['opts'] = self.model._meta + return render(request, "admin/import_csv.html", context) + +@admin.register(Interaction) +class InteractionAdmin(admin.ModelAdmin): + list_display = ('id', 'voter', 'type', 'date', 'description') + list_filter = ('voter__tenant', 'type', 'date') + search_fields = ('voter__first_name', 'voter__last_name', 'voter__voter_id', 'description') + change_list_template = "admin/interaction_change_list.html" + + def get_urls(self): + urls = super().get_urls() + my_urls = [ + path('import-interactions/', self.admin_site.admin_view(self.import_interactions), name='import-interactions'), + ] + return my_urls + urls + + def import_interactions(self, request): + if request.method == "POST": + if "_import" in request.POST: + file_path = request.POST.get('file_path') + tenant_id = request.POST.get('tenant') + tenant = Tenant.objects.get(id=tenant_id) + + mapping = {} + for field_name, _ in INTERACTION_MAPPABLE_FIELDS: + mapping[field_name] = request.POST.get(f'map_{field_name}') + + try: + with open(file_path, 'r', encoding='UTF-8') as f: + reader = csv.DictReader(f) + count = 0 + errors = 0 + for row in reader: + try: + voter_id = row.get(mapping.get('voter_id')) if mapping.get('voter_id') else None + if not voter_id: + errors += 1 + continue + + try: + voter = Voter.objects.get(tenant=tenant, voter_id=voter_id) + except Voter.DoesNotExist: + errors += 1 + continue + + date = row.get(mapping.get('date')) + type_name = row.get(mapping.get('type')) + description = row.get(mapping.get('description')) + notes = row.get(mapping.get('notes')) if mapping.get('notes') else '' + + if not date or not description: + errors += 1 + continue + + interaction_type = None + if type_name: + interaction_type, _ = InteractionType.objects.get_or_create( + tenant=tenant, + name=type_name + ) + + Interaction.objects.create( + voter=voter, + date=date, + type=interaction_type, + description=description, + notes=notes + ) + count += 1 + except Exception as e: + logger.error(f"Error importing interaction row: {e}") + errors += 1 + + if os.path.exists(file_path): + os.remove(file_path) + self.message_user(request, f"Successfully imported {count} interactions.") + if errors > 0: + self.message_user(request, f"Failed to import {errors} rows.", level=messages.WARNING) + return redirect("..") + except Exception as 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(): + csv_file = request.FILES['file'] + tenant = form.cleaned_data['tenant'] + + if not csv_file.name.endswith('.csv'): + self.message_user(request, "Please upload a CSV file.", level=messages.ERROR) + return redirect("..") + + with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp: + for chunk in csv_file.chunks(): + tmp.write(chunk) + file_path = tmp.name + + with open(file_path, 'r', encoding='UTF-8') as f: + reader = csv.reader(f) + headers = next(reader) + + context = self.admin_site.each_context(request) + context.update({ + 'title': "Map Interaction Fields", + 'headers': headers, + 'model_fields': INTERACTION_MAPPABLE_FIELDS, + 'tenant_id': tenant.id, + 'file_path': file_path, + 'action_url': request.path, + 'opts': self.model._meta, + }) + return render(request, "admin/import_mapping.html", context) + else: + form = InteractionImportForm() + + context = self.admin_site.each_context(request) + context['form'] = form + context['title'] = "Import Interactions" + context['opts'] = self.model._meta + return render(request, "admin/import_csv.html", context) + +@admin.register(VoterLikelihood) +class VoterLikelihoodAdmin(admin.ModelAdmin): + list_display = ('id', 'voter', 'election_type', 'likelihood') + list_filter = ('voter__tenant', 'election_type', 'likelihood') + search_fields = ('voter__first_name', 'voter__last_name', 'voter__voter_id') + change_list_template = "admin/voterlikelihood_change_list.html" + + def get_urls(self): + urls = super().get_urls() + my_urls = [ + path('import-likelihoods/', self.admin_site.admin_view(self.import_likelihoods), name='import-likelihoods'), + ] + return my_urls + urls + + def import_likelihoods(self, request): + if request.method == "POST": + if "_import" in request.POST: + file_path = request.POST.get('file_path') + tenant_id = request.POST.get('tenant') + tenant = Tenant.objects.get(id=tenant_id) + + mapping = {} + for field_name, _ in VOTER_LIKELIHOOD_MAPPABLE_FIELDS: + mapping[field_name] = request.POST.get(f'map_{field_name}') + + try: + with open(file_path, 'r', encoding='UTF-8') as f: + reader = csv.DictReader(f) + count = 0 + errors = 0 + for row in reader: + try: + voter_id = row.get(mapping.get('voter_id')) if mapping.get('voter_id') else None + if not voter_id: + errors += 1 + continue + + try: + voter = Voter.objects.get(tenant=tenant, voter_id=voter_id) + except Voter.DoesNotExist: + errors += 1 + continue + + election_type_name = row.get(mapping.get('election_type')) + likelihood_val = row.get(mapping.get('likelihood')) + + if not election_type_name or not likelihood_val: + errors += 1 + continue + + election_type, _ = ElectionType.objects.get_or_create( + tenant=tenant, + name=election_type_name + ) + + # Normalize likelihood + likelihood_choices = dict(VoterLikelihood.LIKELIHOOD_CHOICES) + normalized_likelihood = None + likelihood_val_lower = likelihood_val.lower().replace(' ', '_') + if likelihood_val_lower in likelihood_choices: + normalized_likelihood = likelihood_val_lower + else: + # Try to find by display name + for k, v in likelihood_choices.items(): + if v.lower() == likelihood_val.lower(): + normalized_likelihood = k + break + + if not normalized_likelihood: + errors += 1 + continue + + VoterLikelihood.objects.update_or_create( + voter=voter, + election_type=election_type, + defaults={'likelihood': normalized_likelihood} + ) + count += 1 + except Exception as e: + logger.error(f"Error importing likelihood row: {e}") + errors += 1 + + if os.path.exists(file_path): + os.remove(file_path) + self.message_user(request, f"Successfully imported {count} likelihoods.") + if errors > 0: + self.message_user(request, f"Failed to import {errors} rows.", level=messages.WARNING) + return redirect("..") + except Exception as e: + self.message_user(request, f"Error processing file: {e}", level=messages.ERROR) + return redirect("..") + else: + form = VoterLikelihoodImportForm(request.POST, request.FILES) + if form.is_valid(): + csv_file = request.FILES['file'] + tenant = form.cleaned_data['tenant'] + + if not csv_file.name.endswith('.csv'): + self.message_user(request, "Please upload a CSV file.", level=messages.ERROR) + return redirect("..") + + with tempfile.NamedTemporaryFile(delete=False, suffix='.csv') as tmp: + for chunk in csv_file.chunks(): + tmp.write(chunk) + file_path = tmp.name + + with open(file_path, 'r', encoding='UTF-8') as f: + reader = csv.reader(f) + headers = next(reader) + + context = self.admin_site.each_context(request) + context.update({ + 'title': "Map Likelihood Fields", + 'headers': headers, + 'model_fields': VOTER_LIKELIHOOD_MAPPABLE_FIELDS, + 'tenant_id': tenant.id, + 'file_path': file_path, + 'action_url': request.path, + 'opts': self.model._meta, + }) + return render(request, "admin/import_mapping.html", context) + else: + form = VoterLikelihoodImportForm() + + context = self.admin_site.each_context(request) + context['form'] = form + context['title'] = "Import Likelihoods" + context['opts'] = self.model._meta + return render(request, "admin/import_csv.html", context) @admin.register(CampaignSettings) class CampaignSettingsAdmin(admin.ModelAdmin): diff --git a/core/forms.py b/core/forms.py index 836d8bd..8e811db 100644 --- a/core/forms.py +++ b/core/forms.py @@ -5,7 +5,7 @@ class VoterForm(forms.ModelForm): class Meta: model = Voter fields = [ - 'first_name', 'last_name', 'nickname', 'birthdate', 'address_street', 'city', 'state', + 'first_name', 'last_name', 'nickname', 'birthdate', 'address_street', 'city', 'state', 'prior_state', 'zip_code', 'county', 'latitude', 'longitude', 'phone', 'email', 'voter_id', 'district', 'precinct', 'registration_date', 'is_targeted', 'candidate_support', 'yard_sign', 'window_sticker' @@ -125,4 +125,40 @@ class EventImportForm(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'}) \ No newline at end of file + self.fields['file'].widget.attrs.update({'class': 'form-control'}) + +class EventParticipationImportForm(forms.Form): + tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), label="Campaign") + file = forms.FileField(label="Select CSV file") + + 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'}) + +class DonationImportForm(forms.Form): + tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), label="Campaign") + file = forms.FileField(label="Select CSV file") + + 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'}) + +class InteractionImportForm(forms.Form): + tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), label="Campaign") + file = forms.FileField(label="Select CSV file") + + 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'}) + +class VoterLikelihoodImportForm(forms.Form): + tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), label="Campaign") + file = forms.FileField(label="Select CSV file") + + 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'}) diff --git a/core/migrations/0012_voter_prior_state_alter_voter_state.py b/core/migrations/0012_voter_prior_state_alter_voter_state.py new file mode 100644 index 0000000..5ac16d4 --- /dev/null +++ b/core/migrations/0012_voter_prior_state_alter_voter_state.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.7 on 2026-01-25 01:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0011_voter_birthdate_voter_nickname'), + ] + + operations = [ + migrations.AddField( + model_name='voter', + name='prior_state', + field=models.CharField(blank=True, max_length=2), + ), + migrations.AlterField( + model_name='voter', + name='state', + field=models.CharField(blank=True, max_length=2), + ), + ] diff --git a/core/migrations/__pycache__/0012_voter_prior_state_alter_voter_state.cpython-311.pyc b/core/migrations/__pycache__/0012_voter_prior_state_alter_voter_state.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a39dc2932da3d7b34bb265355348c4ca1b5b1a3b GIT binary patch literal 989 zcmb7By>HV%6uto(M>yM1whRm~ z(XA^}0kwYse@2lqL|&P?xk{%_yt~*1R0g>7J-^?*_j~s~KG*9ug7tBj`F9nBeh5%4 zy;NpxP<9bT6bC58E)Eq}!AL<55mnwHsxmxKE7Bfl`)9~isrD4L^dn(zDY+f`J?^D` z93{CC#*_s~w_=LFRf5?kP<9bRE~dy;DDGibRS?q#RZ{RY6*tD9R&o&4sD5DVK^gmS z=IfyLXF@WDSy>^5`TsF`ifd5|%Cd9oNmUONr*&h_^6TsC8yn + + Import Donations + + + {{ block.super }} +{% endblock %} diff --git a/core/templates/admin/eventparticipation_change_list.html b/core/templates/admin/eventparticipation_change_list.html new file mode 100644 index 0000000..201d73c --- /dev/null +++ b/core/templates/admin/eventparticipation_change_list.html @@ -0,0 +1,7 @@ +{% extends "admin/change_list.html" %} +{% block object-tools-items %} +
  • + Import Participants +
  • + {{ block.super }} +{% endblock %} \ No newline at end of file diff --git a/core/templates/admin/interaction_change_list.html b/core/templates/admin/interaction_change_list.html new file mode 100644 index 0000000..0f1a028 --- /dev/null +++ b/core/templates/admin/interaction_change_list.html @@ -0,0 +1,9 @@ +{% extends "admin/change_list.html" %} +{% block object-tools-items %} +
  • + + Import Interactions + +
  • + {{ block.super }} +{% endblock %} diff --git a/core/templates/admin/voterlikelihood_change_list.html b/core/templates/admin/voterlikelihood_change_list.html new file mode 100644 index 0000000..45091c2 --- /dev/null +++ b/core/templates/admin/voterlikelihood_change_list.html @@ -0,0 +1,9 @@ +{% extends "admin/change_list.html" %} +{% block object-tools-items %} +
  • + + Import Likelihoods + +
  • + {{ block.super }} +{% endblock %} diff --git a/core/templates/core/voter_detail.html b/core/templates/core/voter_detail.html index e54d851..5955dd9 100644 --- a/core/templates/core/voter_detail.html +++ b/core/templates/core/voter_detail.html @@ -93,6 +93,10 @@ {{ voter.birthdate|date:"M d, Y"|default:"N/A" }} +
  • + + {{ voter.prior_state|default:"N/A" }} +
  • {{ voter.registration_date|date:"M d, Y"|default:"Unknown" }} @@ -397,11 +401,15 @@ {{ voter_form.city }} -
    +
    {{ voter_form.state }}
    -
    +
    + + {{ voter_form.prior_state }} +
    +
    {{ voter_form.zip_code }}
    @@ -437,7 +445,7 @@
    - {{ voter_form.district }} + {{ voter_form.voter_id }}