import re import json import logging from django.shortcuts import render, redirect, get_object_or_404 from django.db.models import Q from django.contrib import messages from django.core.paginator import Paginator from django.conf import settings from .models import Voter, Tenant, CampaignSettings from .permissions import role_required logger = logging.getLogger(__name__) @role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin']) def yard_sign_voters(request): """ Manage yard sign requests. Groups voters who want a yard sign by household. Enhanced to ensure neighborhoods are correctly identified even if some voters in a household have empty fields. """ 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) city_filter = request.GET.get("city", "").strip() district_filter = request.GET.get('district', '').strip() neighborhood_filter = request.GET.get('neighborhood', '').strip() address_filter = request.GET.get('address', '').strip() # Initial queryset: voters who want a yard sign for this tenant voters = Voter.objects.filter(tenant=tenant, is_inactive=False, yard_sign='wants') if city_filter: voters = voters.filter(city__icontains=city_filter) if district_filter: voters = voters.filter(district=district_filter) if neighborhood_filter: voters = voters.filter(neighborhood__icontains=neighborhood_filter) if address_filter: voters = voters.filter(Q(address__icontains=address_filter) | Q(address_street__icontains=address_filter)) # Grouping by household (unique address) households_dict = {} for voter in voters: # Normalize address components for robust grouping street = (voter.address_street or "").strip() city = (voter.city or "").strip() state = (voter.state or "").strip() zip_code = (voter.zip_code or "").strip() key = (street.lower(), city.lower(), state.lower(), zip_code.lower()) if key not in households_dict: street_number = "" street_name = street match = re.match(r'^(\d+)\s+(.*)$', street) if match: street_number = match.group(1) street_name = match.group(2) try: street_number_sort = int(street_number) except ValueError: street_number_sort = 0 households_dict[key] = { 'address_street': street, 'city': city, 'state': state, 'zip_code': zip_code, 'neighborhood': (voter.neighborhood or "").strip(), 'district': (voter.district or "").strip(), 'latitude': float(voter.latitude) if voter.latitude else None, 'longitude': float(voter.longitude) if voter.longitude else None, 'street_name_sort': street_name.lower(), 'street_number_sort': street_number_sort, 'voters_who_want_sign': [], } else: # Pick a non-empty neighborhood/district if the current one is empty if not households_dict[key]['neighborhood'] and voter.neighborhood: households_dict[key]['neighborhood'] = voter.neighborhood.strip() if not households_dict[key]['district'] and voter.district: households_dict[key]['district'] = voter.district.strip() households_dict[key]['voters_who_want_sign'].append(voter) households_list = list(households_dict.values()) # Sort by neighborhood, then street name, then street number households_list.sort(key=lambda x: ( (x['neighborhood'] or '').lower(), x['street_name_sort'], x['street_number_sort'] )) # Prepare data for Google Map map_data = [ { 'lat': h['latitude'], 'lng': h['longitude'], 'address': f"{h['address_street']}, {h['city']}, {h['state']}", 'voters': ", ".join([f"{v.first_name} {v.last_name}" for v in h['voters_who_want_sign']]), 'notes': ", ".join([f"{v.first_name}: {v.notes}" for v in h['voters_who_want_sign'] if v.notes]), 'voter_ids': [v.id for v in h['voters_who_want_sign']] } for h in households_list if h['latitude'] and h['longitude'] ] paginator = Paginator(households_list, 50) page_number = request.GET.get('page') households_page = paginator.get_page(page_number) context = { 'selected_tenant': tenant, 'households': households_page, 'district_filter': district_filter, 'neighborhood_filter': neighborhood_filter, 'address_filter': address_filter, "city_filter": city_filter, 'map_data_json': json.dumps(map_data), 'GOOGLE_MAPS_API_KEY': getattr(settings, 'GOOGLE_MAPS_API_KEY', ''), } return render(request, 'core/yard_sign_voters.html', context)