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