.6
This commit is contained in:
parent
0ade1911eb
commit
aade4cc131
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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')
|
||||
|
||||
@ -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'})
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
@ -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()}"
|
||||
|
||||
@ -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>
|
||||
|
||||
127
core/templates/core/event_type_list.html
Normal file
127
core/templates/core/event_type_list.html
Normal 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 %}
|
||||
@ -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>
|
||||
|
||||
@ -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'),
|
||||
]
|
||||
@ -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')
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user