37769-vm/core/views.py
Flatlogic Bot e4aeae1b74 .8
2026-01-24 22:51:48 +00:00

359 lines
16 KiB
Python

from django.http import JsonResponse
from django.urls import reverse
from django.shortcuts import render, redirect, get_object_or_404
from django.db.models import Q, Sum
from django.contrib import messages
from .models import Voter, Tenant, Interaction, Donation, VoterLikelihood, EventParticipation, Event, EventType, InteractionType, DonationMethod, ElectionType
from .forms import VoterForm, InteractionForm, DonationForm, VoterLikelihoodForm, EventParticipationForm
import logging
logger = logging.getLogger(__name__)
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
metrics = {}
if selected_tenant_id:
selected_tenant = Tenant.objects.filter(id=selected_tenant_id).first()
if selected_tenant:
voters = selected_tenant.voters.all()
metrics = {
"total_registered_voters": voters.count(),
"total_target_voters": voters.filter(is_targeted=True).count(),
"total_supporting": voters.filter(candidate_support="supporting").count(),
"total_voter_addresses": voters.values("address").distinct().count(),
"total_door_visits": Interaction.objects.filter(voter__tenant=selected_tenant, type__name="Door Visit").count(),
"total_signs": voters.filter(Q(yard_sign="wants") | Q(yard_sign="has")).count(),
"total_donations": Donation.objects.filter(voter__tenant=selected_tenant).aggregate(total=Sum("amount"))["total"] or 0,
}
context = {
'tenants': tenants,
'selected_tenant': selected_tenant,
'metrics': metrics,
}
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 = [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]
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':
# Log incoming coordinate data for debugging
lat_raw = request.POST.get('latitude')
lon_raw = request.POST.get('longitude')
logger.info(f"Voter Edit POST: lat={lat_raw}, lon={lon_raw}")
form = VoterForm(request.POST, instance=voter)
if form.is_valid():
# If coordinates were provided in POST, ensure they are applied to the instance
# This handles cases where readonly or other widget settings might interfere
voter = form.save(commit=False)
if lat_raw:
try:
voter.latitude = lat_raw
except: pass
if lon_raw:
try:
voter.longitude = lon_raw
except: pass
voter.save()
messages.success(request, "Voter profile updated successfully.")
else:
logger.warning(f"Voter Edit Form Invalid: {form.errors}")
for field, errors in form.errors.items():
for error in errors:
messages.error(request, f"Error in {field}: {error}")
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(reverse('voter_detail', kwargs={'voter_id': voter.id}) + '?active_tab=interactions')
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(reverse('voter_detail', kwargs={'voter_id': interaction.voter.id}) + '?active_tab=interactions')
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(reverse('voter_detail', kwargs={'voter_id': voter_id}) + '?active_tab=interactions')
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(reverse('voter_detail', kwargs={'voter_id': voter.id}) + '?active_tab=donations')
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(reverse('voter_detail', kwargs={'voter_id': donation.voter.id}) + '?active_tab=donations')
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(reverse('voter_detail', kwargs={'voter_id': voter_id}) + '?active_tab=donations')
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(reverse('voter_detail', kwargs={'voter_id': voter.id}) + '?active_tab=events')
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(reverse('voter_detail', kwargs={'voter_id': participation.voter.id}) + '?active_tab=events')
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(reverse('voter_detail', kwargs={'voter_id': voter_id}) + '?active_tab=events')
def voter_geocode(request, voter_id):
"""
Manually trigger geocoding for a voter, potentially using values from the request.
"""
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':
street = request.POST.get('address_street', voter.address_street)
city = request.POST.get('city', voter.city)
state = request.POST.get('state', voter.state)
zip_code = request.POST.get('zip_code', voter.zip_code)
parts = [street, city, state, zip_code]
full_address = ", ".join([p for p in parts if p])
# Use a temporary instance to avoid saving until the user clicks "Save" in the modal
temp_voter = Voter(
address_street=street,
city=city,
state=state,
zip_code=zip_code,
address=full_address
)
success, error_msg = temp_voter.geocode_address()
if success:
return JsonResponse({
'success': True,
'latitude': str(temp_voter.latitude),
'longitude': str(temp_voter.longitude),
'address': full_address
})
else:
return JsonResponse({
'success': False,
'error': f"Geocoding failed: {error_msg or 'No results found.'}"
})
return JsonResponse({'success': False, 'error': 'Invalid request method.'})