From 8de72675e50e5d407a99920b2d9f3702348f5205 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sun, 8 Feb 2026 21:35:24 +0000 Subject: [PATCH] Autosave: 20260208-213524 --- core/__pycache__/forms.cpython-311.pyc | Bin 38712 -> 39161 bytes core/__pycache__/views.cpython-311.pyc | Bin 114957 -> 116723 bytes core/forms.py | 7 +- core/templates/core/door_visits.html | 100 ++++++---- core/templates/core/log_door_visit.html | 150 +++++++++++++++ core/views.py | 88 ++++++--- core_views_log_visit.py | 144 ++++++++++++++ door_views_update.py | 241 +++++++++++++----------- door_visit_patch.py | 156 +++++++++++++++ fix_views.py | 22 +++ fix_views_v2.py | 13 ++ patch_log_visit.py | 33 ++++ 12 files changed, 785 insertions(+), 169 deletions(-) create mode 100644 core/templates/core/log_door_visit.html create mode 100644 core_views_log_visit.py create mode 100644 door_visit_patch.py create mode 100644 fix_views.py create mode 100644 fix_views_v2.py create mode 100644 patch_log_visit.py diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc index dea8a66c6703a0709541ec320334f2b38c7018c1..cd2900782011f1d7a1ddea3d3d8f0484cca99843 100644 GIT binary patch delta 731 zcmdn7j_Kz{Ccfpoyj%=G(8$-3X&$kW&$@xhjA`;*NtMmH4Y`b5tROiMNT2+uQA<&5 z84Cl$Y9NMyDB%?G6p1;E9SmuVDUvN*Q6iINo8%d#CfhcN%Sx}|S;h=h4a88@q8$ts zj8S6244N{VbDLZlH8fd@WPx%;lR-o}h?oK-{4^PF@s{P6q!z^|XXIxlrxs6skSE5N zHn|{JZ?c0UAG1geAY&o2tgEY2+P$xnC7FUr-6kI&4@EQyal%gDe`>@`{6 zSjAD|0|O(cGvf_Yrw*4M?+X%|S9r9p@EBbHqYum=If#lPRglqjK%$DvIWZ?k!4K%0 zVomYM$C^9Eid=yvf?acqD;X#mp9hkhd{9D6*ajp6B36Se{RAX_Z0>7uWvthjka|T- zeFM{u&=WQ%OwZU{h)=taQFyf9gzO>?trTNop&KhRA#HZN$`VP?F) z`D~XMBeyS5qzFV@XPjKLW<}3qmqt@_}g;02W=%V*mgE delta 426 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 WMxuIA1_tLuIr8h` -
{{ household.address_street }}
-
{{ household.city }}
+ +
{{ household.address_street }}
+
{{ household.city }}
+
{% if household.neighborhood %} @@ -128,12 +130,12 @@
    {% if households.has_previous %}
  • - +
  • - +
  • @@ -143,12 +145,12 @@ {% if households.has_next %}
  • - +
  • - +
  • @@ -263,8 +265,10 @@
+{% endblock %} - +{% block extra_js %} + {% if GOOGLE_MAPS_API_KEY %} {% endif %} @@ -302,7 +306,17 @@ }); marker.addListener('click', function() { - infowindow.setContent('' + item.address + '
' + item.voters); + var logUrl = "{% url 'log_door_visit' %}?address_street=" + encodeURIComponent(item.address_street) + + "&city=" + encodeURIComponent(item.city) + + "&state=" + encodeURIComponent(item.state) + + "&zip_code=" + encodeURIComponent(item.zip_code) + + "&next_query_string=" + encodeURIComponent("{{ request.GET.urlencode|escapejs }}") + "" + "&source=map"; + + var content = '' + item.address + '
' + + item.voters + '

' + + 'Log Visit'; + + infowindow.setContent(content); infowindow.open(map, marker); }); @@ -317,20 +331,32 @@ } document.addEventListener('DOMContentLoaded', function() { + // Automatically open map if requested + const urlParams = new URLSearchParams(window.location.search); + if (urlParams.get('open_map') === '1') { + var mapModalElement = document.getElementById('mapModal'); + if (mapModalElement) { + var mapModal = new bootstrap.Modal(mapModalElement); + mapModal.show(); + } + } + // Trigger map initialization when modal is shown - var mapModal = document.getElementById('mapModal'); - if (mapModal) { - mapModal.addEventListener('shown.bs.modal', function () { + var mapModalElement = document.getElementById('mapModal'); + if (mapModalElement) { + mapModalElement.addEventListener('shown.bs.modal', function () { if (!map) { initMap(); } else { - google.maps.event.trigger(map, 'resize'); - if (markers.length > 0) { - var bounds = new google.maps.LatLngBounds(); - markers.forEach(function(marker) { - bounds.extend(marker.getPosition()); - }); - map.fitBounds(bounds); + if (window.google && window.google.maps) { + google.maps.event.trigger(map, 'resize'); + if (markers.length > 0) { + var bounds = new google.maps.LatLngBounds(); + markers.forEach(function(marker) { + bounds.extend(marker.getPosition()); + }); + map.fitBounds(bounds); + } } } }); @@ -340,30 +366,36 @@ if (logVisitModal) { logVisitModal.addEventListener('show.bs.modal', function (event) { var button = event.relatedTarget; + if (!button) return; + var address = button.getAttribute('data-address'); var city = button.getAttribute('data-city'); var state = button.getAttribute('data-state'); var zip = button.getAttribute('data-zip'); - document.getElementById('modalAddressDisplay').textContent = address + ', ' + city + ', ' + state + ' ' + zip; - document.getElementById('modal_address_street').value = address; - document.getElementById('modal_city').value = city; - document.getElementById('modal_state').value = state; - document.getElementById('modal_zip_code').value = zip; + document.getElementById('modalAddressDisplay').textContent = (address || '') + ', ' + (city || '') + ', ' + (state || '') + ' ' + (zip || ''); + document.getElementById('modal_address_street').value = address || ''; + document.getElementById('modal_city').value = city || ''; + document.getElementById('modal_state').value = state || ''; + document.getElementById('modal_zip_code').value = zip || ''; // Populate voters dropdown var votersJson = button.getAttribute('data-voters'); if (votersJson) { - var voters = JSON.parse(votersJson); - var voterSelect = document.getElementById('{{ visit_form.follow_up_voter.id_for_label }}'); - if (voterSelect) { - voterSelect.innerHTML = ''; - voters.forEach(function(voter) { - var option = document.createElement('option'); - option.value = voter.id; - option.textContent = voter.name; - voterSelect.appendChild(option); - }); + try { + var voters = JSON.parse(votersJson); + var voterSelect = document.getElementById('{{ visit_form.follow_up_voter.id_for_label }}'); + if (voterSelect) { + voterSelect.innerHTML = ''; + voters.forEach(function(voter) { + var option = document.createElement('option'); + option.value = voter.id; + option.textContent = voter.name; + voterSelect.appendChild(option); + }); + } + } catch (e) { + console.error("Error parsing voters JSON:", e); } } }); @@ -418,4 +450,4 @@ } } -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/core/templates/core/log_door_visit.html b/core/templates/core/log_door_visit.html new file mode 100644 index 0000000..9e41558 --- /dev/null +++ b/core/templates/core/log_door_visit.html @@ -0,0 +1,150 @@ +{% extends "base.html" %} +{% load static %} + +{% block content %} +
+
+
+
+ + + +
+

Log Door Visit

+

Record interaction for this household

+
+
+ +
+
+
+
Household Address
+
{{ address_street }}
+
{{ city }}, {{ state }} {{ zip_code }}
+
+ +
+ {% csrf_token %} + + + + + + + +
+ +
+ {% for radio in visit_form.outcome %} +
+ {{ radio.tag }} + +
+ {% endfor %} +
+
+ +
+ + {{ visit_form.notes }} +
+ +
+
+ + {{ visit_form.candidate_support }} +
+
+
+ {{ visit_form.wants_yard_sign }} + +
+
+
+ +
+ +
+
+ {{ visit_form.follow_up }} + +
+
+
+ + {{ visit_form.follow_up_voter }} +
+
+ + {{ visit_form.call_notes }} +
These notes will be added to the call queue for the default caller.
+
+
+
+ +
+ Cancel + +
+
+
+
+ +
+
Targeted Voters at this Address
+ +
+
+
+
+ + + + +{% endblock %} diff --git a/core/views.py b/core/views.py index 693abe1..69c271f 100644 --- a/core/views.py +++ b/core/views.py @@ -1655,7 +1655,7 @@ def door_visits(request): { 'lat': h['latitude'], 'lng': h['longitude'], - 'address': f"{h['address_street']}, {h['city']}, {h['state']}", + 'address_street': h['address_street'], 'city': h['city'], 'state': h['state'], 'zip_code': h['zip_code'], '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'] @@ -1672,7 +1672,7 @@ def door_visits(request): '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', ''), + 'GOOGLE_MAPS_API_KEY': getattr(settings, 'GOOGLE_MAPS_API_KEY', ''), 'visit_form': DoorVisitLogForm(), } return render(request, 'core/door_visits.html', context) @@ -1682,6 +1682,7 @@ def log_door_visit(request): """ Mark all targeted voters at a specific address as visited, update their flags, and create interaction records. + Can also render a standalone page for logging a visit. """ selected_tenant_id = request.session.get("tenant_id") if not selected_tenant_id: @@ -1691,22 +1692,53 @@ def log_door_visit(request): 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", "") + next_qs = request.POST.get("next_query_string", request.GET.get("next_query_string", "")) + source = request.POST.get("source", request.GET.get("source", "")) + redirect_url = reverse("door_visits") + + # Build redirect URL + redirect_params = [] if next_qs: - redirect_url += f"?{next_qs}" + redirect_params.append(next_qs) + if source == "map": + redirect_params.append("open_map=1") + + if redirect_params: + redirect_url += "?" + "&".join(redirect_params) + + # Get address components from POST or GET + address_street = request.POST.get("address_street", request.GET.get("address_street")) + city = request.POST.get("city", request.GET.get("city")) + state = request.POST.get("state", request.GET.get("state")) + zip_code = request.POST.get("zip_code", request.GET.get("zip_code")) + + if not address_street: + messages.warning(request, "No address provided.") + return redirect(redirect_url) + + # 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() and request.method == "POST": + messages.warning(request, f"No targeted voters found at {address_street}.") + return redirect(redirect_url) + + voter_choices = [(v.id, f"{v.first_name} {v.last_name}") for v in voters] # 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) + form = DoorVisitLogForm(request.POST, voter_choices=voter_choices) 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"] @@ -1722,25 +1754,11 @@ def log_door_visit(request): except: tz = zoneinfo.ZoneInfo("America/Chicago") - interaction_date = timezone.now().astimezone(tz); + 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: @@ -1784,10 +1802,26 @@ def log_door_visit(request): 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}.") + return redirect(redirect_url) else: messages.error(request, "There was an error in the visit log form.") - - return redirect(redirect_url) + return redirect(redirect_url) + else: + # GET request: render standalone page + form = DoorVisitLogForm(voter_choices=voter_choices) + context = { + 'selected_tenant': tenant, + 'visit_form': form, + 'address_street': address_street, + 'city': city, + 'state': state, + 'zip_code': zip_code, + 'voters': voters, + 'next_query_string': next_qs, + 'source': source, + 'redirect_url': redirect_url, + } + return render(request, 'core/log_door_visit.html', context) def door_visit_history(request): """ diff --git a/core_views_log_visit.py b/core_views_log_visit.py new file mode 100644 index 0000000..33650da --- /dev/null +++ b/core_views_log_visit.py @@ -0,0 +1,144 @@ +def log_door_visit(request): + """ + Mark all targeted voters at a specific address as visited, update their flags, + and create interaction records. + Can also render a standalone page for logging a visit. + """ + 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", request.GET.get("next_query_string", "")) + source = request.POST.get("source", request.GET.get("source", "")) + + redirect_url = reverse("door_visits") + + # Build redirect URL + redirect_params = [] + if next_qs: + redirect_params.append(next_qs) + if source == "map": + redirect_params.append("open_map=1") + + if redirect_params: + redirect_url += "?" + "&".join(redirect_params) + + # Get address components from POST or GET + address_street = request.POST.get("address_street", request.GET.get("address_street")) + city = request.POST.get("city", request.GET.get("city")) + state = request.POST.get("state", request.GET.get("state")) + zip_code = request.POST.get("zip_code", request.GET.get("zip_code")) + + if not address_street: + messages.warning(request, "No address provided.") + return redirect(redirect_url) + + # 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() and request.method == "POST": + messages.warning(request, f"No targeted voters found at {address_street}.") + return redirect(redirect_url) + + voter_choices = [(v.id, f"{v.first_name} {v.last_name}") for v in voters] + + # 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, voter_choices=voter_choices) + if form.is_valid(): + 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") + + # 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}.") + return redirect(redirect_url) + else: + messages.error(request, "There was an error in the visit log form.") + return redirect(redirect_url) + else: + # GET request: render standalone page + form = DoorVisitLogForm(voter_choices=voter_choices) + context = { + 'selected_tenant': tenant, + 'visit_form': form, + 'address_street': address_street, + 'city': city, + 'state': state, + 'zip_code': zip_code, + 'voters': voters, + 'next_query_string': next_qs, + 'source': source, + 'redirect_url': redirect_url, + } + return render(request, 'core/log_door_visit.html', context) diff --git a/door_views_update.py b/door_views_update.py index bf21f0c..3cbd356 100644 --- a/door_views_update.py +++ b/door_views_update.py @@ -1,125 +1,152 @@ -import sys -import re +import os +import django -file_path = 'core/views.py' -with open(file_path, 'r') as f: - content = f.read() +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') +django.setup() -# Define the new function as a single string -new_func = """def door_visit_history(request): +from django.shortcuts import render, redirect, get_object_or_404 +from django.urls import reverse +from django.contrib import messages +from django.utils import timezone +from core.models import Tenant, CampaignSettings, Volunteer, Voter, InteractionType, Interaction, ScheduledCall +from core.forms import DoorVisitLogForm +import zoneinfo + +def log_door_visit(request): """ - Shows a distinct list of Door visit interactions for addresses. + Mark all targeted voters at a specific address as visited, update their flags, + and create interaction records. + Can also render a standalone page for logging a visit. """ 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) + campaign_settings, _ = CampaignSettings.objects.get_or_create(tenant=tenant) - # Date filter - start_date = request.GET.get("start_date") - end_date = request.GET.get("end_date") + # Capture query string for redirecting back with filters + next_qs = request.POST.get("next_query_string", request.GET.get("next_query_string", "")) + redirect_url = reverse("door_visits") + if next_qs: + redirect_url += f"?{next_qs}" - # Get all "Door Visit" interactions for this tenant - interactions = Interaction.objects.filter( - voter__tenant=tenant, - type__name="Door Visit" - ).select_related("voter", "volunteer") + # Get address components from POST or GET + address_street = request.POST.get("address_street", request.GET.get("address_street")) + city = request.POST.get("city", request.GET.get("city")) + state = request.POST.get("state", request.GET.get("state")) + zip_code = request.POST.get("zip_code", request.GET.get("zip_code")) - if start_date or end_date: - try: - if start_date: - d = parse_date(start_date) - if d: - start_dt = timezone.make_aware(datetime.combine(d, time.min)) - interactions = interactions.filter(date__gte=start_dt) - if end_date: - d = parse_date(end_date) - if d: - # Use lt with next day to capture everything on the end_date - end_dt = timezone.make_aware(datetime.combine(d + timedelta(days=1), time.min)) - interactions = interactions.filter(date__lt=end_dt) - except Exception as e: - logger.error(f"Error filtering door visit history by date: {e}") + if not address_street: + messages.warning(request, "No address provided.") + return redirect(redirect_url) - # Summary of counts per volunteer - # Grouping by household (unique address) - visited_households = {} - volunteer_counts = {} + # 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 + ) - for interaction in interactions.order_by("-date"): - v = interaction.voter - addr = v.address.strip() if v.address else f"{v.address_street}, {v.city}, {v.state} {v.zip_code}".strip(", ") - if not addr: - continue - - key = addr.lower() - - if key not in visited_households: - # Calculate volunteer summary - only once per household - v_obj = interaction.volunteer - v_name = f"{v_obj.first_name} {v_obj.last_name}".strip() or v_obj.email if v_obj else "N/A" - volunteer_counts[v_name] = volunteer_counts.get(v_name, 0) + 1 - - # Parse street name and number for sorting - street_number = "" - street_name = v.address_street or "" - match = re.match(r'^(\d+)\s+(.*)$', street_name) - if match: - street_number = match.group(1) - street_name = match.group(2) + if not voters.exists() and request.method == "POST": + messages.warning(request, f"No targeted voters found at {address_street}.") + return redirect(redirect_url) + + voter_choices = [(v.id, f"{v.first_name} {v.last_name}") for v in voters] + + # 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, voter_choices=voter_choices) + if form.is_valid(): + 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: - street_number_sort = int(street_number) - except ValueError: - street_number_sort = 0 + 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") + + # Get default caller for follow-ups + default_caller = None + if follow_up: + default_caller = Volunteer.objects.filter(tenant=tenant, is_default_caller=True).first() - visited_households[key] = { - 'address_display': addr, - 'address_street': v.address_street, - 'city': v.city, - 'state': v.state, - 'zip_code': v.zip_code, - 'neighborhood': v.neighborhood, - 'district': v.district, - 'latitude': float(v.latitude) if v.latitude else None, - 'longitude': float(v.longitude) if v.longitude else None, - 'street_name_sort': street_name.lower(), - 'street_number_sort': street_number_sort, - 'last_visit_date': interaction.date, - 'target_voters': [], - 'voters_json': [] - } - - visited_households[key]["voters_json"].append({'id': v.id, 'name': f"{v.first_name} {v.last_name}"}) - visited_households[key]['target_voters'].append(v) + 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 + ) - # Sort volunteer counts by total (descending) - sorted_volunteer_counts = sorted(volunteer_counts.items(), key=lambda x: x[1], reverse=True) + # 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}.") + return redirect(redirect_url) + else: + if request.headers.get('x-requested-with') == 'XMLHttpRequest': + # If it's the modal, we might want to handle it differently, + # but currently it's a standard POST from modal. + pass + messages.error(request, "There was an error in the visit log form.") + else: + # GET request: render standalone page + form = DoorVisitLogForm(voter_choices=voter_choices) + context = { + 'selected_tenant': tenant, + 'visit_form': form, + 'address_street': address_street, + 'city': city, + 'state': state, + 'zip_code': zip_code, + 'voters': voters, + 'next_query_string': next_qs, + } + return render(request, 'core/log_door_visit.html', context) + + return redirect(redirect_url) - history_list = list(visited_households.values()) - history_list.sort(key=lambda x: x["last_visit_date"], reverse=True) - - paginator = Paginator(history_list, 50) - page_number = request.GET.get("page") - history_page = paginator.get_page(page_number) - - context = { - "selected_tenant": tenant, - "history": history_page, - "start_date": start_date, "end_date": end_date, - "volunteer_counts": sorted_volunteer_counts, - } - return render(request, "core/door_visit_history.html", context) -""" - -# Use regex to find and replace the function -pattern = r'def door_visit_history\(request\):.*?return render\(request, "core/door_visit_history\.html", context\)' -new_content = re.sub(pattern, new_func, content, flags=re.DOTALL) - -if new_content != content: - with open(file_path, 'w') as f: - f.write(new_content) - print("Successfully updated door_visit_history") -else: - print("Could not find function to replace") \ No newline at end of file diff --git a/door_visit_patch.py b/door_visit_patch.py new file mode 100644 index 0000000..10ef5bd --- /dev/null +++ b/door_visit_patch.py @@ -0,0 +1,156 @@ +import os +import django + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') +django.setup() + +import re +from core import views + +def patch_log_door_visit(): + with open('core/views.py', 'r') as f: + content = f.read() + + new_view = """def log_door_visit(request): + \"\"\" + Mark all targeted voters at a specific address as visited, update their flags, + and create interaction records. + Can also render a standalone page for logging a visit. + \"\"\" + 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", request.GET.get("next_query_string", "")) + redirect_url = reverse("door_visits") + if next_qs: + redirect_url += f"?{next_qs}" + + # Get address components from POST or GET + address_street = request.POST.get("address_street", request.GET.get("address_street")) + city = request.POST.get("city", request.GET.get("city")) + state = request.POST.get("state", request.GET.get("state")) + zip_code = request.POST.get("zip_code", request.GET.get("zip_code")) + + if not address_street: + messages.warning(request, "No address provided.") + return redirect(redirect_url) + + # 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() and request.method == "POST": + messages.warning(request, f"No targeted voters found at {address_street}.") + return redirect(redirect_url) + + voter_choices = [(v.id, f"{v.first_name} {v.last_name}") for v in voters] + + # 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, voter_choices=voter_choices) + if form.is_valid(): + 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") + + # 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}.") + return redirect(redirect_url) + else: + messages.error(request, "There was an error in the visit log form.") + return redirect(redirect_url) + else: + # GET request: render standalone page + form = DoorVisitLogForm(voter_choices=voter_choices) + context = { + 'selected_tenant': tenant, + 'visit_form': form, + 'address_street': address_street, + 'city': city, + 'state': state, + 'zip_code': zip_code, + 'voters': voters, + 'next_query_string': next_qs, + } + return render(request, 'core/log_door_visit.html', context) +""" + + # Replace the old function. We use regex to find the start and then match indentation for the end. + pattern = r'def log_door_visit\(request\):.*?return redirect\(redirect_url\)' + content = re.sub(pattern, new_view, content, flags=re.DOTALL) + + with open('core/views.py', 'w') as f: + f.write(content) + +if __name__ == "__main__": + patch_log_door_visit() diff --git a/fix_views.py b/fix_views.py new file mode 100644 index 0000000..ac52e9e --- /dev/null +++ b/fix_views.py @@ -0,0 +1,22 @@ +import os + +with open('core/views.py', 'r') as f: + lines = f.readlines() + +new_lines = [] +skip = False +for i, line in enumerate(lines): + if "return render(request, 'core/log_door_visit.html', context)" in line: + new_lines.append(line) + # Look ahead to see if there is duplicated code + if i + 3 < len(lines) and "default_caller = None" in lines[i+3]: + skip = True + continue + if skip: + if "return redirect(redirect_url)" in line: + skip = False + continue + new_lines.append(line) + +with open('core/views.py', 'w') as f: + f.writelines(new_lines) diff --git a/fix_views_v2.py b/fix_views_v2.py new file mode 100644 index 0000000..1a2705b --- /dev/null +++ b/fix_views_v2.py @@ -0,0 +1,13 @@ +import os +import re + +with open('core/views.py', 'r') as f: + content = f.read() + +# Find the first occurrence of return render(request, 'core/log_door_visit.html', context) +# and the next def door_visit_history(request): +pattern = r"(return render\(request, 'core/log_door_visit.html', context\)).*?(def door_visit_history\(request\):)" +new_content = re.sub(pattern, r"\1\n\n\2", content, flags=re.DOTALL) + +with open('core/views.py', 'w') as f: + f.write(new_content) diff --git a/patch_log_visit.py b/patch_log_visit.py new file mode 100644 index 0000000..35fb9a8 --- /dev/null +++ b/patch_log_visit.py @@ -0,0 +1,33 @@ +import os + +with open('core/views.py', 'r') as f: + lines = f.readlines() + +start_line = -1 +end_line = -1 + +for i, line in enumerate(lines): + if 'def log_door_visit(request):' in line: + start_line = i + if start_line != -1 and 'def door_visit_history(request):' in line: + end_line = i + break + +if start_line != -1 and end_line != -1: + with open('core_views_log_visit.py', 'r') as f: + new_content = f.read() + + # Ensure there is a newline before door_visit_history + if not new_content.endswith('\n'): + new_content += '\n' + + # Prepend indentation to new_content if needed, but it should be top-level def + + lines[start_line:end_line] = [new_content + '\n'] + + with open('core/views.py', 'w') as f: + f.writelines(lines) + print("Successfully patched log_door_visit in core/views.py") +else: + print(f"Could not find log_door_visit boundaries: {start_line}, {end_line}") +