diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc index dea8a66..cd29007 100644 Binary files a/core/__pycache__/forms.cpython-311.pyc and b/core/__pycache__/forms.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index eebca54..912ac55 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/forms.py b/core/forms.py index 9286565..8363c6a 100644 --- a/core/forms.py +++ b/core/forms.py @@ -456,7 +456,12 @@ class DoorVisitLogForm(forms.Form): widget=forms.CheckboxInput(attrs={"class": "form-check-input"}), label="Follow Up" ) - follow_up_voter = forms.CharField( required=False, widget=forms.Select(attrs={"class": "form-select"}), label="Voter to Follow Up") + follow_up_voter = forms.ChoiceField(choices=[], required=False, widget=forms.Select(attrs={"class": "form-select"}), label="Voter to Follow Up") + + def __init__(self, *args, voter_choices=None, **kwargs): + super().__init__(*args, **kwargs) + if voter_choices: + self.fields["follow_up_voter"].choices = voter_choices call_notes = forms.CharField( widget=forms.Textarea(attrs={"class": "form-control", "rows": 2}), required=False, diff --git a/core/templates/core/door_visits.html b/core/templates/core/door_visits.html index 8d139ce..cb75bc2 100644 --- a/core/templates/core/door_visits.html +++ b/core/templates/core/door_visits.html @@ -78,8 +78,10 @@ -
{{ household.address_street }}
-
{{ household.city }}
+ +
{{ household.address_street }}
+
{{ household.city }}
+
{% if household.neighborhood %} @@ -128,12 +130,12 @@
+{% 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}") +