Autosave: 20260208-213524

This commit is contained in:
Flatlogic Bot 2026-02-08 21:35:24 +00:00
parent c92857d73b
commit 8de72675e5
12 changed files with 785 additions and 169 deletions

View File

@ -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,

View File

@ -78,8 +78,10 @@
</button>
</td>
<td>
<div class="fw-bold text-dark">{{ household.address_street }}</div>
<div class="small text-muted">{{ household.city }}</div>
<a href="{% url 'log_door_visit' %}?address_street={{ household.address_street|urlencode }}&city={{ household.city|urlencode }}&state={{ household.state|urlencode }}&zip_code={{ household.zip_code|urlencode }}&next_query_string={{ request.GET.urlencode|urlencode }}" class="text-decoration-none">
<div class="fw-bold text-dark hover-underline">{{ household.address_street }}</div>
<div class="small text-muted">{{ household.city }}</div>
</a>
<div class="d-md-none mt-1">
{% if household.neighborhood %}
<span class="badge border border-primary-subtle bg-primary-subtle text-primary fw-medium px-2 py-1 small">
@ -128,12 +130,12 @@
<ul class="pagination justify-content-center mb-0">
{% if households.has_previous %}
<li class="page-item">
<a class="page-link rounded-circle mx-1 border-0 bg-light text-dark" href="?page=1{% if district_filter %}&district={{ district_filter }}{% endif %}{% if neighborhood_filter %}&neighborhood={{ neighborhood_filter }}{% endif %}{% if address_filter %}&address={{ address_filter }}{% endif %}" aria-label="First">
<a class="page-link rounded-circle mx-1 border-0 bg-light text-dark" href="?page=1{% if district_filter %}&district={{ district_filter|urlencode }}{% endif %}{% if neighborhood_filter %}&neighborhood={{ neighborhood_filter|urlencode }}{% endif %}{% if address_filter %}&address={{ address_filter|urlencode }}{% endif %}" aria-label="First">
<i class="bi bi-chevron-double-left small"></i>
</a>
</li>
<li class="page-item">
<a class="page-link rounded-circle mx-1 border-0 bg-light text-dark" href="?page={{ households.previous_page_number }}{% if district_filter %}&district={{ district_filter }}{% endif %}{% if neighborhood_filter %}&neighborhood={{ neighborhood_filter }}{% endif %}{% if address_filter %}&address={{ address_filter }}{% endif %}" aria-label="Previous">
<a class="page-link rounded-circle mx-1 border-0 bg-light text-dark" href="?page={{ households.previous_page_number }}{% if district_filter %}&district={{ district_filter|urlencode }}{% endif %}{% if neighborhood_filter %}&neighborhood={{ neighborhood_filter|urlencode }}{% endif %}{% if address_filter %}&address={{ address_filter|urlencode }}{% endif %}" aria-label="Previous">
<i class="bi bi-chevron-left small"></i>
</a>
</li>
@ -143,12 +145,12 @@
{% if households.has_next %}
<li class="page-item">
<a class="page-link rounded-circle mx-1 border-0 bg-light text-dark" href="?page={{ households.next_page_number }}{% if district_filter %}&district={{ district_filter }}{% endif %}{% if neighborhood_filter %}&neighborhood={{ neighborhood_filter }}{% endif %}{% if address_filter %}&address={{ address_filter }}{% endif %}" aria-label="Next">
<a class="page-link rounded-circle mx-1 border-0 bg-light text-dark" href="?page={{ households.next_page_number }}{% if district_filter %}&district={{ district_filter|urlencode }}{% endif %}{% if neighborhood_filter %}&neighborhood={{ neighborhood_filter|urlencode }}{% endif %}{% if address_filter %}&address={{ address_filter|urlencode }}{% endif %}" aria-label="Next">
<i class="bi bi-chevron-right small"></i>
</a>
</li>
<li class="page-item">
<a class="page-link rounded-circle mx-1 border-0 bg-light text-dark" href="?page={{ households.paginator.num_pages }}{% if district_filter %}&district={{ district_filter }}{% endif %}{% if neighborhood_filter %}&neighborhood={{ neighborhood_filter }}{% endif %}{% if address_filter %}&address={{ address_filter }}{% endif %}" aria-label="Last">
<a class="page-link rounded-circle mx-1 border-0 bg-light text-dark" href="?page={{ households.paginator.num_pages }}{% if district_filter %}&district={{ district_filter|urlencode }}{% endif %}{% if neighborhood_filter %}&neighborhood={{ neighborhood_filter|urlencode }}{% endif %}{% if address_filter %}&address={{ address_filter|urlencode }}{% endif %}" aria-label="Last">
<i class="bi bi-chevron-double-right small"></i>
</a>
</li>
@ -263,8 +265,10 @@
</div>
</div>
</div>
{% endblock %}
<!-- Google Maps JS - Using global config matching voter page -->
{% block extra_js %}
<!-- Google Maps JS -->
{% if GOOGLE_MAPS_API_KEY %}
<script src="https://maps.googleapis.com/maps/api/js?key={{ GOOGLE_MAPS_API_KEY }}"></script>
{% endif %}
@ -302,7 +306,17 @@
});
marker.addListener('click', function() {
infowindow.setContent('<strong>' + item.address + '</strong><br>' + 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 = '<strong>' + item.address + '</strong><br>' +
item.voters + '<br><br>' +
'<a href="' + logUrl + '" class="btn btn-sm btn-primary text-white px-3 py-1">Log Visit</a>';
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 @@
}
}
</style>
{% endblock %}
{% endblock %}

View File

@ -0,0 +1,150 @@
{% extends "base.html" %}
{% load static %}
{% block content %}
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-12 col-lg-8">
<div class="d-flex align-items-center mb-4">
<a href="{{ redirect_url }}" class="btn btn-outline-secondary rounded-circle me-3">
<i class="bi bi-arrow-left"></i>
</a>
<div>
<h1 class="h3 fw-bold mb-0">Log Door Visit</h1>
<p class="text-muted mb-0">Record interaction for this household</p>
</div>
</div>
<div class="card border-0 shadow-sm rounded-4 overflow-hidden">
<div class="card-body p-4 p-md-5">
<div class="bg-primary-subtle p-4 rounded-3 mb-4 border border-primary-subtle">
<div class="small text-uppercase fw-bold text-primary mb-1">Household Address</div>
<div class="h4 mb-0 fw-bold text-dark">{{ address_street }}</div>
<div class="text-muted">{{ city }}, {{ state }} {{ zip_code }}</div>
</div>
<form action="{% url 'log_door_visit' %}" method="POST">
{% csrf_token %}
<input type="hidden" name="address_street" value="{{ address_street }}">
<input type="hidden" name="city" value="{{ city }}">
<input type="hidden" name="state" value="{{ state }}">
<input type="hidden" name="zip_code" value="{{ zip_code }}">
<input type="hidden" name="next_query_string" value="{{ next_query_string }}">
<input type="hidden" name="source" value="{{ source }}">
<div class="mb-4">
<label class="form-label fw-bold text-primary small text-uppercase">Visit Outcome</label>
<div class="row g-2">
{% for radio in visit_form.outcome %}
<div class="col-12 col-md-4">
{{ radio.tag }}
<label class="btn btn-outline-primary w-100 h-100 d-flex align-items-center justify-content-center text-center py-3 px-2" for="{{ radio.id_for_label }}">
{{ radio.choice_label }}
</label>
</div>
{% endfor %}
</div>
</div>
<div class="mb-4">
<label for="{{ visit_form.notes.id_for_label }}" class="form-label fw-bold text-primary small text-uppercase">Notes / Conversation Summary</label>
{{ visit_form.notes }}
</div>
<div class="row g-3 mb-4">
<div class="col-12 col-md-6">
<label class="form-label fw-bold text-primary small text-uppercase">Support Status</label>
{{ visit_form.candidate_support }}
</div>
<div class="col-12 col-md-6 d-flex align-items-end">
<div class="form-check mb-2">
{{ visit_form.wants_yard_sign }}
<label class="form-check-label fw-bold text-dark" for="{{ visit_form.wants_yard_sign.id_for_label }}">
Wants a Yard Sign
</label>
</div>
</div>
</div>
<hr class="my-5">
<div class="bg-light p-4 rounded-4">
<div class="form-check mb-4">
{{ visit_form.follow_up }}
<label class="form-check-label h6 fw-bold text-dark mb-0" for="{{ visit_form.follow_up.id_for_label }}">
Schedule a Follow-up Call
</label>
</div>
<div id="callNotesContainer" {% if not visit_form.follow_up.value %}style="display: none;"{% endif %}>
<div class="mb-3">
<label for="{{ visit_form.follow_up_voter.id_for_label }}" class="form-label fw-bold text-primary small text-uppercase">Recipient of the Call</label>
{{ visit_form.follow_up_voter }}
</div>
<div class="mb-0">
<label for="{{ visit_form.call_notes.id_for_label }}" class="form-label fw-bold text-primary small text-uppercase">Call Queue Notes</label>
{{ visit_form.call_notes }}
<div class="form-text small">These notes will be added to the call queue for the default caller.</div>
</div>
</div>
</div>
<div class="mt-5 d-grid d-md-flex justify-content-md-end gap-2">
<a href="{{ redirect_url }}" class="btn btn-light px-5 py-3 rounded-3 fw-bold">Cancel</a>
<button type="submit" class="btn btn-primary px-5 py-3 rounded-3 fw-bold shadow">Save Visit</button>
</div>
</form>
</div>
</div>
<div class="mt-4">
<h5 class="fw-bold text-dark mb-3">Targeted Voters at this Address</h5>
<div class="list-group list-group-flush shadow-sm rounded-4 overflow-hidden">
{% for voter in voters %}
<a href="{% url 'voter_detail' voter.id %}" class="list-group-item list-group-item-action py-3 border-0">
<div class="d-flex justify-content-between align-items-center">
<div>
<div class="fw-bold text-primary">{{ voter.first_name }} {{ voter.last_name }}</div>
<div class="small text-muted">Voter ID: {{ voter.voter_id|default:"N/A" }}</div>
</div>
<i class="bi bi-chevron-right text-muted"></i>
</div>
</a>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const followUpCheckbox = document.getElementById('{{ visit_form.follow_up.id_for_label }}');
const callNotesContainer = document.getElementById('callNotesContainer');
if (followUpCheckbox && callNotesContainer) {
followUpCheckbox.addEventListener('change', function() {
callNotesContainer.style.display = this.checked ? 'block' : 'none';
});
}
});
</script>
<style>
.btn-outline-primary {
color: #0d6efd !important;
border-color: #0d6efd !important;
}
.btn-outline-primary:hover, .btn-check:checked + .btn-outline-primary {
background-color: #0d6efd !important;
color: #ffffff !important;
}
.bg-primary-subtle {
background-color: #e7f1ff;
}
.text-primary {
color: #0d6efd !important;
}
.border-primary-subtle {
border-color: #9ec5fe !important;
}
</style>
{% endblock %}

View File

@ -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):
"""

144
core_views_log_visit.py Normal file
View File

@ -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)

View File

@ -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")

156
door_visit_patch.py Normal file
View File

@ -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()

22
fix_views.py Normal file
View File

@ -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)

13
fix_views_v2.py Normal file
View File

@ -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)

33
patch_log_visit.py Normal file
View File

@ -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}")