Autosave: 20260204-221954
This commit is contained in:
parent
bf2d558e03
commit
e0f6e045f3
Binary file not shown.
@ -103,18 +103,18 @@
|
||||
</span>
|
||||
{% endif %}
|
||||
<div class="d-md-none small text-muted w-100">
|
||||
{% with voters=household.voters_at_address %}
|
||||
{{ voters|join:", " }}
|
||||
{% endwith %}
|
||||
{% for v_id, v_name in household.voters_at_address %}
|
||||
<a href="{% url 'voter_detail' v_id %}" class="text-decoration-none text-muted fw-medium">{{ v_name }}</a>{% if not forloop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="d-none d-md-table-cell">
|
||||
<div class="d-flex flex-wrap gap-1">
|
||||
{% for voter_name in household.voters_at_address %}
|
||||
<span class="badge bg-light text-dark border fw-normal">
|
||||
{{ voter_name }}
|
||||
</span>
|
||||
{% for v_id, v_name in household.voters_at_address %}
|
||||
<a href="{% url 'voter_detail' v_id %}" class="badge bg-light text-dark border fw-normal text-decoration-none voter-badge">
|
||||
{{ v_name }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</td>
|
||||
@ -214,6 +214,11 @@
|
||||
.text-info {
|
||||
color: #055160 !important;
|
||||
}
|
||||
.voter-badge:hover {
|
||||
background-color: #e9ecef !important;
|
||||
border-color: #adb5bd !important;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
@media (max-width: 576px) {
|
||||
.container-fluid {
|
||||
padding-left: 0.75rem !important;
|
||||
|
||||
@ -6,9 +6,6 @@
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h2">Advanced Voter Search</h1>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" data-bs-toggle="modal" data-bs-target="#mapModal">
|
||||
<i class="bi bi-map-fill me-1"></i> View Map
|
||||
</button>
|
||||
<a href="{% url 'voter_list' %}" class="btn btn-outline-secondary btn-sm">Back to Registry</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -6,9 +6,6 @@
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h2">Voter Registry</h1>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" data-bs-toggle="modal" data-bs-target="#mapModal">
|
||||
<i class="bi bi-map-fill me-1"></i> View Map
|
||||
</button>
|
||||
<a href="{% url 'voter_advanced_search' %}" class="btn btn-outline-primary btn-sm">Advanced Search</a>
|
||||
{% if can_edit_voter %}
|
||||
<a href="/admin/core/voter/add/" class="btn btn-primary btn-sm">+ Add New Voter</a>
|
||||
|
||||
@ -1487,7 +1487,7 @@ def door_visit_history(request):
|
||||
"latest_interaction": interaction
|
||||
}
|
||||
|
||||
visited_households[key]["voters_at_address"].add(f"{v.first_name} {v.last_name}")
|
||||
visited_households[key]["voters_at_address"].add((v.id, f"{v.first_name} {v.last_name}"))
|
||||
|
||||
# Sort volunteer counts by total (descending)
|
||||
sorted_volunteer_counts = sorted(volunteer_counts.items(), key=lambda x: x[1], reverse=True)
|
||||
|
||||
@ -1,211 +1,16 @@
|
||||
def door_visits(request):
|
||||
"""
|
||||
Manage door knocking visits. Groups unvisited targeted voters by household.
|
||||
"""
|
||||
selected_tenant_id = request.session.get("tenant_id")
|
||||
if not selected_tenant_id:
|
||||
messages.warning(request, "Please select a campaign first.")
|
||||
return redirect("index")
|
||||
import sys
|
||||
|
||||
tenant = get_object_or_404(Tenant, id=selected_tenant_id)
|
||||
|
||||
# Filters from GET parameters
|
||||
district_filter = request.GET.get('district', '').strip()
|
||||
neighborhood_filter = request.GET.get('neighborhood', '').strip()
|
||||
address_filter = request.GET.get('address', '').strip()
|
||||
file_path = 'core/views.py'
|
||||
with open(file_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Initial queryset: unvisited targeted voters for this tenant
|
||||
voters = Voter.objects.filter(tenant=tenant, door_visit=False, is_targeted=True)
|
||||
old_code = 'visited_households[key]["voters_at_address"].add(f"{v.first_name} {v.last_name}")'
|
||||
new_code = 'visited_households[key]["voters_at_address"].add((v.id, f"{v.first_name} {v.last_name}"))'
|
||||
|
||||
# Apply filters if provided
|
||||
if district_filter:
|
||||
voters = voters.filter(district=district_filter)
|
||||
if neighborhood_filter:
|
||||
voters = voters.filter(neighborhood__icontains=neighborhood_filter)
|
||||
if address_filter:
|
||||
voters = voters.filter(Q(address_street__icontains=address_filter) | Q(address__icontains=address_filter))
|
||||
|
||||
# Grouping by household (unique address)
|
||||
households_dict = {}
|
||||
for voter in voters:
|
||||
# Key for grouping is the unique address components
|
||||
key = (voter.address_street, voter.city, voter.state, voter.zip_code)
|
||||
if key not in households_dict:
|
||||
# Parse street name and number for sorting
|
||||
street_number = ""
|
||||
street_name = voter.address_street
|
||||
match = re.match(r'^(\d+)\s+(.*)$', voter.address_street)
|
||||
if match:
|
||||
street_number = match.group(1)
|
||||
street_name = match.group(2)
|
||||
|
||||
try:
|
||||
street_number_sort = int(street_number)
|
||||
except ValueError:
|
||||
street_number_sort = 0
|
||||
|
||||
households_dict[key] = {
|
||||
'address_street': voter.address_street,
|
||||
'city': voter.city,
|
||||
'state': voter.state,
|
||||
'zip_code': voter.zip_code,
|
||||
'neighborhood': voter.neighborhood,
|
||||
'district': voter.district,
|
||||
'latitude': float(voter.latitude) if voter.latitude else None,
|
||||
'longitude': float(voter.longitude) if voter.longitude else None,
|
||||
'street_name_sort': street_name.lower(),
|
||||
'street_number_sort': street_number_sort,
|
||||
'target_voters': [],
|
||||
'voters_json': []
|
||||
}
|
||||
households_dict[key]['target_voters'].append(voter)
|
||||
households_dict[key]['voters_json'].append({'id': voter.id, 'name': f"{voter.first_name} {voter.last_name}"})
|
||||
|
||||
households_list = list(households_dict.values())
|
||||
for h in households_list:
|
||||
h['voters_json_str'] = json.dumps(h['voters_json'])
|
||||
|
||||
households_list.sort(key=lambda x: (
|
||||
(x['neighborhood'] or '').lower(),
|
||||
x['street_name_sort'],
|
||||
x['street_number_sort']
|
||||
))
|
||||
|
||||
# Prepare data for Google Map (all filtered households with coordinates)
|
||||
map_data = [
|
||||
{
|
||||
'lat': h['latitude'],
|
||||
'lng': h['longitude'],
|
||||
'address': f"{h['address_street']}, {h['city']}, {h['state']}",
|
||||
'voters': ", ".join([f"{v.first_name} {v.last_name}" for v in h['target_voters']])
|
||||
}
|
||||
for h in households_list if h['latitude'] and h['longitude']
|
||||
]
|
||||
|
||||
paginator = Paginator(households_list, 50)
|
||||
page_number = request.GET.get('page')
|
||||
households_page = paginator.get_page(page_number)
|
||||
|
||||
context = {
|
||||
'selected_tenant': tenant,
|
||||
'households': households_page,
|
||||
'district_filter': district_filter,
|
||||
'neighborhood_filter': neighborhood_filter,
|
||||
'address_filter': address_filter,
|
||||
'map_data_json': json.dumps(map_data),
|
||||
'google_maps_api_key': getattr(settings, 'GOOGLE_MAPS_API_KEY', ''),
|
||||
'visit_form': DoorVisitLogForm(),
|
||||
}
|
||||
return render(request, 'core/door_visits.html', context)
|
||||
|
||||
@role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin'], permission='core.view_voter')
|
||||
def log_door_visit(request):
|
||||
"""
|
||||
Mark all targeted voters at a specific address as visited, update their flags,
|
||||
and create interaction records.
|
||||
"""
|
||||
selected_tenant_id = request.session.get("tenant_id")
|
||||
if not selected_tenant_id:
|
||||
return redirect("index")
|
||||
|
||||
tenant = get_object_or_404(Tenant, id=selected_tenant_id)
|
||||
campaign_settings, _ = CampaignSettings.objects.get_or_create(tenant=tenant)
|
||||
|
||||
# Capture query string for redirecting back with filters
|
||||
next_qs = request.POST.get("next_query_string", "")
|
||||
redirect_url = reverse("door_visits")
|
||||
if next_qs:
|
||||
redirect_url += f"?{next_qs}"
|
||||
|
||||
# Get the volunteer linked to the current user
|
||||
volunteer = Volunteer.objects.filter(user=request.user, tenant=tenant).first()
|
||||
|
||||
if request.method == "POST":
|
||||
form = DoorVisitLogForm(request.POST)
|
||||
if form.is_valid():
|
||||
address_street = request.POST.get("address_street")
|
||||
city = request.POST.get("city")
|
||||
state = request.POST.get("state")
|
||||
zip_code = request.POST.get("zip_code")
|
||||
|
||||
outcome = form.cleaned_data["outcome"]
|
||||
notes = form.cleaned_data["notes"]
|
||||
wants_yard_sign = form.cleaned_data["wants_yard_sign"]
|
||||
candidate_support = form.cleaned_data["candidate_support"]
|
||||
follow_up = form.cleaned_data["follow_up"]
|
||||
follow_up_voter_id = form.cleaned_data.get("follow_up_voter")
|
||||
call_notes = form.cleaned_data["call_notes"]
|
||||
|
||||
# Determine date/time in campaign timezone
|
||||
campaign_tz_name = campaign_settings.timezone or "America/Chicago"
|
||||
try:
|
||||
tz = zoneinfo.ZoneInfo(campaign_tz_name)
|
||||
except:
|
||||
tz = zoneinfo.ZoneInfo("America/Chicago")
|
||||
|
||||
interaction_date = timezone.now().astimezone(tz)
|
||||
|
||||
# Get or create InteractionType
|
||||
interaction_type, _ = InteractionType.objects.get_or_create(tenant=tenant, name="Door Visit")
|
||||
|
||||
# Find targeted voters at this exact address
|
||||
voters = Voter.objects.filter(
|
||||
tenant=tenant,
|
||||
address_street=address_street,
|
||||
city=city,
|
||||
state=state,
|
||||
zip_code=zip_code,
|
||||
is_targeted=True
|
||||
)
|
||||
|
||||
if not voters.exists():
|
||||
messages.warning(request, f"No targeted voters found at {address_street}.")
|
||||
return redirect(redirect_url)
|
||||
|
||||
# Get default caller for follow-ups
|
||||
default_caller = None
|
||||
if follow_up:
|
||||
default_caller = Volunteer.objects.filter(tenant=tenant, is_default_caller=True).first()
|
||||
|
||||
for voter in voters:
|
||||
# 1) Update voter flags
|
||||
voter.door_visit = True
|
||||
|
||||
# 2) If "Wants a Yard Sign" checkbox is selected
|
||||
if wants_yard_sign:
|
||||
voter.yard_sign = "wants"
|
||||
|
||||
# 3) Update support status if Supporting or Not Supporting
|
||||
if candidate_support in ["supporting", "not_supporting"]:
|
||||
voter.candidate_support = candidate_support
|
||||
|
||||
voter.save()
|
||||
|
||||
# 4) Create interaction
|
||||
Interaction.objects.create(
|
||||
voter=voter,
|
||||
volunteer=volunteer,
|
||||
type=interaction_type,
|
||||
date=interaction_date,
|
||||
description=outcome,
|
||||
notes=notes
|
||||
)
|
||||
|
||||
# 5) Create ScheduledCall if follow_up is checked and this is the selected voter
|
||||
if follow_up and follow_up_voter_id and str(voter.id) == follow_up_voter_id:
|
||||
ScheduledCall.objects.create(
|
||||
tenant=tenant,
|
||||
voter=voter,
|
||||
volunteer=default_caller,
|
||||
comments=call_notes,
|
||||
status="pending"
|
||||
)
|
||||
|
||||
if follow_up:
|
||||
messages.success(request, f"Door visit logged and follow-up call scheduled for {address_street}.")
|
||||
else:
|
||||
messages.success(request, f"Door visit logged for {address_street}.")
|
||||
else:
|
||||
messages.error(request, "There was an error in the visit log form.")
|
||||
|
||||
return redirect(redirect_url)
|
||||
if old_code in content:
|
||||
new_content = content.replace(old_code, new_code)
|
||||
with open(file_path, 'w') as f:
|
||||
f.write(new_content)
|
||||
print("Successfully patched core/views.py")
|
||||
else:
|
||||
print("Could not find the target line in core/views.py")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user