Autosave: 20260124-063234
This commit is contained in:
parent
27bd5182e3
commit
5073ef280c
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
core/__pycache__/forms.cpython-311.pyc
Normal file
BIN
core/__pycache__/forms.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,3 +1,69 @@
|
||||
from django.contrib import admin
|
||||
from .models import (
|
||||
Tenant, TenantUserRole, InteractionType, DonationMethod, ElectionType, Voter,
|
||||
VotingRecord, Event, EventParticipation, Donation, Interaction, VoterLikelihood
|
||||
)
|
||||
|
||||
# Register your models here.
|
||||
class TenantUserRoleInline(admin.TabularInline):
|
||||
model = TenantUserRole
|
||||
extra = 1
|
||||
|
||||
@admin.register(Tenant)
|
||||
class TenantAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'slug', 'created_at')
|
||||
search_fields = ('name',)
|
||||
inlines = [TenantUserRoleInline]
|
||||
|
||||
@admin.register(TenantUserRole)
|
||||
class TenantUserRoleAdmin(admin.ModelAdmin):
|
||||
list_display = ('user', 'tenant', 'role')
|
||||
list_filter = ('tenant', 'role')
|
||||
search_fields = ('user__username', 'tenant__name')
|
||||
|
||||
@admin.register(InteractionType)
|
||||
class InteractionTypeAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'tenant')
|
||||
list_filter = ('tenant',)
|
||||
|
||||
@admin.register(DonationMethod)
|
||||
class DonationMethodAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'tenant')
|
||||
list_filter = ('tenant',)
|
||||
|
||||
@admin.register(ElectionType)
|
||||
class ElectionTypeAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'tenant')
|
||||
list_filter = ('tenant',)
|
||||
|
||||
class VotingRecordInline(admin.TabularInline):
|
||||
model = VotingRecord
|
||||
extra = 1
|
||||
|
||||
class DonationInline(admin.TabularInline):
|
||||
model = Donation
|
||||
extra = 1
|
||||
|
||||
class InteractionInline(admin.TabularInline):
|
||||
model = Interaction
|
||||
extra = 1
|
||||
|
||||
class VoterLikelihoodInline(admin.TabularInline):
|
||||
model = VoterLikelihood
|
||||
extra = 1
|
||||
|
||||
@admin.register(Voter)
|
||||
class VoterAdmin(admin.ModelAdmin):
|
||||
list_display = ('first_name', 'last_name', 'voter_id', 'tenant', 'district', 'candidate_support')
|
||||
list_filter = ('tenant', 'candidate_support', 'yard_sign', 'district')
|
||||
search_fields = ('first_name', 'last_name', 'voter_id')
|
||||
inlines = [VotingRecordInline, DonationInline, InteractionInline, VoterLikelihoodInline]
|
||||
|
||||
@admin.register(Event)
|
||||
class EventAdmin(admin.ModelAdmin):
|
||||
list_display = ('event_type', 'date', 'tenant')
|
||||
list_filter = ('tenant', 'date')
|
||||
|
||||
@admin.register(EventParticipation)
|
||||
class EventParticipationAdmin(admin.ModelAdmin):
|
||||
list_display = ('voter', 'event')
|
||||
list_filter = ('event__tenant', 'event')
|
||||
82
core/forms.py
Normal file
82
core/forms.py
Normal file
@ -0,0 +1,82 @@
|
||||
from django import forms
|
||||
from .models import Voter, Interaction, Donation, VoterLikelihood, InteractionType, DonationMethod, ElectionType, Event, EventParticipation
|
||||
|
||||
class VoterForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Voter
|
||||
fields = [
|
||||
'first_name', 'last_name', 'street_address', 'city', 'state', 'zip_code',
|
||||
'latitude', 'longitude', 'phone', 'email',
|
||||
'voter_id', 'district', 'precinct', 'registration_date',
|
||||
'candidate_support', 'yard_sign'
|
||||
]
|
||||
widgets = {
|
||||
'registration_date': forms.DateInput(attrs={'type': 'date'}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
for field in self.fields.values():
|
||||
field.widget.attrs.update({'class': 'form-control'})
|
||||
self.fields['candidate_support'].widget.attrs.update({'class': 'form-select'})
|
||||
self.fields['yard_sign'].widget.attrs.update({'class': 'form-select'})
|
||||
|
||||
class InteractionForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Interaction
|
||||
fields = ['type', 'date', 'description', 'notes']
|
||||
widgets = {
|
||||
'date': forms.DateInput(attrs={'type': 'date'}),
|
||||
'notes': forms.Textarea(attrs={'rows': 2}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, tenant=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if tenant:
|
||||
self.fields['type'].queryset = InteractionType.objects.filter(tenant=tenant)
|
||||
for field in self.fields.values():
|
||||
field.widget.attrs.update({'class': 'form-control'})
|
||||
self.fields['type'].widget.attrs.update({'class': 'form-select'})
|
||||
|
||||
class DonationForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Donation
|
||||
fields = ['date', 'method', 'amount']
|
||||
widgets = {
|
||||
'date': forms.DateInput(attrs={'type': 'date'}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, tenant=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if tenant:
|
||||
self.fields['method'].queryset = DonationMethod.objects.filter(tenant=tenant)
|
||||
for field in self.fields.values():
|
||||
field.widget.attrs.update({'class': 'form-control'})
|
||||
self.fields['method'].widget.attrs.update({'class': 'form-select'})
|
||||
|
||||
class VoterLikelihoodForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = VoterLikelihood
|
||||
fields = ['election_type', 'likelihood']
|
||||
|
||||
def __init__(self, *args, tenant=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if tenant:
|
||||
self.fields['election_type'].queryset = ElectionType.objects.filter(tenant=tenant)
|
||||
for field in self.fields.values():
|
||||
field.widget.attrs.update({'class': 'form-control'})
|
||||
self.fields['election_type'].widget.attrs.update({'class': 'form-select'})
|
||||
self.fields['likelihood'].widget.attrs.update({'class': 'form-select'})
|
||||
|
||||
class EventParticipationForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = EventParticipation
|
||||
fields = ['event']
|
||||
|
||||
def __init__(self, *args, tenant=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if tenant:
|
||||
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'})
|
||||
78
core/migrations/0001_initial.py
Normal file
78
core/migrations/0001_initial.py
Normal file
@ -0,0 +1,78 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-24 05:12
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Tenant',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('slug', models.SlugField(blank=True, unique=True)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Voter',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('voter_id', models.CharField(blank=True, max_length=50)),
|
||||
('first_name', models.CharField(max_length=100)),
|
||||
('last_name', models.CharField(max_length=100)),
|
||||
('address', models.TextField(blank=True)),
|
||||
('phone', models.CharField(blank=True, max_length=20)),
|
||||
('email', models.EmailField(blank=True, max_length=254)),
|
||||
('geocode', models.CharField(blank=True, max_length=100)),
|
||||
('district', models.CharField(blank=True, max_length=100)),
|
||||
('precinct', models.CharField(blank=True, max_length=100)),
|
||||
('registration_date', models.DateField(blank=True, null=True)),
|
||||
('candidate_support', models.CharField(choices=[('unknown', 'Unknown'), ('supporting', 'Supporting'), ('not_supporting', 'Not Supporting')], default='unknown', max_length=20)),
|
||||
('yard_sign', models.CharField(choices=[('none', 'None'), ('wants', 'Wants a yard sign'), ('has', 'Has a yard sign')], default='none', max_length=20)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='voters', to='core.tenant')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='InteractionType',
|
||||
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='interaction_types', to='core.tenant')),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('tenant', 'name')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ElectionType',
|
||||
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='election_types', to='core.tenant')),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('tenant', 'name')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='DonationMethod',
|
||||
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='donation_methods', to='core.tenant')),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('tenant', 'name')},
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,75 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-24 05:18
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Donation',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('date', models.DateField()),
|
||||
('amount', models.DecimalField(decimal_places=2, max_digits=10)),
|
||||
('method', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.donationmethod')),
|
||||
('voter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='donations', to='core.voter')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Event',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('date', models.DateField()),
|
||||
('event_type', models.CharField(max_length=100)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='events', to='core.tenant')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='EventParticipation',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='participations', to='core.event')),
|
||||
('voter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='event_participations', to='core.voter')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Interaction',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('date', models.DateField()),
|
||||
('description', models.CharField(max_length=255)),
|
||||
('notes', models.TextField(blank=True)),
|
||||
('type', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.interactiontype')),
|
||||
('voter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interactions', to='core.voter')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='VotingRecord',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('election_date', models.DateField()),
|
||||
('election_description', models.CharField(max_length=255)),
|
||||
('primary_party', models.CharField(blank=True, max_length=100)),
|
||||
('voter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='voting_records', to='core.voter')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='VoterLikelihood',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('likelihood', models.CharField(choices=[('not_likely', 'Not Likely'), ('somewhat_likely', 'Somewhat Likely'), ('very_likely', 'Very Likely')], max_length=20)),
|
||||
('election_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.electiontype')),
|
||||
('voter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='likelihoods', to='core.voter')),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('voter', 'election_type')},
|
||||
},
|
||||
),
|
||||
]
|
||||
28
core/migrations/0003_tenantuserrole.py
Normal file
28
core/migrations/0003_tenantuserrole.py
Normal file
@ -0,0 +1,28 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-24 05:27
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0002_donation_event_eventparticipation_interaction_and_more'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='TenantUserRole',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('role', models.CharField(choices=[('system_admin', 'System Administrator'), ('campaign_admin', 'Campaign Administrator'), ('campaign_staff', 'Campaign Staff')], max_length=20)),
|
||||
('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_roles', to='core.tenant')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tenant_roles', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('user', 'tenant', 'role')},
|
||||
},
|
||||
),
|
||||
]
|
||||
BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
core/migrations/__pycache__/0003_tenantuserrole.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0003_tenantuserrole.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
160
core/models.py
160
core/models.py
@ -1,3 +1,161 @@
|
||||
from django.db import models
|
||||
from django.utils.text import slugify
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
# Create your models here.
|
||||
class Tenant(models.Model):
|
||||
name = models.CharField(max_length=255)
|
||||
slug = models.SlugField(unique=True, blank=True)
|
||||
description = models.TextField(blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.slug:
|
||||
self.slug = slugify(self.name)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class TenantUserRole(models.Model):
|
||||
ROLE_CHOICES = [
|
||||
('system_admin', 'System Administrator'),
|
||||
('campaign_admin', 'Campaign Administrator'),
|
||||
('campaign_staff', 'Campaign Staff'),
|
||||
]
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='tenant_roles')
|
||||
tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name='user_roles')
|
||||
role = models.CharField(max_length=20, choices=ROLE_CHOICES)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('user', 'tenant', 'role')
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.username} - {self.tenant.name} ({self.role})"
|
||||
|
||||
class InteractionType(models.Model):
|
||||
tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name='interaction_types')
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('tenant', 'name')
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.tenant.name})"
|
||||
|
||||
class DonationMethod(models.Model):
|
||||
tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name='donation_methods')
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('tenant', 'name')
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.tenant.name})"
|
||||
|
||||
class ElectionType(models.Model):
|
||||
tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name='election_types')
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('tenant', 'name')
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.tenant.name})"
|
||||
|
||||
class Voter(models.Model):
|
||||
SUPPORT_CHOICES = [
|
||||
('unknown', 'Unknown'),
|
||||
('supporting', 'Supporting'),
|
||||
('not_supporting', 'Not Supporting'),
|
||||
]
|
||||
YARD_SIGN_CHOICES = [
|
||||
('none', 'None'),
|
||||
('wants', 'Wants a yard sign'),
|
||||
('has', 'Has a yard sign'),
|
||||
]
|
||||
|
||||
tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name='voters')
|
||||
voter_id = models.CharField(max_length=50, blank=True)
|
||||
first_name = models.CharField(max_length=100)
|
||||
last_name = models.CharField(max_length=100)
|
||||
|
||||
# Separated address fields
|
||||
street_address = models.CharField(max_length=255, blank=True)
|
||||
city = models.CharField(max_length=100, blank=True)
|
||||
state = models.CharField(max_length=50, blank=True)
|
||||
zip_code = models.CharField(max_length=20, blank=True)
|
||||
latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
|
||||
longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
|
||||
|
||||
phone = models.CharField(max_length=20, blank=True)
|
||||
email = models.EmailField(blank=True)
|
||||
district = models.CharField(max_length=100, blank=True)
|
||||
precinct = models.CharField(max_length=100, blank=True)
|
||||
registration_date = models.DateField(null=True, blank=True)
|
||||
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')
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.first_name} {self.last_name}"
|
||||
|
||||
class VotingRecord(models.Model):
|
||||
voter = models.ForeignKey(Voter, on_delete=models.CASCADE, related_name='voting_records')
|
||||
election_date = models.DateField()
|
||||
election_description = models.CharField(max_length=255)
|
||||
primary_party = models.CharField(max_length=100, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.voter} - {self.election_description}"
|
||||
|
||||
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)
|
||||
description = models.TextField(blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.event_type} on {self.date}"
|
||||
|
||||
class EventParticipation(models.Model):
|
||||
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name='participations')
|
||||
voter = models.ForeignKey(Voter, on_delete=models.CASCADE, related_name='event_participations')
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.voter} at {self.event}"
|
||||
|
||||
class Donation(models.Model):
|
||||
voter = models.ForeignKey(Voter, on_delete=models.CASCADE, related_name='donations')
|
||||
date = models.DateField()
|
||||
method = models.ForeignKey(DonationMethod, on_delete=models.SET_NULL, null=True)
|
||||
amount = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.voter} - {self.amount} on {self.date}"
|
||||
|
||||
class Interaction(models.Model):
|
||||
voter = models.ForeignKey(Voter, on_delete=models.CASCADE, related_name='interactions')
|
||||
type = models.ForeignKey(InteractionType, on_delete=models.SET_NULL, null=True)
|
||||
date = models.DateField()
|
||||
description = models.CharField(max_length=255)
|
||||
notes = models.TextField(blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.voter} - {self.type} on {self.date}"
|
||||
|
||||
class VoterLikelihood(models.Model):
|
||||
LIKELIHOOD_CHOICES = [
|
||||
('not_likely', 'Not Likely'),
|
||||
('somewhat_likely', 'Somewhat Likely'),
|
||||
('very_likely', 'Very Likely'),
|
||||
]
|
||||
voter = models.ForeignKey(Voter, on_delete=models.CASCADE, related_name='likelihoods')
|
||||
election_type = models.ForeignKey(ElectionType, on_delete=models.CASCADE)
|
||||
likelihood = models.CharField(max_length=20, choices=LIKELIHOOD_CHOICES)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('voter', 'election_type')
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.voter} - {self.election_type}: {self.get_likelihood_display()}"
|
||||
@ -1,25 +1,67 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{% block title %}Knowledge Base{% endblock %}</title>
|
||||
{% if project_description %}
|
||||
<meta name="description" content="{{ project_description }}">
|
||||
<meta property="og:description" content="{{ project_description }}">
|
||||
<meta property="twitter:description" content="{{ project_description }}">
|
||||
{% endif %}
|
||||
{% if project_image_url %}
|
||||
<meta property="og:image" content="{{ project_image_url }}">
|
||||
<meta property="twitter:image" content="{{ project_image_url }}">
|
||||
{% endif %}
|
||||
{% load static %}
|
||||
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
|
||||
{% block head %}{% endblock %}
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Grassroots Campaign Manager{% endblock %}</title>
|
||||
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Bootstrap Icons -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
|
||||
|
||||
{% load static %}
|
||||
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
|
||||
|
||||
{% if project_description %}
|
||||
<meta name="description" content="{{ project_description }}">
|
||||
{% endif %}
|
||||
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{% block content %}{% endblock %}
|
||||
</body>
|
||||
<nav class="navbar navbar-expand-lg sticky-top">
|
||||
<div class="container">
|
||||
<a class="navbar-brand d-flex align-items-center" href="/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" class="bi bi-person-check-fill me-2" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M15.854 5.146a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 0 1 .708-.708L12.5 7.793l2.646-2.647a.5.5 0 0 1 .708 0z"/>
|
||||
<path d="M1 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H1zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
|
||||
</svg>
|
||||
Grassroots
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/">Dashboard</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/voters/">Voters</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>
|
||||
{% if user.is_authenticated %}
|
||||
<span class="text-muted small me-2">{{ user.username }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer class="py-5 bg-white border-top mt-5">
|
||||
<div class="container text-center">
|
||||
<p class="text-muted mb-0">© 2026 Grassroots Campaign Manager. All rights reserved.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Bootstrap 5 JS Bundle -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,145 +1,64 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ project_name }}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg-color-start: #6a11cb;
|
||||
--bg-color-end: #2575fc;
|
||||
--text-color: #ffffff;
|
||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'><path d='M-10 10L110 10M10 -10L10 110' stroke-width='1' stroke='rgba(255,255,255,0.05)'/></svg>");
|
||||
animation: bg-pan 20s linear infinite;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
@keyframes bg-pan {
|
||||
0% {
|
||||
background-position: 0% 0%;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 100% 100%;
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card-bg-color);
|
||||
border: 1px solid var(--card-border-color);
|
||||
border-radius: 16px;
|
||||
padding: 2.5rem 2rem;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(2.2rem, 3vw + 1.2rem, 3.2rem);
|
||||
font-weight: 700;
|
||||
margin: 0 0 1.2rem;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.loader {
|
||||
margin: 1.5rem auto;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border: 4px solid rgba(255, 255, 255, 0.25);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.runtime code {
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
padding: 0.15rem 0.45rem;
|
||||
border-radius: 4px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.75;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<main>
|
||||
<div class="card">
|
||||
<h1>Analyzing your requirements and generating your app…</h1>
|
||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||
<span class="sr-only">Loading…</span>
|
||||
</div>
|
||||
<p class="hint">AppWizzy AI is collecting your requirements and applying the first changes.</p>
|
||||
<p class="hint">This page will refresh automatically as the plan is implemented.</p>
|
||||
<p class="runtime">
|
||||
Runtime: Django <code>{{ django_version }}</code> · Python <code>{{ python_version }}</code>
|
||||
— UTC <code>{{ current_time|date:"Y-m-d H:i:s" }}</code>
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
|
||||
</footer>
|
||||
<div class="container py-5">
|
||||
{% if not selected_tenant %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8 text-center">
|
||||
<h1 class="display-4 fw-bold text-emerald mb-4">Welcome to Grassroots</h1>
|
||||
<p class="lead text-slate mb-5">Select a campaign to begin managing your voter database and organizing your efforts.</p>
|
||||
|
||||
<div class="row g-4">
|
||||
{% for tenant in tenants %}
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100 border-0 shadow-sm hover-shadow transition">
|
||||
<div class="card-body p-4">
|
||||
<h3 class="h5 fw-bold mb-2">{{ tenant.name }}</h3>
|
||||
<p class="text-muted small mb-4">{{ tenant.description|truncatewords:20 }}</p>
|
||||
<a href="{% url 'select_campaign' tenant.id %}" class="btn btn-emerald w-100">Select Campaign</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="col-12">
|
||||
<div class="alert alert-info">No campaigns found. Please create one in the admin.</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="row">
|
||||
<div class="col-12 mb-4">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h1 class="h2 fw-bold">Dashboard: <span class="text-emerald">{{ selected_tenant.name }}</span></h1>
|
||||
<a href="{% url 'index' %}" class="btn btn-outline-secondary btn-sm">Switch Campaign</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 mb-4">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-muted">Voters Registry</h5>
|
||||
<p class="card-text display-6 fw-bold">{{ selected_tenant.voters.count }}</p>
|
||||
<a href="{% url 'voter_list' %}" class="btn btn-sm btn-emerald mt-2">View Registry</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-8 mb-4">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title mb-4">Quick Voter Search</h5>
|
||||
<form action="{% url 'voter_list' %}" method="GET" class="d-flex">
|
||||
<input type="text" name="q" class="form-control me-2" placeholder="Search by name or voter ID..." required>
|
||||
<button type="submit" class="btn btn-emerald px-4">Search</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
782
core/templates/core/voter_detail.html
Normal file
782
core/templates/core/voter_detail.html
Normal file
@ -0,0 +1,782 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-5">
|
||||
<!-- Breadcrumb -->
|
||||
<nav aria-label="breadcrumb" class="mb-4">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'index' %}" class="text-decoration-none">Dashboard</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'voter_list' %}" class="text-decoration-none">Voters</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">{{ voter.first_name }} {{ voter.last_name }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- Header / Demographics Card -->
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-body p-4 p-md-5">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-auto mb-3 mb-md-0">
|
||||
<div class="bg-primary text-white rounded-circle d-flex align-items-center justify-content-center shadow-sm" style="width: 80px; height: 80px; font-size: 2rem;">
|
||||
{{ voter.first_name|first }}{{ voter.last_name|first }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<div class="d-flex align-items-center">
|
||||
<h1 class="h3 mb-1 me-3">{{ voter.first_name }} {{ voter.last_name }}</h1>
|
||||
<button class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#editVoterModal">
|
||||
<i class="bi bi-pencil me-1"></i>Edit
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-muted mb-0">
|
||||
<i class="bi bi-geo-alt me-1"></i> {{ voter.address|default:"No address on file" }}
|
||||
</p>
|
||||
<div class="mt-2">
|
||||
<span class="badge bg-light text-dark border me-1">Voter ID: {{ voter.voter_id|default:"N/A" }}</span>
|
||||
<span class="badge bg-light text-dark border me-1">District: {{ voter.district|default:"-" }}</span>
|
||||
<span class="badge bg-light text-dark border">Precinct: {{ voter.precinct|default:"-" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-auto text-md-end mt-3 mt-md-0">
|
||||
{% if voter.candidate_support == 'supporting' %}
|
||||
<div class="h4 text-success mb-0"><i class="bi bi-check-circle-fill me-2"></i>Supporting</div>
|
||||
{% elif voter.candidate_support == 'not_supporting' %}
|
||||
<div class="h4 text-danger mb-0"><i class="bi bi-x-circle-fill me-2"></i>Not Supporting</div>
|
||||
{% else %}
|
||||
<div class="h4 text-secondary mb-0"><i class="bi bi-question-circle-fill me-2"></i>Unknown Support</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Left Column: Quick Stats & Likelihood -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header bg-white py-3">
|
||||
<h5 class="card-title mb-0">Contact Information</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="list-unstyled mb-0">
|
||||
<li class="mb-3">
|
||||
<label class="small text-muted d-block">Email</label>
|
||||
<span class="fw-semibold">{{ voter.email|default:"N/A" }}</span>
|
||||
</li>
|
||||
<li class="mb-3">
|
||||
<label class="small text-muted d-block">Phone</label>
|
||||
<span class="fw-semibold">{{ voter.phone|default:"N/A" }}</span>
|
||||
</li>
|
||||
<li>
|
||||
<label class="small text-muted d-block">Registration Date</label>
|
||||
<span class="fw-semibold">{{ voter.registration_date|date:"M d, Y"|default:"Unknown" }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title mb-0">Likelihood to Vote</h5>
|
||||
<button class="btn btn-sm btn-link text-primary p-0" data-bs-toggle="modal" data-bs-target="#addLikelihoodModal">
|
||||
<i class="bi bi-plus-circle"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% for likelihood in likelihoods %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-3 last-child-mb-0">
|
||||
<div>
|
||||
<span class="text-muted">{{ likelihood.election_type.name }}</span>
|
||||
{% if likelihood.likelihood == 'very_likely' %}
|
||||
<span class="badge bg-success ms-2">Very Likely</span>
|
||||
{% elif likelihood.likelihood == 'somewhat_likely' %}
|
||||
<span class="badge bg-info ms-2">Somewhat Likely</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary ms-2">Not Likely</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="text-nowrap ms-2">
|
||||
<button class="btn btn-sm btn-link text-primary p-0 me-2" data-bs-toggle="modal" data-bs-target="#editLikelihoodModal{{ likelihood.id }}">
|
||||
<i class="bi bi-pencil small"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-link text-danger p-0" data-bs-toggle="modal" data-bs-target="#deleteLikelihoodModal{{ likelihood.id }}">
|
||||
<i class="bi bi-trash small"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<p class="text-muted mb-0 italic">No likelihood data available.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header bg-white py-3">
|
||||
<h5 class="card-title mb-0">Campaign Assets</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-grow-1">
|
||||
<span class="text-muted">Yard Sign Status</span>
|
||||
</div>
|
||||
<div>
|
||||
{% if voter.yard_sign == 'has' %}
|
||||
<span class="badge bg-warning text-dark">Has Sign</span>
|
||||
{% elif voter.yard_sign == 'wants' %}
|
||||
<span class="badge bg-info">Wants Sign</span>
|
||||
{% else %}
|
||||
<span class="badge bg-light text-dark border">None</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Timeline & Detailed Records -->
|
||||
<div class="col-lg-8">
|
||||
<!-- Nav Tabs -->
|
||||
<ul class="nav nav-tabs border-0 mb-4" id="voterTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active border-0 py-3 px-4" id="interactions-tab" data-bs-toggle="tab" data-bs-target="#interactions" type="button" role="tab">Interactions</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link border-0 py-3 px-4" id="voting-tab" data-bs-toggle="tab" data-bs-target="#voting" type="button" role="tab">Voting History</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link border-0 py-3 px-4" id="donations-tab" data-bs-toggle="tab" data-bs-target="#donations" type="button" role="tab">Donations</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link border-0 py-3 px-4" id="events-tab" data-bs-toggle="tab" data-bs-target="#events" type="button" role="tab">Events</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content" id="voterTabsContent">
|
||||
<!-- Interactions Tab -->
|
||||
<div class="tab-pane fade show active" id="interactions" role="tabpanel">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">Interaction History</h6>
|
||||
<button class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#addInteractionModal">
|
||||
<i class="bi bi-plus-lg me-1"></i>New Interaction
|
||||
</button>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table align-middle mb-0">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th class="ps-4">Date</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>Notes</th>
|
||||
<th class="pe-4 text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for interaction in interactions %}
|
||||
<tr>
|
||||
<td class="ps-4 text-nowrap">{{ interaction.date|date:"M d, Y" }}</td>
|
||||
<td><span class="badge bg-light text-dark border">{{ interaction.type.name }}</span></td>
|
||||
<td>{{ interaction.description }}</td>
|
||||
<td class="small text-muted">{{ interaction.notes|truncatechars:30 }}</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="#editInteractionModal{{ interaction.id }}">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-link text-danger p-0" data-bs-toggle="modal" data-bs-target="#deleteInteractionModal{{ interaction.id }}">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="5" class="text-center py-4 text-muted">No interactions recorded.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Voting History Tab -->
|
||||
<div class="tab-pane fade" id="voting" role="tabpanel">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="table-responsive">
|
||||
<table class="table align-middle mb-0">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th class="ps-4">Election Date</th>
|
||||
<th>Description</th>
|
||||
<th class="pe-4">Party</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for record in voting_records %}
|
||||
<tr>
|
||||
<td class="ps-4 text-nowrap">{{ record.election_date|date:"M d, Y" }}</td>
|
||||
<td>{{ record.election_description }}</td>
|
||||
<td class="pe-4">{{ record.primary_party|default:"-" }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="3" class="text-center py-4 text-muted">No voting records found.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Donations Tab -->
|
||||
<div class="tab-pane fade" id="donations" role="tabpanel">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">Donation History</h6>
|
||||
<button class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#addDonationModal">
|
||||
<i class="bi bi-plus-lg me-1"></i>Add Donation
|
||||
</button>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table align-middle mb-0">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th class="ps-4">Date</th>
|
||||
<th>Method</th>
|
||||
<th class="text-end">Amount</th>
|
||||
<th class="pe-4 text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for donation in donations %}
|
||||
<tr>
|
||||
<td class="ps-4 text-nowrap">{{ donation.date|date:"M d, Y" }}</td>
|
||||
<td>{{ donation.method.name }}</td>
|
||||
<td class="text-end fw-semibold text-success">${{ donation.amount }}</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="#editDonationModal{{ donation.id }}">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-link text-danger p-0" data-bs-toggle="modal" data-bs-target="#deleteDonationModal{{ donation.id }}">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="4" class="text-center py-4 text-muted">No donations recorded.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Events Tab -->
|
||||
<div class="tab-pane fade" id="events" role="tabpanel">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">Event Participation</h6>
|
||||
<button class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#addEventParticipationModal">
|
||||
<i class="bi bi-plus-lg me-1"></i>Add Participation
|
||||
</button>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table align-middle mb-0">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th class="ps-4">Date</th>
|
||||
<th>Event Type</th>
|
||||
<th>Description</th>
|
||||
<th class="pe-4 text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% 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 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 }}">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-link text-danger p-0" data-bs-toggle="modal" data-bs-target="#deleteEventParticipationModal{{ participation.id }}">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="4" class="text-center py-4 text-muted">No event participations found.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Voter Modal -->
|
||||
<div class="modal fade" id="editVoterModal" tabindex="-1" aria-labelledby="editVoterModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content border-0">
|
||||
<div class="modal-header border-0 bg-light">
|
||||
<h5 class="modal-title" id="editVoterModalLabel">Edit Voter Profile</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form action="{% url 'voter_edit' voter.id %}" method="POST">
|
||||
{% csrf_token %}
|
||||
<div class="modal-body p-4">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-medium">{{ voter_form.first_name.label }}</label>
|
||||
{{ voter_form.first_name }}
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-medium">{{ voter_form.last_name.label }}</label>
|
||||
{{ voter_form.last_name }}
|
||||
</div>
|
||||
<div class="col-12 mb-3">
|
||||
<label class="form-label fw-medium">{{ voter_form.address.label }}</label>
|
||||
{{ voter_form.address }}
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-medium">{{ voter_form.phone.label }}</label>
|
||||
{{ voter_form.phone }}
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-medium">{{ voter_form.email.label }}</label>
|
||||
{{ voter_form.email }}
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label fw-medium">{{ voter_form.voter_id.label }}</label>
|
||||
{{ voter_form.voter_id }}
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label fw-medium">{{ voter_form.district.label }}</label>
|
||||
{{ voter_form.district }}
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label fw-medium">{{ voter_form.precinct.label }}</label>
|
||||
{{ voter_form.precinct }}
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-medium">{{ voter_form.registration_date.label }}</label>
|
||||
{{ voter_form.registration_date }}
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-medium">{{ voter_form.candidate_support.label }}</label>
|
||||
{{ voter_form.candidate_support }}
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-medium">{{ voter_form.yard_sign.label }}</label>
|
||||
{{ voter_form.yard_sign }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer border-0 p-4 pt-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>
|
||||
|
||||
<!-- Add Interaction Modal -->
|
||||
<div class="modal fade" id="addInteractionModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content border-0">
|
||||
<div class="modal-header border-0 bg-light">
|
||||
<h5 class="modal-title">New Interaction</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form action="{% url 'add_interaction' voter.id %}" method="POST">
|
||||
{% csrf_token %}
|
||||
<div class="modal-body p-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-medium">{{ interaction_form.type.label }}</label>
|
||||
{{ interaction_form.type }}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-medium">{{ interaction_form.date.label }}</label>
|
||||
{{ interaction_form.date }}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-medium">{{ interaction_form.description.label }}</label>
|
||||
{{ interaction_form.description }}
|
||||
</div>
|
||||
<div class="mb-0">
|
||||
<label class="form-label fw-medium">{{ interaction_form.notes.label }}</label>
|
||||
{{ interaction_form.notes }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer border-0 p-4 pt-0">
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary px-4">Log Interaction</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Interaction Modals -->
|
||||
{% for interaction in interactions %}
|
||||
<div class="modal fade" id="editInteractionModal{{ interaction.id }}" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content border-0">
|
||||
<div class="modal-header border-0 bg-light">
|
||||
<h5 class="modal-title">Edit Interaction</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form action="{% url 'edit_interaction' interaction.id %}" method="POST">
|
||||
{% csrf_token %}
|
||||
<div class="modal-body p-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-medium">Type</label>
|
||||
<select name="type" class="form-select">
|
||||
{% for type in interaction_form.fields.type.queryset %}
|
||||
<option value="{{ type.id }}" {% if type.id == interaction.type.id %}selected{% endif %}>{{ type.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-medium">Date</label>
|
||||
<input type="date" name="date" class="form-control" value="{{ interaction.date|date:'Y-m-d' }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-medium">Description</label>
|
||||
<input type="text" name="description" class="form-control" value="{{ interaction.description }}">
|
||||
</div>
|
||||
<div class="mb-0">
|
||||
<label class="form-label fw-medium">Notes</label>
|
||||
<textarea name="notes" class="form-control" rows="2">{{ interaction.notes }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer border-0 p-4 pt-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>
|
||||
|
||||
<!-- Delete Interaction Modal -->
|
||||
<div class="modal fade" id="deleteInteractionModal{{ interaction.id }}" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-sm">
|
||||
<div class="modal-content border-0">
|
||||
<div class="modal-header border-0 bg-light">
|
||||
<h5 class="modal-title">Delete Interaction?</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body p-4 text-center">
|
||||
<p class="text-muted mb-0">Are you sure you want to delete this interaction?</p>
|
||||
</div>
|
||||
<div class="modal-footer border-0 p-4 pt-0 justify-content-center">
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal">No, Keep</button>
|
||||
<form action="{% url 'delete_interaction' interaction.id %}" method="POST">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-danger">Yes, Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<!-- Add Donation Modal -->
|
||||
<div class="modal fade" id="addDonationModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content border-0">
|
||||
<div class="modal-header border-0 bg-light">
|
||||
<h5 class="modal-title">Add Donation</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form action="{% url 'add_donation' voter.id %}" method="POST">
|
||||
{% csrf_token %}
|
||||
<div class="modal-body p-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-medium">{{ donation_form.amount.label }}</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">$</span>
|
||||
{{ donation_form.amount }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-medium">{{ donation_form.date.label }}</label>
|
||||
{{ donation_form.date }}
|
||||
</div>
|
||||
<div class="mb-0">
|
||||
<label class="form-label fw-medium">{{ donation_form.method.label }}</label>
|
||||
{{ donation_form.method }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer border-0 p-4 pt-0">
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary px-4">Record Donation</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Donation Modals -->
|
||||
{% for donation in donations %}
|
||||
<div class="modal fade" id="editDonationModal{{ donation.id }}" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content border-0">
|
||||
<div class="modal-header border-0 bg-light">
|
||||
<h5 class="modal-title">Edit Donation</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form action="{% url 'edit_donation' donation.id %}" method="POST">
|
||||
{% csrf_token %}
|
||||
<div class="modal-body p-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-medium">Amount</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">$</span>
|
||||
<input type="number" name="amount" class="form-control" step="0.01" value="{{ donation.amount }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-medium">Date</label>
|
||||
<input type="date" name="date" class="form-control" value="{{ donation.date|date:'Y-m-d' }}">
|
||||
</div>
|
||||
<div class="mb-0">
|
||||
<label class="form-label fw-medium">Method</label>
|
||||
<select name="method" class="form-select">
|
||||
{% for method in donation_form.fields.method.queryset %}
|
||||
<option value="{{ method.id }}" {% if method.id == donation.method.id %}selected{% endif %}>{{ method.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer border-0 p-4 pt-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>
|
||||
|
||||
<!-- Delete Donation Modal -->
|
||||
<div class="modal fade" id="deleteDonationModal{{ donation.id }}" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-sm">
|
||||
<div class="modal-content border-0">
|
||||
<div class="modal-header border-0 bg-light">
|
||||
<h5 class="modal-title">Delete Donation?</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body p-4 text-center">
|
||||
<p class="text-muted mb-0">Are you sure you want to delete this donation record?</p>
|
||||
</div>
|
||||
<div class="modal-footer border-0 p-4 pt-0 justify-content-center">
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal">No, Keep</button>
|
||||
<form action="{% url 'delete_donation' donation.id %}" method="POST">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-danger">Yes, Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<!-- Add Likelihood Modal -->
|
||||
<div class="modal fade" id="addLikelihoodModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content border-0">
|
||||
<div class="modal-header border-0 bg-light">
|
||||
<h5 class="modal-title">Update Voter Likelihood</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form action="{% url 'add_likelihood' voter.id %}" method="POST">
|
||||
{% csrf_token %}
|
||||
<div class="modal-body p-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-medium">{{ likelihood_form.election_type.label }}</label>
|
||||
{{ likelihood_form.election_type }}
|
||||
</div>
|
||||
<div class="mb-0">
|
||||
<label class="form-label fw-medium">{{ likelihood_form.likelihood.label }}</label>
|
||||
{{ likelihood_form.likelihood }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer border-0 p-4 pt-0">
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary px-4">Update</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Likelihood Modals -->
|
||||
{% for likelihood in likelihoods %}
|
||||
<div class="modal fade" id="editLikelihoodModal{{ likelihood.id }}" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content border-0">
|
||||
<div class="modal-header border-0 bg-light">
|
||||
<h5 class="modal-title">Edit Likelihood</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form action="{% url 'edit_likelihood' likelihood.id %}" method="POST">
|
||||
{% csrf_token %}
|
||||
<div class="modal-body p-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-medium">Election Type</label>
|
||||
<select name="election_type" class="form-select">
|
||||
{% for et in likelihood_form.fields.election_type.queryset %}
|
||||
<option value="{{ et.id }}" {% if et.id == likelihood.election_type.id %}selected{% endif %}>{{ et.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-0">
|
||||
<label class="form-label fw-medium">Likelihood</label>
|
||||
<select name="likelihood" class="form-select">
|
||||
<option value="not_likely" {% if likelihood.likelihood == 'not_likely' %}selected{% endif %}>Not Likely</option>
|
||||
<option value="somewhat_likely" {% if likelihood.likelihood == 'somewhat_likely' %}selected{% endif %}>Somewhat Likely</option>
|
||||
<option value="very_likely" {% if likelihood.likelihood == 'very_likely' %}selected{% endif %}>Very Likely</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer border-0 p-4 pt-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>
|
||||
|
||||
<!-- Delete Likelihood Modal -->
|
||||
<div class="modal fade" id="deleteLikelihoodModal{{ likelihood.id }}" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-sm">
|
||||
<div class="modal-content border-0">
|
||||
<div class="modal-header border-0 bg-light">
|
||||
<h5 class="modal-title">Delete Likelihood?</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body p-4 text-center">
|
||||
<p class="text-muted mb-0">Delete likelihood for <strong>{{ likelihood.election_type.name }}</strong>?</p>
|
||||
</div>
|
||||
<div class="modal-footer border-0 p-4 pt-0 justify-content-center">
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal">No</button>
|
||||
<form action="{% url 'delete_likelihood' likelihood.id %}" method="POST">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-danger">Yes, Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<!-- Add Event Participation Modal -->
|
||||
<div class="modal fade" id="addEventParticipationModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content border-0">
|
||||
<div class="modal-header border-0 bg-light">
|
||||
<h5 class="modal-title">Add Event Participation</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form action="{% url 'add_event_participation' voter.id %}" method="POST">
|
||||
{% csrf_token %}
|
||||
<div class="modal-body p-4">
|
||||
<div class="mb-0">
|
||||
<label class="form-label fw-medium">Select Event</label>
|
||||
{{ event_participation_form.event }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer border-0 p-4 pt-0">
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary px-4">Add Participation</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Event Participation Modals -->
|
||||
{% for participation in event_participations %}
|
||||
<div class="modal fade" id="editEventParticipationModal{{ participation.id }}" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content border-0">
|
||||
<div class="modal-header border-0 bg-light">
|
||||
<h5 class="modal-title">Edit Participation</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form action="{% url 'edit_event_participation' participation.id %}" method="POST">
|
||||
{% csrf_token %}
|
||||
<div class="modal-body p-4">
|
||||
<div class="mb-0">
|
||||
<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>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer border-0 p-4 pt-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>
|
||||
|
||||
<!-- Delete Event Participation Modal -->
|
||||
<div class="modal fade" id="deleteEventParticipationModal{{ participation.id }}" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-sm">
|
||||
<div class="modal-content border-0">
|
||||
<div class="modal-header border-0 bg-light">
|
||||
<h5 class="modal-title">Remove Participation?</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body p-4 text-center">
|
||||
<p class="text-muted mb-0">Are you sure you want to remove this event participation?</p>
|
||||
</div>
|
||||
<div class="modal-footer border-0 p-4 pt-0 justify-content-center">
|
||||
<button type="button" class="btn btn-light" data-bs-dismiss="modal">No</button>
|
||||
<form action="{% url 'delete_event_participation' participation.id %}" method="POST">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-danger">Yes, Remove</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<style>
|
||||
.nav-tabs .nav-link {
|
||||
color: #6c757d;
|
||||
font-weight: 500;
|
||||
border-radius: 0;
|
||||
}
|
||||
.nav-tabs .nav-link.active {
|
||||
color: #059669;
|
||||
background: transparent;
|
||||
border-bottom: 2px solid #059669 !important;
|
||||
}
|
||||
.last-child-mb-0:last-child {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.modal-content {
|
||||
border-radius: 1rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
.form-control, .form-select {
|
||||
border-color: #e5e7eb;
|
||||
padding: 0.6rem 0.8rem;
|
||||
}
|
||||
.form-control:focus, .form-select:focus {
|
||||
border-color: #059669;
|
||||
box-shadow: 0 0 0 0.25rem rgba(5, 150, 105, 0.1);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
79
core/templates/core/voter_list.html
Normal file
79
core/templates/core/voter_list.html
Normal file
@ -0,0 +1,79 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-5">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h2">Voter Registry</h1>
|
||||
<a href="/admin/core/voter/add/" class="btn btn-primary btn-sm">+ Add New Voter</a>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-body p-4">
|
||||
<form action="." method="GET" class="row g-3">
|
||||
<div class="col-md-10">
|
||||
<input type="text" name="q" class="form-control" placeholder="Search by name or Voter ID..." value="{{ query|default:'' }}">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="submit" class="btn btn-primary w-100">Filter</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0 align-middle">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th class="ps-4">Voter ID</th>
|
||||
<th>Name</th>
|
||||
<th>District</th>
|
||||
<th>Support</th>
|
||||
<th>Yard Sign</th>
|
||||
<th class="text-end pe-4">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for voter in voters %}
|
||||
<tr>
|
||||
<td class="ps-4"><code>{{ voter.voter_id|default:"N/A" }}</code></td>
|
||||
<td>
|
||||
<div class="fw-semibold text-dark">{{ voter.first_name }} {{ voter.last_name }}</div>
|
||||
<div class="small text-muted">{{ voter.address|truncatechars:40 }}</div>
|
||||
</td>
|
||||
<td><span class="badge bg-light text-dark border">{{ voter.district|default:"-" }}</span></td>
|
||||
<td>
|
||||
{% if voter.candidate_support == 'supporting' %}
|
||||
<span class="badge bg-success-subtle text-success">Supporting</span>
|
||||
{% elif voter.candidate_support == 'not_supporting' %}
|
||||
<span class="badge bg-danger-subtle text-danger">Not Supporting</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary-subtle text-secondary">Unknown</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if voter.yard_sign == 'has' %}
|
||||
<span class="badge bg-warning-subtle text-warning-emphasis">Has Sign</span>
|
||||
{% elif voter.yard_sign == 'wants' %}
|
||||
<span class="badge bg-info-subtle text-info-emphasis">Wants Sign</span>
|
||||
{% else %}
|
||||
<span class="text-muted small">None</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-end pe-4">
|
||||
<a href="{% url 'voter_detail' voter.id %}" class="btn btn-sm btn-outline-primary">View 360°</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="6" class="text-center py-5 text-muted">
|
||||
<p class="mb-0">No voters found matching your search.</p>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
25
core/urls.py
25
core/urls.py
@ -1,7 +1,26 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import home
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path("", home, name="home"),
|
||||
path('', views.index, name='index'),
|
||||
path('select-campaign/<int:tenant_id>/', views.select_campaign, name='select_campaign'),
|
||||
path('voters/', views.voter_list, name='voter_list'),
|
||||
path('voters/<int:voter_id>/', views.voter_detail, name='voter_detail'),
|
||||
path('voters/<int:voter_id>/edit/', views.voter_edit, name='voter_edit'),
|
||||
|
||||
path('voters/<int:voter_id>/interaction/add/', views.add_interaction, name='add_interaction'),
|
||||
path('interaction/<int:interaction_id>/edit/', views.edit_interaction, name='edit_interaction'),
|
||||
path('interaction/<int:interaction_id>/delete/', views.delete_interaction, name='delete_interaction'),
|
||||
|
||||
path('voters/<int:voter_id>/donation/add/', views.add_donation, name='add_donation'),
|
||||
path('donation/<int:donation_id>/edit/', views.edit_donation, name='edit_donation'),
|
||||
path('donation/<int:donation_id>/delete/', views.delete_donation, name='delete_donation'),
|
||||
|
||||
path('voters/<int:voter_id>/likelihood/add/', views.add_likelihood, name='add_likelihood'),
|
||||
path('likelihood/<int:likelihood_id>/edit/', views.edit_likelihood, name='edit_likelihood'),
|
||||
path('likelihood/<int:likelihood_id>/delete/', views.delete_likelihood, name='delete_likelihood'),
|
||||
|
||||
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'),
|
||||
]
|
||||
|
||||
287
core/views.py
287
core/views.py
@ -1,25 +1,270 @@
|
||||
import os
|
||||
import platform
|
||||
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 django import get_version as django_version
|
||||
from django.shortcuts import render
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
def home(request):
|
||||
"""Render the landing screen with loader and environment details."""
|
||||
host_name = request.get_host().lower()
|
||||
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic"
|
||||
now = timezone.now()
|
||||
def index(request):
|
||||
"""
|
||||
Main landing page for Grassroots Campaign Manager.
|
||||
Displays a list of campaigns if the user is logged in but hasn't selected one.
|
||||
"""
|
||||
tenants = Tenant.objects.all()
|
||||
selected_tenant_id = request.session.get('tenant_id')
|
||||
selected_tenant = None
|
||||
if selected_tenant_id:
|
||||
selected_tenant = Tenant.objects.filter(id=selected_tenant_id).first()
|
||||
|
||||
context = {
|
||||
"project_name": "New Style",
|
||||
"agent_brand": agent_brand,
|
||||
"django_version": django_version(),
|
||||
"python_version": platform.python_version(),
|
||||
"current_time": now,
|
||||
"host_name": host_name,
|
||||
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
|
||||
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
||||
'tenants': tenants,
|
||||
'selected_tenant': selected_tenant,
|
||||
}
|
||||
return render(request, "core/index.html", context)
|
||||
return render(request, 'core/index.html', context)
|
||||
|
||||
def select_campaign(request, tenant_id):
|
||||
"""
|
||||
Sets the selected campaign in the session.
|
||||
"""
|
||||
tenant = get_object_or_404(Tenant, id=tenant_id)
|
||||
request.session['tenant_id'] = tenant.id
|
||||
messages.success(request, f"You are now managing: {tenant.name}")
|
||||
return redirect('index')
|
||||
|
||||
def voter_list(request):
|
||||
"""
|
||||
List and search voters. Restricted to selected tenant.
|
||||
"""
|
||||
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)
|
||||
query = request.GET.get('q')
|
||||
voters = Voter.objects.filter(tenant=tenant)
|
||||
|
||||
if query:
|
||||
query = query.strip()
|
||||
search_filter = Q(first_name__icontains=query) | Q(last_name__icontains=query) | Q(voter_id__icontains=query)
|
||||
|
||||
if " " in query:
|
||||
parts = query.split()
|
||||
if len(parts) >= 2:
|
||||
first_part = parts[0]
|
||||
last_part = " ".join(parts[1:])
|
||||
search_filter |= Q(first_name__icontains=first_part, last_name__icontains=last_part)
|
||||
|
||||
voters = voters.filter(search_filter)
|
||||
|
||||
context = {
|
||||
'voters': voters,
|
||||
'query': query,
|
||||
'selected_tenant': tenant
|
||||
}
|
||||
return render(request, 'core/voter_list.html', context)
|
||||
|
||||
def voter_detail(request, voter_id):
|
||||
"""
|
||||
360-degree view of a voter.
|
||||
"""
|
||||
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)
|
||||
voter = get_object_or_404(Voter, id=voter_id, tenant=tenant)
|
||||
|
||||
context = {
|
||||
'voter': voter,
|
||||
'selected_tenant': tenant,
|
||||
'voting_records': voter.voting_records.all().order_by('-election_date'),
|
||||
'donations': voter.donations.all().order_by('-date'),
|
||||
'interactions': voter.interactions.all().order_by('-date'),
|
||||
'event_participations': voter.event_participations.all().order_by('-event__date'),
|
||||
'likelihoods': voter.likelihoods.all(),
|
||||
'voter_form': VoterForm(instance=voter),
|
||||
'interaction_form': InteractionForm(tenant=tenant),
|
||||
'donation_form': DonationForm(tenant=tenant),
|
||||
'likelihood_form': VoterLikelihoodForm(tenant=tenant),
|
||||
'event_participation_form': EventParticipationForm(tenant=tenant),
|
||||
}
|
||||
return render(request, 'core/voter_detail.html', context)
|
||||
|
||||
def voter_edit(request, voter_id):
|
||||
"""
|
||||
Update voter core demographics.
|
||||
"""
|
||||
selected_tenant_id = request.session.get('tenant_id')
|
||||
tenant = get_object_or_404(Tenant, id=selected_tenant_id)
|
||||
voter = get_object_or_404(Voter, id=voter_id, tenant=tenant)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = VoterForm(request.POST, instance=voter)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, "Voter profile updated successfully.")
|
||||
return redirect('voter_detail', voter_id=voter.id)
|
||||
return redirect('voter_detail', voter_id=voter.id)
|
||||
|
||||
def add_interaction(request, voter_id):
|
||||
selected_tenant_id = request.session.get('tenant_id')
|
||||
tenant = get_object_or_404(Tenant, id=selected_tenant_id)
|
||||
voter = get_object_or_404(Voter, id=voter_id, tenant=tenant)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = InteractionForm(request.POST, tenant=tenant)
|
||||
if form.is_valid():
|
||||
interaction = form.save(commit=False)
|
||||
interaction.voter = voter
|
||||
interaction.save()
|
||||
messages.success(request, "Interaction added.")
|
||||
return redirect('voter_detail', voter_id=voter.id)
|
||||
|
||||
def edit_interaction(request, interaction_id):
|
||||
selected_tenant_id = request.session.get('tenant_id')
|
||||
tenant = get_object_or_404(Tenant, id=selected_tenant_id)
|
||||
interaction = get_object_or_404(Interaction, id=interaction_id, voter__tenant=tenant)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = InteractionForm(request.POST, instance=interaction, tenant=tenant)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, "Interaction updated.")
|
||||
return redirect('voter_detail', voter_id=interaction.voter.id)
|
||||
|
||||
def delete_interaction(request, interaction_id):
|
||||
selected_tenant_id = request.session.get('tenant_id')
|
||||
tenant = get_object_or_404(Tenant, id=selected_tenant_id)
|
||||
interaction = get_object_or_404(Interaction, id=interaction_id, voter__tenant=tenant)
|
||||
voter_id = interaction.voter.id
|
||||
|
||||
if request.method == 'POST':
|
||||
interaction.delete()
|
||||
messages.success(request, "Interaction deleted.")
|
||||
return redirect('voter_detail', voter_id=voter_id)
|
||||
|
||||
def add_donation(request, voter_id):
|
||||
selected_tenant_id = request.session.get('tenant_id')
|
||||
tenant = get_object_or_404(Tenant, id=selected_tenant_id)
|
||||
voter = get_object_or_404(Voter, id=voter_id, tenant=tenant)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = DonationForm(request.POST, tenant=tenant)
|
||||
if form.is_valid():
|
||||
donation = form.save(commit=False)
|
||||
donation.voter = voter
|
||||
donation.save()
|
||||
messages.success(request, "Donation recorded.")
|
||||
return redirect('voter_detail', voter_id=voter.id)
|
||||
|
||||
def edit_donation(request, donation_id):
|
||||
selected_tenant_id = request.session.get('tenant_id')
|
||||
tenant = get_object_or_404(Tenant, id=selected_tenant_id)
|
||||
donation = get_object_or_404(Donation, id=donation_id, voter__tenant=tenant)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = DonationForm(request.POST, instance=donation, tenant=tenant)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, "Donation updated.")
|
||||
return redirect('voter_detail', voter_id=donation.voter.id)
|
||||
|
||||
def delete_donation(request, donation_id):
|
||||
selected_tenant_id = request.session.get('tenant_id')
|
||||
tenant = get_object_or_404(Tenant, id=selected_tenant_id)
|
||||
donation = get_object_or_404(Donation, id=donation_id, voter__tenant=tenant)
|
||||
voter_id = donation.voter.id
|
||||
|
||||
if request.method == 'POST':
|
||||
donation.delete()
|
||||
messages.success(request, "Donation deleted.")
|
||||
return redirect('voter_detail', voter_id=voter_id)
|
||||
|
||||
def add_likelihood(request, voter_id):
|
||||
selected_tenant_id = request.session.get('tenant_id')
|
||||
tenant = get_object_or_404(Tenant, id=selected_tenant_id)
|
||||
voter = get_object_or_404(Voter, id=voter_id, tenant=tenant)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = VoterLikelihoodForm(request.POST, tenant=tenant)
|
||||
if form.is_valid():
|
||||
likelihood = form.save(commit=False)
|
||||
likelihood.voter = voter
|
||||
# Handle potential duplicate election_type
|
||||
VoterLikelihood.objects.filter(voter=voter, election_type=likelihood.election_type).delete()
|
||||
likelihood.save()
|
||||
messages.success(request, "Likelihood updated.")
|
||||
return redirect('voter_detail', voter_id=voter.id)
|
||||
|
||||
def edit_likelihood(request, likelihood_id):
|
||||
selected_tenant_id = request.session.get('tenant_id')
|
||||
tenant = get_object_or_404(Tenant, id=selected_tenant_id)
|
||||
likelihood = get_object_or_404(VoterLikelihood, id=likelihood_id, voter__tenant=tenant)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = VoterLikelihoodForm(request.POST, instance=likelihood, tenant=tenant)
|
||||
if form.is_valid():
|
||||
# Check for conflict with another record of same election_type
|
||||
election_type = form.cleaned_data['election_type']
|
||||
if VoterLikelihood.objects.filter(voter=likelihood.voter, election_type=election_type).exclude(id=likelihood.id).exists():
|
||||
VoterLikelihood.objects.filter(voter=likelihood.voter, election_type=election_type).exclude(id=likelihood.id).delete()
|
||||
form.save()
|
||||
messages.success(request, "Likelihood updated.")
|
||||
return redirect('voter_detail', voter_id=likelihood.voter.id)
|
||||
|
||||
def delete_likelihood(request, likelihood_id):
|
||||
selected_tenant_id = request.session.get('tenant_id')
|
||||
tenant = get_object_or_404(Tenant, id=selected_tenant_id)
|
||||
likelihood = get_object_or_404(VoterLikelihood, id=likelihood_id, voter__tenant=tenant)
|
||||
voter_id = likelihood.voter.id
|
||||
|
||||
if request.method == 'POST':
|
||||
likelihood.delete()
|
||||
messages.success(request, "Likelihood record deleted.")
|
||||
return redirect('voter_detail', voter_id=voter_id)
|
||||
|
||||
def add_event_participation(request, voter_id):
|
||||
selected_tenant_id = request.session.get('tenant_id')
|
||||
tenant = get_object_or_404(Tenant, id=selected_tenant_id)
|
||||
voter = get_object_or_404(Voter, id=voter_id, tenant=tenant)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = EventParticipationForm(request.POST, tenant=tenant)
|
||||
if form.is_valid():
|
||||
participation = form.save(commit=False)
|
||||
participation.voter = voter
|
||||
# Avoid duplicate participation
|
||||
if not EventParticipation.objects.filter(voter=voter, event=participation.event).exists():
|
||||
participation.save()
|
||||
messages.success(request, "Event participation added.")
|
||||
else:
|
||||
messages.warning(request, "Voter is already participating in this event.")
|
||||
return redirect('voter_detail', voter_id=voter.id)
|
||||
|
||||
def edit_event_participation(request, participation_id):
|
||||
selected_tenant_id = request.session.get('tenant_id')
|
||||
tenant = get_object_or_404(Tenant, id=selected_tenant_id)
|
||||
participation = get_object_or_404(EventParticipation, id=participation_id, voter__tenant=tenant)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = EventParticipationForm(request.POST, instance=participation, tenant=tenant)
|
||||
if form.is_valid():
|
||||
event = form.cleaned_data['event']
|
||||
if EventParticipation.objects.filter(voter=participation.voter, event=event).exclude(id=participation.id).exists():
|
||||
messages.warning(request, "Voter is already participating in that event.")
|
||||
else:
|
||||
form.save()
|
||||
messages.success(request, "Event participation updated.")
|
||||
return redirect('voter_detail', voter_id=participation.voter.id)
|
||||
|
||||
def delete_event_participation(request, participation_id):
|
||||
selected_tenant_id = request.session.get('tenant_id')
|
||||
tenant = get_object_or_404(Tenant, id=selected_tenant_id)
|
||||
participation = get_object_or_404(EventParticipation, id=participation_id, voter__tenant=tenant)
|
||||
voter_id = participation.voter.id
|
||||
|
||||
if request.method == 'POST':
|
||||
participation.delete()
|
||||
messages.success(request, "Event participation removed.")
|
||||
return redirect('voter_detail', voter_id=voter_id)
|
||||
@ -1,9 +1,9 @@
|
||||
<?php
|
||||
// Generated by setup_mariadb_project.sh — edit as needed.
|
||||
define('DB_HOST', '127.0.0.1');
|
||||
define('DB_NAME', 'app_37737');
|
||||
define('DB_USER', 'app_37737');
|
||||
define('DB_PASS', 'e1644473-e554-4867-a6ea-5cf35176c57c');
|
||||
define('DB_NAME', 'app_37769');
|
||||
define('DB_USER', 'app_37769');
|
||||
define('DB_PASS', '05736420-f47f-46c0-9196-f6ddfdb0d67a');
|
||||
|
||||
function db() {
|
||||
static $pdo;
|
||||
|
||||
@ -1,4 +1,109 @@
|
||||
/* Custom styles for the application */
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Outfit:wght@500;600;700&display=swap');
|
||||
|
||||
:root {
|
||||
--primary-color: #059669; /* Emerald 600 */
|
||||
--primary-hover: #047857; /* Emerald 700 */
|
||||
--secondary-color: #475569; /* Slate 600 */
|
||||
--accent-color: #F59E0B; /* Amber 500 */
|
||||
--background-color: #F8FAFC; /* Slate 50 */
|
||||
--text-color: #1E293B; /* Slate 800 */
|
||||
--border-color: #E2E8F0; /* Slate 200 */
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
background-color: var(--background-color);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: 'Outfit', sans-serif;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-family: 'Outfit', sans-serif;
|
||||
font-weight: 700;
|
||||
color: var(--primary-color) !important;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
font-weight: 600;
|
||||
padding: 0.6rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--primary-hover);
|
||||
border-color: var(--primary-hover);
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
padding: 5rem 0;
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f0fdf4 100%);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 3.5rem;
|
||||
line-height: 1.1;
|
||||
margin-bottom: 1.5rem;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1.25rem;
|
||||
color: var(--secondary-color);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.search-container {
|
||||
background: white;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.search-input {
|
||||
border: none;
|
||||
padding: 0.75rem 1rem;
|
||||
width: 100%;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
background-color: #ecfdf5;
|
||||
color: var(--primary-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
@ -1,21 +1,109 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Outfit:wght@500;600;700&display=swap');
|
||||
|
||||
:root {
|
||||
--bg-color-start: #6a11cb;
|
||||
--bg-color-end: #2575fc;
|
||||
--text-color: #ffffff;
|
||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
||||
--primary-color: #059669; /* Emerald 600 */
|
||||
--primary-hover: #047857; /* Emerald 700 */
|
||||
--secondary-color: #475569; /* Slate 600 */
|
||||
--accent-color: #F59E0B; /* Amber 500 */
|
||||
--background-color: #F8FAFC; /* Slate 50 */
|
||||
--text-color: #1E293B; /* Slate 800 */
|
||||
--border-color: #E2E8F0; /* Slate 200 */
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
background-color: var(--background-color);
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: 'Outfit', sans-serif;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-family: 'Outfit', sans-serif;
|
||||
font-weight: 700;
|
||||
color: var(--primary-color) !important;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
font-weight: 600;
|
||||
padding: 0.6rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--primary-hover);
|
||||
border-color: var(--primary-hover);
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
padding: 5rem 0;
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f0fdf4 100%);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 3.5rem;
|
||||
line-height: 1.1;
|
||||
margin-bottom: 1.5rem;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1.25rem;
|
||||
color: var(--secondary-color);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.search-container {
|
||||
background: white;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.search-input {
|
||||
border: none;
|
||||
padding: 0.75rem 1rem;
|
||||
width: 100%;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
background-color: #ecfdf5;
|
||||
color: var(--primary-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user