Autosave: 20260208-213524
This commit is contained in:
parent
c92857d73b
commit
8de72675e5
Binary file not shown.
Binary file not shown.
@ -456,7 +456,12 @@ class DoorVisitLogForm(forms.Form):
|
|||||||
widget=forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
widget=forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
||||||
label="Follow Up"
|
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(
|
call_notes = forms.CharField(
|
||||||
widget=forms.Textarea(attrs={"class": "form-control", "rows": 2}),
|
widget=forms.Textarea(attrs={"class": "form-control", "rows": 2}),
|
||||||
required=False,
|
required=False,
|
||||||
|
|||||||
@ -78,8 +78,10 @@
|
|||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="fw-bold text-dark">{{ household.address_street }}</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>
|
<div class="small text-muted">{{ household.city }}</div>
|
||||||
|
</a>
|
||||||
<div class="d-md-none mt-1">
|
<div class="d-md-none mt-1">
|
||||||
{% if household.neighborhood %}
|
{% if household.neighborhood %}
|
||||||
<span class="badge border border-primary-subtle bg-primary-subtle text-primary fw-medium px-2 py-1 small">
|
<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">
|
<ul class="pagination justify-content-center mb-0">
|
||||||
{% if households.has_previous %}
|
{% if households.has_previous %}
|
||||||
<li class="page-item">
|
<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>
|
<i class="bi bi-chevron-double-left small"></i>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="page-item">
|
<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>
|
<i class="bi bi-chevron-left small"></i>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@ -143,12 +145,12 @@
|
|||||||
|
|
||||||
{% if households.has_next %}
|
{% if households.has_next %}
|
||||||
<li class="page-item">
|
<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>
|
<i class="bi bi-chevron-right small"></i>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="page-item">
|
<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>
|
<i class="bi bi-chevron-double-right small"></i>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@ -263,8 +265,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
<!-- Google Maps JS - Using global config matching voter page -->
|
{% block extra_js %}
|
||||||
|
<!-- Google Maps JS -->
|
||||||
{% if GOOGLE_MAPS_API_KEY %}
|
{% if GOOGLE_MAPS_API_KEY %}
|
||||||
<script src="https://maps.googleapis.com/maps/api/js?key={{ GOOGLE_MAPS_API_KEY }}"></script>
|
<script src="https://maps.googleapis.com/maps/api/js?key={{ GOOGLE_MAPS_API_KEY }}"></script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -302,7 +306,17 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
marker.addListener('click', function() {
|
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);
|
infowindow.open(map, marker);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -317,13 +331,24 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
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
|
// Trigger map initialization when modal is shown
|
||||||
var mapModal = document.getElementById('mapModal');
|
var mapModalElement = document.getElementById('mapModal');
|
||||||
if (mapModal) {
|
if (mapModalElement) {
|
||||||
mapModal.addEventListener('shown.bs.modal', function () {
|
mapModalElement.addEventListener('shown.bs.modal', function () {
|
||||||
if (!map) {
|
if (!map) {
|
||||||
initMap();
|
initMap();
|
||||||
} else {
|
} else {
|
||||||
|
if (window.google && window.google.maps) {
|
||||||
google.maps.event.trigger(map, 'resize');
|
google.maps.event.trigger(map, 'resize');
|
||||||
if (markers.length > 0) {
|
if (markers.length > 0) {
|
||||||
var bounds = new google.maps.LatLngBounds();
|
var bounds = new google.maps.LatLngBounds();
|
||||||
@ -333,6 +358,7 @@
|
|||||||
map.fitBounds(bounds);
|
map.fitBounds(bounds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -340,20 +366,23 @@
|
|||||||
if (logVisitModal) {
|
if (logVisitModal) {
|
||||||
logVisitModal.addEventListener('show.bs.modal', function (event) {
|
logVisitModal.addEventListener('show.bs.modal', function (event) {
|
||||||
var button = event.relatedTarget;
|
var button = event.relatedTarget;
|
||||||
|
if (!button) return;
|
||||||
|
|
||||||
var address = button.getAttribute('data-address');
|
var address = button.getAttribute('data-address');
|
||||||
var city = button.getAttribute('data-city');
|
var city = button.getAttribute('data-city');
|
||||||
var state = button.getAttribute('data-state');
|
var state = button.getAttribute('data-state');
|
||||||
var zip = button.getAttribute('data-zip');
|
var zip = button.getAttribute('data-zip');
|
||||||
|
|
||||||
document.getElementById('modalAddressDisplay').textContent = address + ', ' + city + ', ' + state + ' ' + zip;
|
document.getElementById('modalAddressDisplay').textContent = (address || '') + ', ' + (city || '') + ', ' + (state || '') + ' ' + (zip || '');
|
||||||
document.getElementById('modal_address_street').value = address;
|
document.getElementById('modal_address_street').value = address || '';
|
||||||
document.getElementById('modal_city').value = city;
|
document.getElementById('modal_city').value = city || '';
|
||||||
document.getElementById('modal_state').value = state;
|
document.getElementById('modal_state').value = state || '';
|
||||||
document.getElementById('modal_zip_code').value = zip;
|
document.getElementById('modal_zip_code').value = zip || '';
|
||||||
|
|
||||||
// Populate voters dropdown
|
// Populate voters dropdown
|
||||||
var votersJson = button.getAttribute('data-voters');
|
var votersJson = button.getAttribute('data-voters');
|
||||||
if (votersJson) {
|
if (votersJson) {
|
||||||
|
try {
|
||||||
var voters = JSON.parse(votersJson);
|
var voters = JSON.parse(votersJson);
|
||||||
var voterSelect = document.getElementById('{{ visit_form.follow_up_voter.id_for_label }}');
|
var voterSelect = document.getElementById('{{ visit_form.follow_up_voter.id_for_label }}');
|
||||||
if (voterSelect) {
|
if (voterSelect) {
|
||||||
@ -365,6 +394,9 @@
|
|||||||
voterSelect.appendChild(option);
|
voterSelect.appendChild(option);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error parsing voters JSON:", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
150
core/templates/core/log_door_visit.html
Normal file
150
core/templates/core/log_door_visit.html
Normal 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 %}
|
||||||
@ -1655,7 +1655,7 @@ def door_visits(request):
|
|||||||
{
|
{
|
||||||
'lat': h['latitude'],
|
'lat': h['latitude'],
|
||||||
'lng': h['longitude'],
|
'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']])
|
'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']
|
for h in households_list if h['latitude'] and h['longitude']
|
||||||
@ -1672,7 +1672,7 @@ def door_visits(request):
|
|||||||
'neighborhood_filter': neighborhood_filter,
|
'neighborhood_filter': neighborhood_filter,
|
||||||
'address_filter': address_filter,
|
'address_filter': address_filter,
|
||||||
'map_data_json': json.dumps(map_data),
|
'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(),
|
'visit_form': DoorVisitLogForm(),
|
||||||
}
|
}
|
||||||
return render(request, 'core/door_visits.html', context)
|
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,
|
Mark all targeted voters at a specific address as visited, update their flags,
|
||||||
and create interaction records.
|
and create interaction records.
|
||||||
|
Can also render a standalone page for logging a visit.
|
||||||
"""
|
"""
|
||||||
selected_tenant_id = request.session.get("tenant_id")
|
selected_tenant_id = request.session.get("tenant_id")
|
||||||
if not selected_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)
|
campaign_settings, _ = CampaignSettings.objects.get_or_create(tenant=tenant)
|
||||||
|
|
||||||
# Capture query string for redirecting back with filters
|
# 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")
|
redirect_url = reverse("door_visits")
|
||||||
|
|
||||||
|
# Build redirect URL
|
||||||
|
redirect_params = []
|
||||||
if next_qs:
|
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
|
# Get the volunteer linked to the current user
|
||||||
volunteer = Volunteer.objects.filter(user=request.user, tenant=tenant).first()
|
volunteer = Volunteer.objects.filter(user=request.user, tenant=tenant).first()
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = DoorVisitLogForm(request.POST)
|
form = DoorVisitLogForm(request.POST, voter_choices=voter_choices)
|
||||||
if form.is_valid():
|
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"]
|
outcome = form.cleaned_data["outcome"]
|
||||||
notes = form.cleaned_data["notes"]
|
notes = form.cleaned_data["notes"]
|
||||||
wants_yard_sign = form.cleaned_data["wants_yard_sign"]
|
wants_yard_sign = form.cleaned_data["wants_yard_sign"]
|
||||||
@ -1722,25 +1754,11 @@ def log_door_visit(request):
|
|||||||
except:
|
except:
|
||||||
tz = zoneinfo.ZoneInfo("America/Chicago")
|
tz = zoneinfo.ZoneInfo("America/Chicago")
|
||||||
|
|
||||||
interaction_date = timezone.now().astimezone(tz);
|
interaction_date = timezone.now().astimezone(tz)
|
||||||
|
|
||||||
# Get or create InteractionType
|
# Get or create InteractionType
|
||||||
interaction_type, _ = InteractionType.objects.get_or_create(tenant=tenant, name="Door Visit")
|
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
|
# Get default caller for follow-ups
|
||||||
default_caller = None
|
default_caller = None
|
||||||
if follow_up:
|
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}.")
|
messages.success(request, f"Door visit logged and follow-up call scheduled for {address_street}.")
|
||||||
else:
|
else:
|
||||||
messages.success(request, f"Door visit logged for {address_street}.")
|
messages.success(request, f"Door visit logged for {address_street}.")
|
||||||
|
return redirect(redirect_url)
|
||||||
else:
|
else:
|
||||||
messages.error(request, "There was an error in the visit log form.")
|
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):
|
def door_visit_history(request):
|
||||||
"""
|
"""
|
||||||
|
|||||||
144
core_views_log_visit.py
Normal file
144
core_views_log_visit.py
Normal 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)
|
||||||
@ -1,125 +1,152 @@
|
|||||||
import sys
|
import os
|
||||||
import re
|
import django
|
||||||
|
|
||||||
file_path = 'core/views.py'
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
with open(file_path, 'r') as f:
|
django.setup()
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
# Define the new function as a single string
|
from django.shortcuts import render, redirect, get_object_or_404
|
||||||
new_func = """def door_visit_history(request):
|
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")
|
selected_tenant_id = request.session.get("tenant_id")
|
||||||
if not selected_tenant_id:
|
if not selected_tenant_id:
|
||||||
messages.warning(request, "Please select a campaign first.")
|
|
||||||
return redirect("index")
|
return redirect("index")
|
||||||
|
|
||||||
tenant = get_object_or_404(Tenant, id=selected_tenant_id)
|
tenant = get_object_or_404(Tenant, id=selected_tenant_id)
|
||||||
|
campaign_settings, _ = CampaignSettings.objects.get_or_create(tenant=tenant)
|
||||||
|
|
||||||
# Date filter
|
# Capture query string for redirecting back with filters
|
||||||
start_date = request.GET.get("start_date")
|
next_qs = request.POST.get("next_query_string", request.GET.get("next_query_string", ""))
|
||||||
end_date = request.GET.get("end_date")
|
redirect_url = reverse("door_visits")
|
||||||
|
if next_qs:
|
||||||
|
redirect_url += f"?{next_qs}"
|
||||||
|
|
||||||
# Get all "Door Visit" interactions for this tenant
|
# Get address components from POST or GET
|
||||||
interactions = Interaction.objects.filter(
|
address_street = request.POST.get("address_street", request.GET.get("address_street"))
|
||||||
voter__tenant=tenant,
|
city = request.POST.get("city", request.GET.get("city"))
|
||||||
type__name="Door Visit"
|
state = request.POST.get("state", request.GET.get("state"))
|
||||||
).select_related("voter", "volunteer")
|
zip_code = request.POST.get("zip_code", request.GET.get("zip_code"))
|
||||||
|
|
||||||
if start_date or end_date:
|
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:
|
try:
|
||||||
if start_date:
|
tz = zoneinfo.ZoneInfo(campaign_tz_name)
|
||||||
d = parse_date(start_date)
|
except:
|
||||||
if d:
|
tz = zoneinfo.ZoneInfo("America/Chicago")
|
||||||
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}")
|
|
||||||
|
|
||||||
# Summary of counts per volunteer
|
interaction_date = timezone.now().astimezone(tz)
|
||||||
# Grouping by household (unique address)
|
|
||||||
visited_households = {}
|
|
||||||
volunteer_counts = {}
|
|
||||||
|
|
||||||
for interaction in interactions.order_by("-date"):
|
# Get or create InteractionType
|
||||||
v = interaction.voter
|
interaction_type, _ = InteractionType.objects.get_or_create(tenant=tenant, name="Door Visit")
|
||||||
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()
|
# Get default caller for follow-ups
|
||||||
|
default_caller = None
|
||||||
|
if follow_up:
|
||||||
|
default_caller = Volunteer.objects.filter(tenant=tenant, is_default_caller=True).first()
|
||||||
|
|
||||||
if key not in visited_households:
|
for voter in voters:
|
||||||
# Calculate volunteer summary - only once per household
|
# 1) Update voter flags
|
||||||
v_obj = interaction.volunteer
|
voter.door_visit = True
|
||||||
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
|
# 2) If "Wants a Yard Sign" checkbox is selected
|
||||||
street_number = ""
|
if wants_yard_sign:
|
||||||
street_name = v.address_street or ""
|
voter.yard_sign = "wants"
|
||||||
match = re.match(r'^(\d+)\s+(.*)$', street_name)
|
|
||||||
if match:
|
|
||||||
street_number = match.group(1)
|
|
||||||
street_name = match.group(2)
|
|
||||||
|
|
||||||
try:
|
# 3) Update support status if Supporting or Not Supporting
|
||||||
street_number_sort = int(street_number)
|
if candidate_support in ["supporting", "not_supporting"]:
|
||||||
except ValueError:
|
voter.candidate_support = candidate_support
|
||||||
street_number_sort = 0
|
|
||||||
|
|
||||||
visited_households[key] = {
|
voter.save()
|
||||||
'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}"})
|
# 4) Create interaction
|
||||||
visited_households[key]['target_voters'].append(v)
|
Interaction.objects.create(
|
||||||
|
voter=voter,
|
||||||
|
volunteer=volunteer,
|
||||||
|
type=interaction_type,
|
||||||
|
date=interaction_date,
|
||||||
|
description=outcome,
|
||||||
|
notes=notes
|
||||||
|
)
|
||||||
|
|
||||||
# Sort volunteer counts by total (descending)
|
# 5) Create ScheduledCall if follow_up is checked and this is the selected voter
|
||||||
sorted_volunteer_counts = sorted(volunteer_counts.items(), key=lambda x: x[1], reverse=True)
|
if follow_up and follow_up_voter_id and str(voter.id) == follow_up_voter_id:
|
||||||
|
ScheduledCall.objects.create(
|
||||||
history_list = list(visited_households.values())
|
tenant=tenant,
|
||||||
history_list.sort(key=lambda x: x["last_visit_date"], reverse=True)
|
voter=voter,
|
||||||
|
volunteer=default_caller,
|
||||||
paginator = Paginator(history_list, 50)
|
comments=call_notes,
|
||||||
page_number = request.GET.get("page")
|
status="pending"
|
||||||
history_page = paginator.get_page(page_number)
|
)
|
||||||
|
|
||||||
|
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 = {
|
context = {
|
||||||
"selected_tenant": tenant,
|
'selected_tenant': tenant,
|
||||||
"history": history_page,
|
'visit_form': form,
|
||||||
"start_date": start_date, "end_date": end_date,
|
'address_street': address_street,
|
||||||
"volunteer_counts": sorted_volunteer_counts,
|
'city': city,
|
||||||
|
'state': state,
|
||||||
|
'zip_code': zip_code,
|
||||||
|
'voters': voters,
|
||||||
|
'next_query_string': next_qs,
|
||||||
}
|
}
|
||||||
return render(request, "core/door_visit_history.html", context)
|
return render(request, 'core/log_door_visit.html', context)
|
||||||
"""
|
|
||||||
|
|
||||||
# Use regex to find and replace the function
|
return redirect(redirect_url)
|
||||||
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
156
door_visit_patch.py
Normal 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
22
fix_views.py
Normal 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
13
fix_views_v2.py
Normal 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
33
patch_log_visit.py
Normal 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}")
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user