diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index eff3575..cf83c17 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 249a4d1..9509470 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index a166bb4..3af98a6 100644 --- a/core/admin.py +++ b/core/admin.py @@ -11,7 +11,8 @@ from django.shortcuts import render, redirect from django.template.response import TemplateResponse from .models import ( Tenant, TenantUserRole, InteractionType, DonationMethod, ElectionType, EventType, Voter, - VotingRecord, Event, EventParticipation, Donation, Interaction, VoterLikelihood, CampaignSettings + VotingRecord, Event, EventParticipation, Donation, Interaction, VoterLikelihood, CampaignSettings, + Interest, Volunteer, VolunteerEvent ) from .forms import ( VoterImportForm, EventImportForm, EventParticipationImportForm, @@ -147,6 +148,12 @@ class EventTypeAdmin(admin.ModelAdmin): list_filter = ('tenant', 'is_active') search_fields = ('name',) +@admin.register(Interest) +class InterestAdmin(admin.ModelAdmin): + list_display = ('name', 'tenant') + list_filter = ('tenant',) + search_fields = ('name',) + class VotingRecordInline(admin.TabularInline): model = VotingRecord extra = 1 @@ -163,6 +170,10 @@ class VoterLikelihoodInline(admin.TabularInline): model = VoterLikelihood extra = 1 +class VolunteerEventInline(admin.TabularInline): + model = VolunteerEvent + extra = 1 + @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') @@ -509,6 +520,19 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin): context['opts'] = self.model._meta return render(request, "admin/import_csv.html", context) +@admin.register(Volunteer) +class VolunteerAdmin(admin.ModelAdmin): + list_display = ('name', 'email', 'phone', 'tenant', 'user') + list_filter = ('tenant',) + search_fields = ('name', 'email', 'phone') + inlines = [VolunteerEventInline, InteractionInline] + filter_horizontal = ('interests',) + +@admin.register(VolunteerEvent) +class VolunteerEventAdmin(admin.ModelAdmin): + list_display = ('volunteer', 'event', 'role') + list_filter = ('event__tenant', 'event', 'role') + @admin.register(EventParticipation) class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin): list_display = ('voter', 'event', 'participation_type') @@ -912,9 +936,9 @@ class DonationAdmin(BaseImportAdminMixin, admin.ModelAdmin): @admin.register(Interaction) class InteractionAdmin(BaseImportAdminMixin, 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') + list_display = ('id', 'voter', 'volunteer', 'type', 'date', 'description') + list_filter = ('voter__tenant', 'type', 'date', 'volunteer') + search_fields = ('voter__first_name', 'voter__last_name', 'voter__voter_id', 'description', 'volunteer__name') change_list_template = "admin/interaction_change_list.html" def get_urls(self): @@ -1308,4 +1332,4 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin): @admin.register(CampaignSettings) class CampaignSettingsAdmin(admin.ModelAdmin): list_display = ('tenant', 'donation_goal') - list_filter = ('tenant',) + list_filter = ('tenant',) \ No newline at end of file diff --git a/core/migrations/0013_remove_tenant_description_remove_tenant_slug_and_more.py b/core/migrations/0013_remove_tenant_description_remove_tenant_slug_and_more.py new file mode 100644 index 0000000..4964de2 --- /dev/null +++ b/core/migrations/0013_remove_tenant_description_remove_tenant_slug_and_more.py @@ -0,0 +1,71 @@ +# Generated by Django 5.2.7 on 2026-01-25 16:33 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0012_voter_prior_state_alter_voter_state'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.RemoveField( + model_name='tenant', + name='description', + ), + migrations.RemoveField( + model_name='tenant', + name='slug', + ), + migrations.AlterField( + model_name='tenant', + name='name', + field=models.CharField(max_length=100), + ), + migrations.AlterField( + model_name='tenantuserrole', + name='role', + field=models.CharField(choices=[('admin', 'Admin'), ('campaign_manager', 'Campaign Manager'), ('campaign_staff', 'Campaign Staff')], max_length=20), + ), + migrations.CreateModel( + name='Interest', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interests', to='core.tenant')), + ], + options={ + 'unique_together': {('tenant', 'name')}, + }, + ), + migrations.CreateModel( + name='Volunteer', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('email', models.EmailField(max_length=254)), + ('phone', models.CharField(blank=True, max_length=20)), + ('interests', models.ManyToManyField(blank=True, related_name='volunteers', to='core.interest')), + ('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='volunteers', to='core.tenant')), + ('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='volunteer_profile', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.AddField( + model_name='interaction', + name='volunteer', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='interactions', to='core.volunteer'), + ), + migrations.CreateModel( + name='VolunteerEvent', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('role', models.CharField(max_length=100)), + ('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='volunteers', to='core.event')), + ('volunteer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='event_assignments', to='core.volunteer')), + ], + ), + ] diff --git a/core/migrations/0014_volunteer_assigned_events_alter_volunteerevent_event.py b/core/migrations/0014_volunteer_assigned_events_alter_volunteerevent_event.py new file mode 100644 index 0000000..34d0459 --- /dev/null +++ b/core/migrations/0014_volunteer_assigned_events_alter_volunteerevent_event.py @@ -0,0 +1,24 @@ +# Generated by Django 5.2.7 on 2026-01-25 16:34 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0013_remove_tenant_description_remove_tenant_slug_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='volunteer', + name='assigned_events', + field=models.ManyToManyField(related_name='assigned_volunteers', through='core.VolunteerEvent', to='core.event'), + ), + migrations.AlterField( + model_name='volunteerevent', + name='event', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='volunteer_assignments', to='core.event'), + ), + ] diff --git a/core/migrations/__pycache__/0013_remove_tenant_description_remove_tenant_slug_and_more.cpython-311.pyc b/core/migrations/__pycache__/0013_remove_tenant_description_remove_tenant_slug_and_more.cpython-311.pyc new file mode 100644 index 0000000..1d301f1 Binary files /dev/null and b/core/migrations/__pycache__/0013_remove_tenant_description_remove_tenant_slug_and_more.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0014_volunteer_assigned_events_alter_volunteerevent_event.cpython-311.pyc b/core/migrations/__pycache__/0014_volunteer_assigned_events_alter_volunteerevent_event.cpython-311.pyc new file mode 100644 index 0000000..60aaa6b Binary files /dev/null and b/core/migrations/__pycache__/0014_volunteer_assigned_events_alter_volunteerevent_event.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 97216d5..eafb0d4 100644 --- a/core/models.py +++ b/core/models.py @@ -76,6 +76,16 @@ class EventType(models.Model): def __str__(self): return self.name +class Interest(models.Model): + tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name='interests') + name = models.CharField(max_length=100) + + class Meta: + unique_together = ('tenant', 'name') + + def __str__(self): + return self.name + class Voter(models.Model): SUPPORT_CHOICES = [ ('unknown', 'Unknown'), @@ -240,6 +250,26 @@ class Event(models.Model): def __str__(self): return f"{self.event_type} on {self.date}" +class Volunteer(models.Model): + tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name='volunteers') + user = models.OneToOneField(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='volunteer_profile') + name = models.CharField(max_length=255) + email = models.EmailField() + phone = models.CharField(max_length=20, blank=True) + interests = models.ManyToManyField(Interest, blank=True, related_name='volunteers') + assigned_events = models.ManyToManyField(Event, through='VolunteerEvent', related_name='assigned_volunteers') + + def __str__(self): + return self.name + +class VolunteerEvent(models.Model): + volunteer = models.ForeignKey(Volunteer, on_delete=models.CASCADE, related_name='event_assignments') + event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name='volunteer_assignments') + role = models.CharField(max_length=100) + + def __str__(self): + return f"{self.volunteer} at {self.event} as {self.role}" + class EventParticipation(models.Model): PARTICIPATION_TYPE_CHOICES = [ ("invited", "Invited"), @@ -265,6 +295,7 @@ class Donation(models.Model): 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() description = models.CharField(max_length=255)