This commit is contained in:
Flatlogic Bot 2026-01-24 15:09:45 +00:00
parent 0ade1911eb
commit aade4cc131
15 changed files with 324 additions and 23 deletions

View File

@ -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')
list_filter = ('event__tenant', 'event')

View File

@ -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'})
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'})

View File

@ -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'),
),
]

View File

@ -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()}"
return f"{self.voter} - {self.election_type}: {self.get_likelihood_display()}"

View File

@ -40,6 +40,9 @@
<li class="nav-item">
<a class="nav-link" href="/voters/">Voters</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'event_type_list' %}">Maintenance</a>
</li>
</ul>
<div class="d-flex align-items-center">
<a href="/admin/" class="btn btn-outline-primary btn-sm me-2">Admin Panel</a>

View File

@ -0,0 +1,127 @@
{% extends 'base.html' %}
{% load static %}
{% block content %}
<div class="container py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-1">
<li class="breadcrumb-item"><a href="{% url 'index' %}">Home</a></li>
<li class="breadcrumb-item active" aria-current="page">Maintenance</li>
</ol>
</nav>
<h1 class="h3 mb-0">Event Types</h1>
</div>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addTypeModal">
<i class="bi bi-plus-lg me-1"></i> Add Event Type
</button>
</div>
<div class="card border-0 shadow-sm">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">Name</th>
<th>Status</th>
<th class="text-end pe-4">Actions</th>
</tr>
</thead>
<tbody>
{% for type in event_types %}
<tr>
<td class="ps-4 fw-medium">{{ type.name }}</td>
<td>
{% if type.is_active %}
<span class="badge bg-success-subtle text-success border border-success-subtle px-2">Active</span>
{% else %}
<span class="badge bg-secondary-subtle text-secondary border border-secondary-subtle px-2">Inactive</span>
{% endif %}
</td>
<td class="text-end pe-4">
<button type="button" class="btn btn-sm btn-outline-secondary me-1"
data-bs-toggle="modal" data-bs-target="#editTypeModal{{ type.id }}">
<i class="bi bi-pencil"></i>
</button>
<form action="{% url 'event_type_delete' type.id %}" method="POST" class="d-inline">
{% csrf_token %}
<button type="submit" class="btn btn-sm btn-outline-danger"
onclick="return confirm('Are you sure you want to delete this event type?')">
<i class="bi bi-trash"></i>
</button>
</form>
</td>
</tr>
<!-- Edit Modal -->
<div class="modal fade" id="editTypeModal{{ type.id }}" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<form action="{% url 'event_type_edit' type.id %}" method="POST">
{% csrf_token %}
<div class="modal-header border-0">
<h5 class="modal-title">Edit Event Type</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Name</label>
<input type="text" name="name" class="form-control" value="{{ type.name }}" required>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="is_active" id="activeSwitch{{ type.id }}" {% if type.is_active %}checked{% endif %}>
<label class="form-check-label" for="activeSwitch{{ type.id }}">Active</label>
</div>
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary px-4">Save Changes</button>
</div>
</form>
</div>
</div>
</div>
{% empty %}
<tr>
<td colspan="3" class="text-center py-5 text-muted">
<i class="bi bi-info-circle fs-4 d-block mb-2"></i>
No event types defined yet.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Add Modal -->
<div class="modal fade" id="addTypeModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<form action="{% url 'event_type_add' %}" method="POST">
{% csrf_token %}
<div class="modal-header border-0">
<h5 class="modal-title">Add Event Type</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Name</label>
{{ form.name }}
</div>
<div class="form-check form-switch">
{{ form.is_active }}
<label class="form-check-label" for="{{ form.is_active.id_for_label }}">Active</label>
</div>
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary px-4">Add Type</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -291,7 +291,7 @@
{% for participation in event_participations %}
<tr>
<td class="ps-4 text-nowrap">{{ participation.event.date|date:"M d, Y" }}</td>
<td><span class="badge bg-light text-dark border">{{ participation.event.event_type }}</span></td>
<td><span class="badge bg-light text-dark border">{{ participation.event.event_type.name }}</span></td>
<td class="small text-muted">{{ participation.event.description|truncatechars:60 }}</td>
<td class="pe-4 text-end">
<button class="btn btn-sm btn-link text-primary p-0 me-2" data-bs-toggle="modal" data-bs-target="#editEventParticipationModal{{ participation.id }}">
@ -715,7 +715,7 @@
<label class="form-label fw-medium">Event</label>
<select name="event" class="form-select">
{% for ev in event_participation_form.fields.event.queryset %}
<option value="{{ ev.id }}" {% if ev.id == participation.event.id %}selected{% endif %}>{{ ev.event_type }} on {{ ev.date }}</option>
<option value="{{ ev.id }}" {% if ev.id == participation.event.id %}selected{% endif %}>{{ ev.event_type.name }} on {{ ev.date }}</option>
{% endfor %}
</select>
</div>

View File

@ -23,4 +23,10 @@ urlpatterns = [
path('voters/<int:voter_id>/event-participation/add/', views.add_event_participation, name='add_event_participation'),
path('event-participation/<int:participation_id>/edit/', views.edit_event_participation, name='edit_event_participation'),
path('event-participation/<int:participation_id>/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/<int:type_id>/edit/', views.event_type_edit, name='event_type_edit'),
path('maintenance/event-types/<int:type_id>/delete/', views.event_type_delete, name='event_type_delete'),
]

View File

@ -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)
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')