diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index 9eeed8d..c60c979 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 dc6edf3..1bb7bb9 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 a033569..2c3fe0d 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 d1c2c24..a3e9c96 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 2b1d115..da2d528 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 5395bfb..ddcba09 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin from .models import ( - Tenant, TenantUserRole, InteractionType, DonationMethod, ElectionType, Voter, + Tenant, TenantUserRole, InteractionType, DonationMethod, ElectionType, EventType, Voter, VotingRecord, Event, EventParticipation, Donation, Interaction, VoterLikelihood ) @@ -22,18 +22,27 @@ class TenantUserRoleAdmin(admin.ModelAdmin): @admin.register(InteractionType) class InteractionTypeAdmin(admin.ModelAdmin): - list_display = ('name', 'tenant') - list_filter = ('tenant',) + list_display = ('name', 'tenant', 'is_active') + list_filter = ('tenant', 'is_active') + search_fields = ('name',) @admin.register(DonationMethod) class DonationMethodAdmin(admin.ModelAdmin): - list_display = ('name', 'tenant') - list_filter = ('tenant',) + list_display = ('name', 'tenant', 'is_active') + list_filter = ('tenant', 'is_active') + search_fields = ('name',) @admin.register(ElectionType) class ElectionTypeAdmin(admin.ModelAdmin): - list_display = ('name', 'tenant') - list_filter = ('tenant',) + list_display = ('name', 'tenant', 'is_active') + list_filter = ('tenant', 'is_active') + search_fields = ('name',) + +@admin.register(EventType) +class EventTypeAdmin(admin.ModelAdmin): + list_display = ('name', 'tenant', 'is_active') + list_filter = ('tenant', 'is_active') + search_fields = ('name',) class VotingRecordInline(admin.TabularInline): model = VotingRecord @@ -61,9 +70,9 @@ class VoterAdmin(admin.ModelAdmin): @admin.register(Event) class EventAdmin(admin.ModelAdmin): list_display = ('event_type', 'date', 'tenant') - list_filter = ('tenant', 'date') + list_filter = ('tenant', 'date', 'event_type') @admin.register(EventParticipation) class EventParticipationAdmin(admin.ModelAdmin): list_display = ('voter', 'event') - list_filter = ('event__tenant', 'event') \ No newline at end of file + list_filter = ('event__tenant', 'event') diff --git a/core/forms.py b/core/forms.py index 8235a81..72519d6 100644 --- a/core/forms.py +++ b/core/forms.py @@ -1,5 +1,5 @@ from django import forms -from .models import Voter, Interaction, Donation, VoterLikelihood, InteractionType, DonationMethod, ElectionType, Event, EventParticipation +from .models import Voter, Interaction, Donation, VoterLikelihood, InteractionType, DonationMethod, ElectionType, Event, EventParticipation, EventType class VoterForm(forms.ModelForm): class Meta: @@ -33,7 +33,7 @@ class InteractionForm(forms.ModelForm): def __init__(self, *args, tenant=None, **kwargs): super().__init__(*args, **kwargs) if tenant: - self.fields['type'].queryset = InteractionType.objects.filter(tenant=tenant) + self.fields['type'].queryset = InteractionType.objects.filter(tenant=tenant, is_active=True) for field in self.fields.values(): field.widget.attrs.update({'class': 'form-control'}) self.fields['type'].widget.attrs.update({'class': 'form-select'}) @@ -49,7 +49,7 @@ class DonationForm(forms.ModelForm): def __init__(self, *args, tenant=None, **kwargs): super().__init__(*args, **kwargs) if tenant: - self.fields['method'].queryset = DonationMethod.objects.filter(tenant=tenant) + self.fields['method'].queryset = DonationMethod.objects.filter(tenant=tenant, is_active=True) for field in self.fields.values(): field.widget.attrs.update({'class': 'form-control'}) self.fields['method'].widget.attrs.update({'class': 'form-select'}) @@ -62,7 +62,7 @@ class VoterLikelihoodForm(forms.ModelForm): def __init__(self, *args, tenant=None, **kwargs): super().__init__(*args, **kwargs) if tenant: - self.fields['election_type'].queryset = ElectionType.objects.filter(tenant=tenant) + self.fields['election_type'].queryset = ElectionType.objects.filter(tenant=tenant, is_active=True) for field in self.fields.values(): field.widget.attrs.update({'class': 'form-control'}) self.fields['election_type'].widget.attrs.update({'class': 'form-select'}) @@ -79,4 +79,33 @@ class EventParticipationForm(forms.ModelForm): self.fields['event'].queryset = Event.objects.filter(tenant=tenant) for field in self.fields.values(): field.widget.attrs.update({'class': 'form-control'}) - self.fields['event'].widget.attrs.update({'class': 'form-select'}) \ No newline at end of file + self.fields['event'].widget.attrs.update({'class': 'form-select'}) + +class EventForm(forms.ModelForm): + class Meta: + model = Event + fields = ['date', 'event_type', 'description'] + widgets = { + 'date': forms.DateInput(attrs={'type': 'date'}), + 'description': forms.Textarea(attrs={'rows': 2}), + } + + def __init__(self, *args, tenant=None, **kwargs): + super().__init__(*args, **kwargs) + if tenant: + self.fields['event_type'].queryset = EventType.objects.filter(tenant=tenant, is_active=True) + for field in self.fields.values(): + field.widget.attrs.update({'class': 'form-control'}) + self.fields['event_type'].widget.attrs.update({'class': 'form-select'}) + +class EventTypeForm(forms.ModelForm): + class Meta: + model = EventType + fields = ['name', 'is_active'] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for field in self.fields.values(): + if not isinstance(field.widget, forms.CheckboxInput): + field.widget.attrs.update({'class': 'form-control'}) + self.fields['is_active'].widget.attrs.update({'class': 'form-check-input'}) \ No newline at end of file diff --git a/core/migrations/0004_remove_voter_geocode_donationmethod_is_active_and_more.py b/core/migrations/0004_remove_voter_geocode_donationmethod_is_active_and_more.py new file mode 100644 index 0000000..e75116c --- /dev/null +++ b/core/migrations/0004_remove_voter_geocode_donationmethod_is_active_and_more.py @@ -0,0 +1,50 @@ +# Generated by Django 5.2.7 on 2026-01-24 14:50 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0003_tenantuserrole'), + ] + + operations = [ + migrations.RemoveField( + model_name='voter', + name='geocode', + ), + migrations.AddField( + model_name='donationmethod', + name='is_active', + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name='electiontype', + name='is_active', + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name='interactiontype', + name='is_active', + field=models.BooleanField(default=True), + ), + migrations.CreateModel( + name='EventType', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('is_active', models.BooleanField(default=True)), + ('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='event_types', to='core.tenant')), + ], + options={ + 'unique_together': {('tenant', 'name')}, + }, + ), + migrations.AlterField( + model_name='event', + name='event_type', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='core.eventtype'), + ), + ] diff --git a/core/migrations/__pycache__/0004_remove_voter_geocode_donationmethod_is_active_and_more.cpython-311.pyc b/core/migrations/__pycache__/0004_remove_voter_geocode_donationmethod_is_active_and_more.cpython-311.pyc new file mode 100644 index 0000000..62fe111 Binary files /dev/null and b/core/migrations/__pycache__/0004_remove_voter_geocode_donationmethod_is_active_and_more.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index c32522a..92d0a64 100644 --- a/core/models.py +++ b/core/models.py @@ -35,6 +35,7 @@ class TenantUserRole(models.Model): class InteractionType(models.Model): tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name='interaction_types') name = models.CharField(max_length=100) + is_active = models.BooleanField(default=True) class Meta: unique_together = ('tenant', 'name') @@ -45,6 +46,7 @@ class InteractionType(models.Model): class DonationMethod(models.Model): tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name='donation_methods') name = models.CharField(max_length=100) + is_active = models.BooleanField(default=True) class Meta: unique_together = ('tenant', 'name') @@ -55,6 +57,18 @@ class DonationMethod(models.Model): class ElectionType(models.Model): tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name='election_types') name = models.CharField(max_length=100) + is_active = models.BooleanField(default=True) + + class Meta: + unique_together = ('tenant', 'name') + + def __str__(self): + return f"{self.name} ({self.tenant.name})" + +class EventType(models.Model): + tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name='event_types') + name = models.CharField(max_length=100) + is_active = models.BooleanField(default=True) class Meta: unique_together = ('tenant', 'name') @@ -104,7 +118,7 @@ class VotingRecord(models.Model): class Event(models.Model): tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name='events') date = models.DateField() - event_type = models.CharField(max_length=100) + event_type = models.ForeignKey(EventType, on_delete=models.PROTECT, null=True) description = models.TextField(blank=True) def __str__(self): @@ -150,4 +164,4 @@ 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()}" diff --git a/core/templates/base.html b/core/templates/base.html index d40c799..a1fe79a 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -40,6 +40,9 @@ +
Admin Panel diff --git a/core/templates/core/event_type_list.html b/core/templates/core/event_type_list.html new file mode 100644 index 0000000..fa9e81b --- /dev/null +++ b/core/templates/core/event_type_list.html @@ -0,0 +1,127 @@ +{% extends 'base.html' %} +{% load static %} + +{% block content %} +
+
+
+ +

Event Types

+
+ +
+ +
+
+ + + + + + + + + + {% for type in event_types %} + + + + + + + + + {% empty %} + + + + {% endfor %} + +
NameStatusActions
{{ type.name }} + {% if type.is_active %} + Active + {% else %} + Inactive + {% endif %} + + +
+ {% csrf_token %} + +
+
+ + No event types defined yet. +
+
+
+
+ + + +{% endblock %} diff --git a/core/templates/core/voter_detail.html b/core/templates/core/voter_detail.html index 25aaebb..6b3b9bc 100644 --- a/core/templates/core/voter_detail.html +++ b/core/templates/core/voter_detail.html @@ -291,7 +291,7 @@ {% for participation in event_participations %} {{ participation.event.date|date:"M d, Y" }} - {{ participation.event.event_type }} + {{ participation.event.event_type.name }} {{ participation.event.description|truncatechars:60 }}
diff --git a/core/urls.py b/core/urls.py index 9712077..e2038a7 100644 --- a/core/urls.py +++ b/core/urls.py @@ -23,4 +23,10 @@ urlpatterns = [ path('voters//event-participation/add/', views.add_event_participation, name='add_event_participation'), path('event-participation//edit/', views.edit_event_participation, name='edit_event_participation'), path('event-participation//delete/', views.delete_event_participation, name='delete_event_participation'), -] + + # Maintenance + path('maintenance/event-types/', views.event_type_list, name='event_type_list'), + path('maintenance/event-types/add/', views.event_type_add, name='event_type_add'), + path('maintenance/event-types//edit/', views.event_type_edit, name='event_type_edit'), + path('maintenance/event-types//delete/', views.event_type_delete, name='event_type_delete'), +] \ No newline at end of file diff --git a/core/views.py b/core/views.py index 90f37ba..eebfdf5 100644 --- a/core/views.py +++ b/core/views.py @@ -1,8 +1,8 @@ from django.shortcuts import render, redirect, get_object_or_404 from django.db.models import Q from django.contrib import messages -from .models import Voter, Tenant, Interaction, Donation, VoterLikelihood, EventParticipation, Event -from .forms import VoterForm, InteractionForm, DonationForm, VoterLikelihoodForm, EventParticipationForm +from .models import Voter, Tenant, Interaction, Donation, VoterLikelihood, EventParticipation, Event, EventType, InteractionType, DonationMethod, ElectionType +from .forms import VoterForm, InteractionForm, DonationForm, VoterLikelihoodForm, EventParticipationForm, EventTypeForm def index(request): """ @@ -47,7 +47,13 @@ def voter_list(request): query = query.strip() search_filter = Q(first_name__icontains=query) | Q(last_name__icontains=query) | Q(voter_id__icontains=query) - if " " in query: + if "," in query: + parts = [p.strip() for p in query.split(",")] + if len(parts) >= 2: + last_part = parts[0] + first_part = parts[1] + search_filter |= Q(last_name__icontains=last_part, first_name__icontains=first_part) + elif " " in query: parts = query.split() if len(parts) >= 2: first_part = parts[0] @@ -267,4 +273,61 @@ def delete_event_participation(request, participation_id): if request.method == 'POST': participation.delete() messages.success(request, "Event participation removed.") - return redirect('voter_detail', voter_id=voter_id) \ No newline at end of file + return redirect('voter_detail', voter_id=voter_id) + +def event_type_list(request): + """ + Maintenance page for Event Types. + """ + 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) + event_types = EventType.objects.filter(tenant=tenant) + + context = { + 'event_types': event_types, + 'selected_tenant': tenant, + 'form': EventTypeForm(), + } + return render(request, 'core/event_type_list.html', context) + +def event_type_add(request): + selected_tenant_id = request.session.get('tenant_id') + tenant = get_object_or_404(Tenant, id=selected_tenant_id) + + if request.method == 'POST': + form = EventTypeForm(request.POST) + if form.is_valid(): + event_type = form.save(commit=False) + event_type.tenant = tenant + event_type.save() + messages.success(request, "Event type added.") + return redirect('event_type_list') + +def event_type_edit(request, type_id): + selected_tenant_id = request.session.get('tenant_id') + tenant = get_object_or_404(Tenant, id=selected_tenant_id) + event_type = get_object_or_404(EventType, id=type_id, tenant=tenant) + + if request.method == 'POST': + form = EventTypeForm(request.POST, instance=event_type) + if form.is_valid(): + form.save() + messages.success(request, "Event type updated.") + return redirect('event_type_list') + +def event_type_delete(request, type_id): + selected_tenant_id = request.session.get('tenant_id') + tenant = get_object_or_404(Tenant, id=selected_tenant_id) + event_type = get_object_or_404(EventType, id=type_id, tenant=tenant) + + if request.method == 'POST': + try: + event_type.delete() + messages.success(request, "Event type deleted.") + except Exception as e: + messages.error(request, f"Cannot delete event type: {e}") + return redirect('event_type_list')