diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index c6c1cfd..1d14269 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc index a81cff1..6f72c17 100644 Binary files a/core/__pycache__/forms.cpython-311.pyc and b/core/__pycache__/forms.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index ccc4975..7b0eccf 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 1b0776e..363abb5 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 2874f74..c8bcb73 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index 5b46c21..35068b0 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,18 +1,22 @@ from django.contrib import admin from .models import ( Tenant, TenantUserRole, InteractionType, DonationMethod, ElectionType, EventType, Voter, - VotingRecord, Event, EventParticipation, Donation, Interaction, VoterLikelihood + VotingRecord, Event, EventParticipation, Donation, Interaction, VoterLikelihood, CampaignSettings ) class TenantUserRoleInline(admin.TabularInline): model = TenantUserRole extra = 1 +class CampaignSettingsInline(admin.StackedInline): + model = CampaignSettings + can_delete = False + @admin.register(Tenant) class TenantAdmin(admin.ModelAdmin): list_display = ('name', 'slug', 'created_at') search_fields = ('name',) - inlines = [TenantUserRoleInline] + inlines = [TenantUserRoleInline, CampaignSettingsInline] @admin.register(TenantUserRole) class TenantUserRoleAdmin(admin.ModelAdmin): @@ -75,4 +79,9 @@ class EventAdmin(admin.ModelAdmin): @admin.register(EventParticipation) class EventParticipationAdmin(admin.ModelAdmin): list_display = ('voter', 'event', 'participation_type') - list_filter = ('event__tenant', 'event', 'participation_type') \ No newline at end of file + list_filter = ('event__tenant', 'event', 'participation_type') + +@admin.register(CampaignSettings) +class CampaignSettingsAdmin(admin.ModelAdmin): + list_display = ('tenant', 'donation_goal') + list_filter = ('tenant',) \ No newline at end of file diff --git a/core/forms.py b/core/forms.py index 2208cad..a025ab4 100644 --- a/core/forms.py +++ b/core/forms.py @@ -8,7 +8,7 @@ class VoterForm(forms.ModelForm): 'first_name', 'last_name', 'address_street', 'city', 'state', 'zip_code', 'county', 'latitude', 'longitude', 'phone', 'email', 'voter_id', 'district', 'precinct', - 'registration_date', 'is_targeted', 'candidate_support', 'yard_sign' + 'registration_date', 'is_targeted', 'candidate_support', 'yard_sign', 'window_sticker' ] widgets = { 'registration_date': forms.DateInput(attrs={'type': 'date'}), @@ -28,6 +28,7 @@ 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'}) class InteractionForm(forms.ModelForm): class Meta: @@ -106,3 +107,10 @@ class EventForm(forms.ModelForm): for field in self.fields.values(): field.widget.attrs.update({'class': 'form-control'}) self.fields['event_type'].widget.attrs.update({'class': 'form-select'}) + +class VoterImportForm(forms.Form): + file = forms.FileField(label="Select CSV file") + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['file'].widget.attrs.update({'class': 'form-control'}) \ No newline at end of file diff --git a/core/migrations/0009_voter_window_sticker.py b/core/migrations/0009_voter_window_sticker.py new file mode 100644 index 0000000..691d6ee --- /dev/null +++ b/core/migrations/0009_voter_window_sticker.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2026-01-24 23:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0008_alter_voter_latitude_alter_voter_longitude'), + ] + + operations = [ + migrations.AddField( + model_name='voter', + name='window_sticker', + field=models.CharField(choices=[('none', 'None'), ('wants', 'Wants Sticker'), ('has', 'Has Sticker')], default='none', max_length=20), + ), + ] diff --git a/core/migrations/0010_alter_voter_window_sticker_campaignsettings.py b/core/migrations/0010_alter_voter_window_sticker_campaignsettings.py new file mode 100644 index 0000000..a3a1431 --- /dev/null +++ b/core/migrations/0010_alter_voter_window_sticker_campaignsettings.py @@ -0,0 +1,31 @@ +# Generated by Django 5.2.7 on 2026-01-24 23:58 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0009_voter_window_sticker'), + ] + + operations = [ + migrations.AlterField( + model_name='voter', + name='window_sticker', + field=models.CharField(choices=[('none', 'None'), ('wants', 'Wants Sticker'), ('has', 'Has Sticker')], default='none', max_length=20, verbose_name='Window Sticker Status'), + ), + migrations.CreateModel( + name='CampaignSettings', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('donation_goal', models.DecimalField(decimal_places=2, default=170000.0, max_digits=12)), + ('tenant', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='settings', to='core.tenant')), + ], + options={ + 'verbose_name': 'Campaign Settings', + 'verbose_name_plural': 'Campaign Settings', + }, + ), + ] diff --git a/core/migrations/__pycache__/0009_voter_window_sticker.cpython-311.pyc b/core/migrations/__pycache__/0009_voter_window_sticker.cpython-311.pyc new file mode 100644 index 0000000..6013c41 Binary files /dev/null and b/core/migrations/__pycache__/0009_voter_window_sticker.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0010_alter_voter_window_sticker_campaignsettings.cpython-311.pyc b/core/migrations/__pycache__/0010_alter_voter_window_sticker_campaignsettings.cpython-311.pyc new file mode 100644 index 0000000..11000e2 Binary files /dev/null and b/core/migrations/__pycache__/0010_alter_voter_window_sticker_campaignsettings.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 7a0450d..9034959 100644 --- a/core/models.py +++ b/core/models.py @@ -95,6 +95,11 @@ class Voter(models.Model): ('wants', 'Wants a yard sign'), ('has', 'Has a yard sign'), ] + WINDOW_STICKER_CHOICES = [ + ('none', 'None'), + ('wants', 'Wants Sticker'), + ('has', 'Has Sticker'), + ] tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name='voters') voter_id = models.CharField(max_length=50, blank=True) @@ -116,6 +121,7 @@ class Voter(models.Model): is_targeted = models.BooleanField(default=False) candidate_support = models.CharField(max_length=20, choices=SUPPORT_CHOICES, default='unknown') yard_sign = models.CharField(max_length=20, choices=YARD_SIGN_CHOICES, default='none') + window_sticker = models.CharField(max_length=20, choices=WINDOW_STICKER_CHOICES, default='none', verbose_name='Window Sticker Status') created_at = models.DateTimeField(auto_now_add=True) @@ -286,4 +292,14 @@ class VoterLikelihood(models.Model): unique_together = ('voter', 'election_type') def __str__(self): - return f"{self.voter} - {self.election_type}: {self.get_likelihood_display()}" \ No newline at end of file + return f"{self.voter} - {self.election_type}: {self.get_likelihood_display()}" +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) + + class Meta: + verbose_name = 'Campaign Settings' + verbose_name_plural = 'Campaign Settings' + + def __str__(self): + return f'Settings for {self.tenant.name}' diff --git a/core/templates/core/index.html b/core/templates/core/index.html index db73f03..91ce1a5 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -31,14 +31,18 @@ {% else %}
-
-

Dashboard: {{ selected_tenant.name }}

-
-
- - +
+

Dashboard: {{ selected_tenant.name }}

+ Switch Campaign +
+ + +
+
+ + + - Switch Campaign
@@ -46,60 +50,84 @@
-
-
-
Registered Voters
-

{{ metrics.total_registered_voters }}

+ +
+
+
Registered Voters
+

{{ metrics.total_registered_voters }}

+
-
+
-
-
-
Target Voters
-

{{ metrics.total_target_voters }}

+ +
+
+
Target Voters
+

{{ metrics.total_target_voters }}

+
-
+
-
-
-
Supporting
-

{{ metrics.total_supporting }}

+ +
+
+
Supporting
+

{{ metrics.total_supporting }}

+
-
+
-
-
-
Voter Addresses
-

{{ metrics.total_voter_addresses }}

+ +
+
+
Voter Addresses
+

{{ metrics.total_voter_addresses }}

+
-
+
-
-
-
Door Visits
-

{{ metrics.total_door_visits }}

+ +
+
+
Door Visits
+

{{ metrics.total_door_visits }}

+
-
+
-
-
-
Signs (Wants/Has)
-

{{ metrics.total_signs }}

+ +
+
+
Signs (Wants/Has)
+

{{ metrics.total_signs }}

+
-
+
@@ -117,4 +145,14 @@
{% endif %}
-{% endblock %} \ No newline at end of file + + +{% endblock %} diff --git a/core/templates/core/voter_detail.html b/core/templates/core/voter_detail.html index 3a3412a..f1895cf 100644 --- a/core/templates/core/voter_detail.html +++ b/core/templates/core/voter_detail.html @@ -137,7 +137,7 @@
Campaign Assets
-
+
Yard Sign Status
@@ -151,6 +151,20 @@ {% endif %}
+
+
+ Window Sticker Status +
+
+ {% if voter.window_sticker == 'has' %} + Has Sticker + {% elif voter.window_sticker == 'wants' %} + Wants Sticker + {% else %} + None + {% endif %} +
+
@@ -441,6 +455,10 @@ {{ voter_form.yard_sign }}
+
+ + {{ voter_form.window_sticker }} +