def door_visits(request): """ Manage door knocking visits. Groups unvisited targeted voters by household. """ 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) # Filters from GET parameters district_filter = request.GET.get('district', '').strip() neighborhood_filter = request.GET.get('neighborhood', '').strip() address_filter = request.GET.get('address', '').strip() # Initial queryset: unvisited targeted voters for this tenant voters = Voter.objects.filter(tenant=tenant, door_visit=False, is_targeted=True) # Apply filters if provided 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_street__icontains=address_filter) | Q(address__icontains=address_filter)) # Grouping by household (unique address) households_dict = {} for voter in voters: # Key for grouping is the unique address components key = (voter.address_street, voter.city, voter.state, voter.zip_code) if key not in households_dict: # Parse street name and number for sorting street_number = "" street_name = voter.address_street match = re.match(r'^(\d+)\s+(.*)$', voter.address_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': voter.address_street, 'city': voter.city, 'state': voter.state, 'zip_code': voter.zip_code, 'neighborhood': voter.neighborhood, 'district': voter.district, '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, 'target_voters': [], 'voters_json': [] } households_dict[key]['target_voters'].append(voter) households_dict[key]['voters_json'].append({'id': voter.id, 'name': f"{voter.first_name} {voter.last_name}"}) households_list = list(households_dict.values()) for h in households_list: h['voters_json_str'] = json.dumps(h['voters_json']) households_list.sort(key=lambda x: ( (x['neighborhood'] or '').lower(), x['street_name_sort'], x['street_number_sort'] )) # Prepare data for Google Map (all filtered households with coordinates) 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['target_voters']]) } 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, 'map_data_json': json.dumps(map_data), 'google_maps_api_key': getattr(settings, 'GOOGLE_MAPS_API_KEY', ''), 'visit_form': DoorVisitLogForm(), } return render(request, 'core/door_visits.html', context) @role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin'], permission='core.view_voter') def log_door_visit(request): """ Mark all targeted voters at a specific address as visited, update their flags, and create interaction records. """ selected_tenant_id = request.session.get("tenant_id") if not selected_tenant_id: return redirect("index") tenant = get_object_or_404(Tenant, id=selected_tenant_id) campaign_settings, _ = CampaignSettings.objects.get_or_create(tenant=tenant) # Capture query string for redirecting back with filters next_qs = request.POST.get("next_query_string", "") redirect_url = reverse("door_visits") if next_qs: redirect_url += f"?{next_qs}" # Get the volunteer linked to the current user volunteer = Volunteer.objects.filter(user=request.user, tenant=tenant).first() if request.method == "POST": form = DoorVisitLogForm(request.POST) if form.is_valid(): address_street = request.POST.get("address_street") city = request.POST.get("city") state = request.POST.get("state") zip_code = request.POST.get("zip_code") outcome = form.cleaned_data["outcome"] notes = form.cleaned_data["notes"] wants_yard_sign = form.cleaned_data["wants_yard_sign"] candidate_support = form.cleaned_data["candidate_support"] follow_up = form.cleaned_data["follow_up"] follow_up_voter_id = form.cleaned_data.get("follow_up_voter") call_notes = form.cleaned_data["call_notes"] # Determine date/time in campaign timezone campaign_tz_name = campaign_settings.timezone or "America/Chicago" try: tz = zoneinfo.ZoneInfo(campaign_tz_name) except: tz = zoneinfo.ZoneInfo("America/Chicago") interaction_date = timezone.now().astimezone(tz) # Get or create InteractionType interaction_type, _ = InteractionType.objects.get_or_create(tenant=tenant, name="Door Visit") # Find targeted voters at this exact address voters = Voter.objects.filter( tenant=tenant, address_street=address_street, city=city, state=state, zip_code=zip_code, is_targeted=True ) if not voters.exists(): messages.warning(request, f"No targeted voters found at {address_street}.") return redirect(redirect_url) # Get default caller for follow-ups default_caller = None if follow_up: default_caller = Volunteer.objects.filter(tenant=tenant, is_default_caller=True).first() for voter in voters: # 1) Update voter flags voter.door_visit = True # 2) If "Wants a Yard Sign" checkbox is selected if wants_yard_sign: voter.yard_sign = "wants" # 3) Update support status if Supporting or Not Supporting if candidate_support in ["supporting", "not_supporting"]: voter.candidate_support = candidate_support voter.save() # 4) Create interaction Interaction.objects.create( voter=voter, volunteer=volunteer, type=interaction_type, date=interaction_date, description=outcome, notes=notes ) # 5) Create ScheduledCall if follow_up is checked and this is the selected voter if follow_up and follow_up_voter_id and str(voter.id) == follow_up_voter_id: ScheduledCall.objects.create( tenant=tenant, voter=voter, volunteer=default_caller, comments=call_notes, status="pending" ) if follow_up: messages.success(request, f"Door visit logged and follow-up call scheduled for {address_street}.") else: messages.success(request, f"Door visit logged for {address_street}.") else: messages.error(request, "There was an error in the visit log form.") return redirect(redirect_url)