from django.shortcuts import render, get_object_or_404, redirect from django.contrib.auth.decorators import login_required from django.db.models import Count, Case, When, IntegerField, Sum, F, DecimalField from django.db.models.functions import Coalesce from django.http import JsonResponse, HttpResponseForbidden, HttpResponseBadRequest, HttpResponse from django.core.paginator import Paginator from django.forms import modelformset_factory from .models import Tenant, Voter, Interaction, Event, EventParticipation, Volunteer, VolunteerEvent, CampaignSettings, ParticipationStatus, VoterLikelihood, ScheduledCall, VolunteerRole, EventType, TenantUserRole from .forms import VoterForm, EventForm, VolunteerForm, ScheduledCallForm, InteractionForm from datetime import datetime, date, time, timedelta from django.utils import timezone import pytz import re from django.conf import settings from django.template.defaultfilters import date as date_filter import csv # Import necessary modules for Twilio from twilio.rest import Client from twilio.base.exceptions import TwilioRestException import logging logger = logging.getLogger(__name__) # Helper function to get the current tenant def get_current_tenant(request): if request.user.is_authenticated: tenant_role = TenantUserRole.objects.filter(user=request.user).first() if tenant_role: return tenant_role.tenant return None def get_tenant_campaign_settings(tenant): if tenant: try: return CampaignSettings.objects.get(tenant=tenant) except CampaignSettings.DoesNotExist: pass return None @login_required def dashboard(request): user_tenants = Tenant.objects.filter(user_roles__user=request.user) if not user_tenants.exists(): return redirect('admin:index') selected_tenant_id = request.session.get('selected_tenant_id') if selected_tenant_id: selected_tenant = get_object_or_404(Tenant, pk=selected_tenant_id) if selected_tenant not in user_tenants: # If the selected tenant is not among the user's tenants, reset session and show selection del request.session['selected_tenant_id'] return redirect('dashboard') else: # If no tenant is selected, and there's only one available, select it automatically if user_tenants.count() == 1: selected_tenant = user_tenants.first() request.session['selected_tenant_id'] = selected_tenant.id else: # Otherwise, prompt the user to select a tenant return render(request, 'core/index.html', {'tenants': user_tenants, 'selected_tenant': None}) campaign_settings = get_tenant_campaign_settings(selected_tenant) # Total Voters total_voters = Voter.objects.filter(tenant=selected_tenant).count() # Total Interactions total_interactions = Interaction.objects.filter(voter__tenant=selected_tenant).count() return render(request, 'core/index.html', { 'total_voters': total_voters, 'total_interactions': total_interactions, 'campaign_settings': campaign_settings, 'selected_tenant': selected_tenant, 'tenants': user_tenants, # Pass all tenants for potential switching }) @login_required def select_campaign(request, tenant_id): # Ensure the tenant exists and the user has access to it tenant = get_object_or_404(Tenant, pk=tenant_id, user_roles__user=request.user) request.session['selected_tenant_id'] = tenant.id return redirect('dashboard') # Placeholder views for other functions @login_required def voter_list(request): tenant = get_current_tenant(request) if not tenant: return redirect('admin:index') voters = Voter.objects.filter(tenant=tenant) return render(request, 'core/voter_list.html', {'voters': voters}) @login_required def voter_detail(request, pk): tenant = get_current_tenant(request) if not tenant: return redirect('admin:index') voter = get_object_or_404(Voter, pk=pk, tenant=tenant) return render(request, 'core/voter_detail.html', {'voter': voter}) @login_required def voter_create(request): tenant = get_current_tenant(request) if not tenant: return redirect('admin:index') if request.method == 'POST': form = VoterForm(request.POST) if form.is_valid(): voter = form.save(commit=False) voter.tenant = tenant voter.save() return redirect('voter_detail', pk=voter.pk) else: form = VoterForm() return render(request, 'core/voter_form.html', {'form': form}) @login_required def voter_update(request, voter_id): tenant = get_current_tenant(request) if not tenant: return redirect('admin:index') voter = get_object_or_404(Voter, pk=voter_id, tenant=tenant) if request.method == 'POST': form = VoterForm(request.POST, instance=voter) if form.is_valid(): form.save() return redirect('voter_detail', pk=voter.pk) else: form = VoterForm(instance=voter) return render(request, 'core/voter_form.html', {'form': form, 'voter': voter}) @login_required def voter_delete(request, pk): tenant = get_current_tenant(request) if not tenant: return redirect('admin:index') voter = get_object_or_404(Voter, pk=pk, tenant=tenant) if request.method == 'POST': voter.delete() return redirect('voter_list') return render(request, 'core/voter_confirm_delete.html', {'voter': voter}) @login_required def interaction_list(request): tenant = get_current_tenant(request) if not tenant: return redirect('admin:index') interactions = Interaction.objects.filter(voter__tenant=tenant) return render(request, 'core/interaction_list.html', {'interactions': interactions}) @login_required def interaction_detail(request, pk): tenant = get_current_tenant(request) if not tenant: return redirect('admin:index') interaction = get_object_or_404(Interaction, pk=pk, voter__tenant=tenant) return render(request, 'core/interaction_detail.html', {'interaction': interaction}) @login_required def interaction_create(request, voter_id): tenant = get_current_tenant(request) if not tenant: return redirect('admin:index') voter = get_object_or_404(Voter, pk=voter_id, tenant=tenant) if request.method == 'POST': form = InteractionForm(request.POST) if form.is_valid(): interaction = form.save(commit=False) interaction.voter = voter # Assign volunteer if relevant or leave null interaction.save() return redirect('voter_detail', pk=voter.pk) else: form = InteractionForm() return render(request, 'core/interaction_form.html', {'form': form, 'voter': voter}) @login_required def interaction_update(request, pk): tenant = get_current_tenant(request) if not tenant: return redirect('admin:index') interaction = get_object_or_404(Interaction, pk=pk, voter__tenant=tenant) if request.method == 'POST': form = InteractionForm(request.POST, instance=interaction) if form.is_valid(): form.save() return redirect('voter_detail', pk=interaction.voter.pk) else: form = InteractionForm(instance=interaction) return render(request, 'core/interaction_form.html', {'form': form, 'interaction': interaction}) @login_required def interaction_delete(request, pk): tenant = get_current_tenant(request) if not tenant: return redirect('admin:index') interaction = get_object_or_404(Interaction, pk=pk, voter__tenant=tenant) if request.method == 'POST': interaction.delete() return redirect('voter_detail', pk=interaction.voter.pk) return render(request, 'core/interaction_confirm_delete.html', {'interaction': interaction}) @login_required def donation_list(request): tenant = get_current_tenant(request) if not tenant: return redirect('admin:index') donations = Interaction.objects.filter(voter__tenant=tenant) return render(request, 'core/donation_list.html', {'donations': donations}) @login_required def donation_detail(request, pk): tenant = get_current_tenant(request) if not tenant: return redirect('admin:index') donation = get_object_or_404(Interaction, pk=pk, voter__tenant=tenant) return render(request, 'core/donation_detail.html', {'donation': donation}) @login_required def donation_create(request, voter_pk): return HttpResponse("Placeholder for donation_create") @login_required def donation_update(request, voter_pk, pk): return HttpResponse("Placeholder for donation_update") @login_required def donation_delete(request, voter_pk, pk): return HttpResponse("Placeholder for donation_delete") @login_required def likelihood_create(request, voter_pk): return HttpResponse("Placeholder for likelihood_create") @login_required def likelihood_update(request, voter_pk, pk): return HttpResponse("Placeholder for likelihood_update") @login_required def likelihood_delete(request, voter_pk, pk): return HttpResponse("Placeholder for likelihood_delete") @login_required def event_list(request): tenant = get_current_tenant(request) if not tenant: return redirect('admin:index') events = Event.objects.filter(tenant=tenant) return render(request, 'core/event_list.html', {'events': events}) @login_required def event_detail(request, pk): tenant = get_current_tenant(request) if not tenant: return redirect('admin:index') event = get_object_or_404(Event, pk=pk, tenant=tenant) return render(request, 'core/event_detail.html', {'event': event}) @login_required def event_create(request): tenant = get_current_tenant(request) if not tenant: return redirect('admin:index') if request.method == 'POST': form = EventForm(request.POST) if form.is_valid(): event = form.save(commit=False) event.tenant = tenant event.save() return redirect('event_detail', pk=event.pk) else: form = EventForm() return render(request, 'core/event_form.html', {'form': form}) @login_required def event_update(request, pk): tenant = get_current_tenant(request) if not tenant: return redirect('admin:index') event = get_object_or_404(Event, pk=pk, tenant=tenant) if request.method == 'POST': form = EventForm(request.POST, instance=event) if form.is_valid(): form.save() return redirect('event_detail', pk=event.pk) else: form = EventForm(instance=event) return render(request, 'core/event_form.html', {'form': form, 'event': event}) @login_required def event_delete(request, pk): tenant = get_current_tenant(request) if not tenant: return redirect('admin:index') event = get_object_or_404(Event, pk=pk, tenant=tenant) if request.method == 'POST': event.delete() return redirect('event_list') return render(request, 'core/event_confirm_delete.html', {'event': event}) @login_required def event_add_participant(request, pk): return HttpResponse("Placeholder for event_add_participant") @login_required def event_edit_participant(request, event_pk, pk): return HttpResponse("Placeholder for event_edit_participant") @login_required def event_delete_participant(request, event_pk, pk): return HttpResponse("Placeholder for event_delete_participant") @login_required def event_participation_create(request, voter_id): return HttpResponse("Placeholder for event_participation_create") @login_required def event_participation_update(request, pk): return HttpResponse("Placeholder for event_participation_update") @login_required def event_participation_delete(request, pk): return HttpResponse("Placeholder for event_participation_delete") @login_required def volunteer_list(request): tenant = get_current_tenant(request) if not tenant: return redirect('admin:index') volunteers = Volunteer.objects.filter(tenant=tenant) return render(request, 'core/volunteer_list.html', {'volunteers': volunteers}) @login_required def volunteer_detail(request, pk): tenant = get_current_tenant(request) if not tenant: return redirect('admin:index') volunteer = get_object_or_404(Volunteer, pk=pk, tenant=tenant) return render(request, 'core/volunteer_detail.html', {'volunteer': volunteer}) @login_required def volunteer_create(request): tenant = get_current_tenant(request) if not tenant: return redirect('admin:index') if request.method == 'POST': form = VolunteerForm(request.POST) if form.is_valid(): volunteer = form.save(commit=False) volunteer.tenant = tenant volunteer.save() return redirect('volunteer_detail', pk=volunteer.pk) else: form = VolunteerForm() return render(request, 'core/volunteer_form.html', {'form': form}) @login_required def volunteer_edit(request, pk): tenant = get_current_tenant(request) if not tenant: return redirect('admin:index') volunteer = get_object_or_404(Volunteer, pk=pk, tenant=tenant) if request.method == 'POST': form = VolunteerForm(request.POST, instance=volunteer) if form.is_valid(): form.save() return redirect('volunteer_detail', pk=volunteer.pk) else: form = VolunteerForm(instance=volunteer) return render(request, 'core/volunteer_form.html', {'form': form, 'voter': volunteer.pk}) @login_required def volunteer_delete(request, pk): tenant = get_current_tenant(request) if not tenant: return redirect('admin:index') volunteer = get_object_or_404(Volunteer, pk=pk, tenant=tenant) if request.method == 'POST': volunteer.delete() return redirect('volunteer_list') return render(request, 'core/volunteer_confirm_delete.html', {'volunteer': volunteer}) @login_required def voter_geocode(request, voter_pk): return HttpResponse("Placeholder for voter_geocode") @login_required def export_voters_csv(request): return HttpResponse("Placeholder for export_voters_csv") @login_required def create_scheduled_call(request, voter_id): return HttpResponse("Placeholder for create_scheduled_call") @login_required def scheduled_call_list(request): tenant = get_current_tenant(request) if not tenant: return redirect('admin:index') scheduled_calls = ScheduledCall.objects.filter(tenant=tenant) return render(request, 'core/scheduled_call_list.html', {'scheduled_calls': scheduled_calls}) @login_required def scheduled_call_detail(request, pk): tenant = get_current_tenant(request) if not tenant: return redirect('admin:index') scheduled_call = get_object_or_404(ScheduledCall, pk=pk, tenant=tenant) return render(request, 'core/scheduled_call_detail.html', {'scheduled_call': scheduled_call}) @login_required def scheduled_call_create(request, voter_pk): tenant = get_current_tenant(request) if not tenant: return redirect('admin:index') voter = get_object_or_404(Voter, pk=voter_pk, tenant=tenant) if request.method == 'POST': form = ScheduledCallForm(request.POST) if form.is_valid(): scheduled_call = form.save(commit=False) scheduled_call.voter = voter scheduled_call.tenant = tenant scheduled_call.save() return redirect('scheduled_call_detail', pk=scheduled_call.pk) else: form = ScheduledCallForm(initial={'voter': voter}) return render(request, 'core/scheduled_call_form.html', {'form': form, 'voter': voter}) @login_required def scheduled_call_edit(request, voter_pk, pk): tenant = get_current_tenant(request) if not tenant: return redirect('admin:index') scheduled_call = get_object_or_404(ScheduledCall, pk=pk, voter__pk=voter_pk, tenant=tenant) if request.method == 'POST': form = ScheduledCallForm(request.POST, instance=scheduled_call) if form.is_valid(): form.save() return redirect('scheduled_call_detail', pk=scheduled_call.pk) else: form = ScheduledCallForm(instance=scheduled_call) return render(request, 'core/scheduled_call_form.html', {'form': form, 'voter': scheduled_call.voter}) @login_required def scheduled_call_delete(request, voter_pk, pk): tenant = get_current_tenant(request) if not tenant: return redirect('admin:index') scheduled_call = get_object_or_404(ScheduledCall, pk=pk, voter__pk=voter_pk, tenant=tenant) if request.method == 'POST': scheduled_call.delete() return redirect('scheduled_call_list') return render(request, 'core/scheduled_call_confirm_delete.html', {'scheduled_call': scheduled_call}) @login_required def bulk_schedule_calls(request): return HttpResponse("Placeholder for bulk_schedule_calls") @login_required def voter_search_json(request): return HttpResponse("Placeholder for voter_search_json") @login_required def import_participants(request): return HttpResponse("Placeholder for import_participants") @login_required def import_participants_map_fields(request): return HttpResponse("Placeholder for import_participants_map_fields") @login_required def process_participants_import(request): return HttpResponse("Placeholder for process_participants_import") @login_required def match_participants(request): return HttpResponse("Placeholder for match_participants") @login_required def interest_add(request): return HttpResponse("Placeholder for interest_add") @login_required def interest_delete(request): return HttpResponse("Placeholder for interest_delete") @login_required def volunteer_add(request): return HttpResponse("Placeholder for volunteer_add") @login_required def volunteer_assign_event(request, volunteer_pk): return HttpResponse("Placeholder for volunteer_assign_event") @login_required def volunteer_remove_event(request, volunteer_pk, event_pk): return HttpResponse("Placeholder for volunteer_remove_event") @login_required def volunteer_search_json(request): return HttpResponse("Placeholder for volunteer_search_json") @login_required def bulk_send_sms(request): return HttpResponse("Placeholder for bulk_send_sms") @login_required def create_interaction_for_voter(request, voter_pk): return HttpResponse("Placeholder for create_interaction_for_voter") @login_required def complete_call(request, call_pk): return HttpResponse("Placeholder for complete_call") @login_required def door_visits(request): tenant = get_current_tenant(request) if not tenant: return redirect('admin:index') # Get voters for the current tenant that have door_visit set to True door_to_door_voters = Voter.objects.filter(tenant=tenant, door_visit=True) # Dictionary to store visited households with the latest visit date visited_households = {} for voter in door_to_door_voters: # Construct a unique key for the household (e.g., street address and city) household_key = f"{voter.address_street.lower().strip()}-{voter.city.lower().strip()}" # Find the latest interaction for this voter latest_interaction = Interaction.objects.filter(voter=voter).order_by('-date').first() # Update visited_households with the latest interaction date for the household if household_key not in visited_households or (latest_interaction and latest_interaction.date > visited_households[household_key]['last_visit_date']): visited_households[household_key] = { 'voter': voter, 'last_visit_date': latest_interaction.date if latest_interaction else None, 'voters_in_household': [] } # Ensure 'last_visit_date' is always present in the dictionary for comparison if 'last_visit_date' not in visited_households[household_key]: visited_households[household_key]['last_visit_date'] = None visited_households[household_key]['voters_in_household'].append(voter) # Sort households by the last visit date, with None dates appearing last sorted_households = sorted(visited_households.values(), key=lambda x: x['last_visit_date'] if x['last_visit_date'] is not None else datetime.min.replace(tzinfo=pytz.UTC), reverse=True) # Render the door_visits.html template with the sorted household data return render(request, 'core/door_visits.html', {'households': sorted_households}) @login_required def door_visit_history(request, voter_pk): tenant = get_current_tenant(request) if not tenant: return redirect('admin:index') voter = get_object_or_404(Voter, pk=voter_pk, tenant=tenant) interactions = Interaction.objects.filter(voter=voter).order_by('-date') context = { 'voter': voter, 'interactions': interactions } return render(request, 'core/door_visit_history.html', context) @login_required def voter_advanced_search(request): return HttpResponse("Placeholder for voter_advanced_search") @login_required def volunteer_event_create(request, event_id): return HttpResponse("Placeholder for volunteer_event_create") @login_required def volunteer_event_delete(request, pk): return HttpResponse("Placeholder for volunteer_event_delete") @login_required def call_queue(request): return HttpResponse("Placeholder for call_queue") @login_required def profile(request): return HttpResponse("Placeholder for profile") @login_required def volunteer_bulk_send_sms(request): return HttpResponse("Placeholder for volunteer_bulk_send_sms")