3.2
This commit is contained in:
parent
8de72675e5
commit
fbcc2964bf
Binary file not shown.
Binary file not shown.
@ -12,6 +12,9 @@
|
||||
<button type="button" class="btn btn-outline-primary shadow-sm py-1 px-4 flex-grow-1" data-bs-toggle="modal" data-bs-target="#mapModal">
|
||||
<i class="bi bi-map-fill me-1"></i> View Map
|
||||
</button>
|
||||
<a href="{% url 'neighborhood_counts' %}?city={{ city_filter|urlencode }}&district={{ district_filter|urlencode }}&neighborhood={{ neighborhood_filter|urlencode }}&address={{ address_filter|urlencode }}" class="btn btn-outline-info shadow-sm py-1 px-4 flex-grow-1">
|
||||
<i class="bi bi-grid-3x3-gap-fill me-1"></i> Neighborhoods
|
||||
</a>
|
||||
<a href="{% url 'door_visit_history' %}" class="btn btn-outline-success shadow-sm py-1 px-4 flex-grow-1">
|
||||
<i class="bi bi-clock-history me-1"></i> Visit History
|
||||
</a>
|
||||
@ -25,14 +28,18 @@
|
||||
</div>
|
||||
<div class="card-body p-3 p-md-4">
|
||||
<form method="GET" action="." class="row g-3 align-items-end">
|
||||
<div class="col-12 col-md-3">
|
||||
<div class="col-12 col-md-2">
|
||||
<label class="form-label small fw-bold text-uppercase text-muted">District</label>
|
||||
<input type="text" name="district" class="form-control rounded-3" placeholder="Filter by district..." value="{{ district_filter }}">
|
||||
</div>
|
||||
<div class="col-12 col-md-3">
|
||||
<div class="col-12 col-md-2">
|
||||
<label class="form-label small fw-bold text-uppercase text-muted">Neighborhood</label>
|
||||
<input type="text" name="neighborhood" class="form-control rounded-3" placeholder="Filter by neighborhood..." value="{{ neighborhood_filter }}">
|
||||
</div>
|
||||
<div class="col-12 col-md-2">
|
||||
<label class="form-label small fw-bold text-uppercase text-muted">City</label>
|
||||
<input type="text" name="city" class="form-control rounded-3" placeholder="Filter by city..." value="{{ city_filter }}">
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<label class="form-label small fw-bold text-uppercase text-muted">Address Search</label>
|
||||
<input type="text" name="address" class="form-control rounded-3" placeholder="Filter by street address..." value="{{ address_filter }}">
|
||||
@ -130,12 +137,12 @@
|
||||
<ul class="pagination justify-content-center mb-0">
|
||||
{% if households.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link rounded-circle mx-1 border-0 bg-light text-dark" href="?page=1{% if district_filter %}&district={{ district_filter|urlencode }}{% endif %}{% if neighborhood_filter %}&neighborhood={{ neighborhood_filter|urlencode }}{% endif %}{% if address_filter %}&address={{ address_filter|urlencode }}{% 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 city_filter %}&city={{ city_filter|urlencode }}{% endif %}{% if address_filter %}&address={{ address_filter|urlencode }}{% endif %}" aria-label="First">
|
||||
<i class="bi bi-chevron-double-left small"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link rounded-circle mx-1 border-0 bg-light text-dark" href="?page={{ households.previous_page_number }}{% if district_filter %}&district={{ district_filter|urlencode }}{% endif %}{% if neighborhood_filter %}&neighborhood={{ neighborhood_filter|urlencode }}{% endif %}{% if address_filter %}&address={{ address_filter|urlencode }}{% 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 city_filter %}&city={{ city_filter|urlencode }}{% endif %}{% if address_filter %}&address={{ address_filter|urlencode }}{% endif %}" aria-label="Previous">
|
||||
<i class="bi bi-chevron-left small"></i>
|
||||
</a>
|
||||
</li>
|
||||
@ -145,12 +152,12 @@
|
||||
|
||||
{% if households.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link rounded-circle mx-1 border-0 bg-light text-dark" href="?page={{ households.next_page_number }}{% if district_filter %}&district={{ district_filter|urlencode }}{% endif %}{% if neighborhood_filter %}&neighborhood={{ neighborhood_filter|urlencode }}{% endif %}{% if address_filter %}&address={{ address_filter|urlencode }}{% 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 city_filter %}&city={{ city_filter|urlencode }}{% endif %}{% if address_filter %}&address={{ address_filter|urlencode }}{% endif %}" aria-label="Next">
|
||||
<i class="bi bi-chevron-right small"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link rounded-circle mx-1 border-0 bg-light text-dark" href="?page={{ households.paginator.num_pages }}{% if district_filter %}&district={{ district_filter|urlencode }}{% endif %}{% if neighborhood_filter %}&neighborhood={{ neighborhood_filter|urlencode }}{% endif %}{% if address_filter %}&address={{ address_filter|urlencode }}{% 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 city_filter %}&city={{ city_filter|urlencode }}{% endif %}{% if address_filter %}&address={{ address_filter|urlencode }}{% endif %}" aria-label="Last">
|
||||
<i class="bi bi-chevron-double-right small"></i>
|
||||
</a>
|
||||
</li>
|
||||
@ -450,4 +457,4 @@
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
76
core/templates/core/neighborhood_counts.html
Normal file
76
core/templates/core/neighborhood_counts.html
Normal file
@ -0,0 +1,76 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4 py-md-5 px-3 px-md-4">
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-4 mb-md-5 gap-3">
|
||||
<div>
|
||||
<h1 class="h2 fw-bold text-dark mb-1">Neighborhood Summary</h1>
|
||||
<p class="text-muted mb-0">Household counts by neighborhood based on active filters.</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{% url 'door_visits' %}?city={{ city_filter|urlencode }}&district={{ district_filter|urlencode }}&neighborhood={{ neighborhood_filter|urlencode }}&address={{ address_filter|urlencode }}" class="btn btn-outline-secondary shadow-sm py-1 px-4">
|
||||
<i class="bi bi-arrow-left me-1"></i> Back to Door Visits
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters Info -->
|
||||
{% if district_filter or neighborhood_filter or city_filter or address_filter %}
|
||||
<div class="alert alert-info border-0 shadow-sm rounded-4 mb-4">
|
||||
<i class="bi bi-info-circle-fill me-2"></i>
|
||||
Active Filters:
|
||||
{% if district_filter %}<strong>District:</strong> {{ district_filter }}{% endif %}
|
||||
{% if neighborhood_filter %}{% if district_filter %}, {% endif %}<strong>Neighborhood:</strong> {{ neighborhood_filter }}{% endif %}
|
||||
{% if city_filter %}{% if district_filter or neighborhood_filter %}, {% endif %}<strong>City:</strong> {{ city_filter }}{% endif %}
|
||||
{% if address_filter %}{% if district_filter or neighborhood_filter or city_filter %}, {% endif %}<strong>Address:</strong> {{ address_filter }}{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Neighborhood Counts List -->
|
||||
<div class="card border-0 shadow-sm rounded-4 overflow-hidden">
|
||||
<div class="card-header bg-white py-3 border-bottom d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title mb-0 fw-bold text-dark">Neighborhoods</h5>
|
||||
<span class="badge bg-primary-subtle text-primary px-3 py-2 rounded-pill">
|
||||
{{ neighborhoods|length }} Neighborhoods
|
||||
</span>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="bg-light text-muted">
|
||||
<tr>
|
||||
<th class="ps-4 py-3 text-uppercase small ls-1">Neighborhood</th>
|
||||
<th class="py-3 text-uppercase small ls-1 text-center">Unvisited Households</th>
|
||||
<th class="pe-4 py-3 text-uppercase small ls-1 text-end">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in neighborhoods %}
|
||||
<tr>
|
||||
<td class="ps-4">
|
||||
<span class="fw-bold text-dark">{{ item.display_name }}</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="badge bg-light text-dark border px-3 py-2 fw-bold" style="font-size: 1rem;">
|
||||
{{ item.count }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="pe-4 text-end">
|
||||
<a href="{% url 'door_visits' %}?city={{ city_filter|urlencode }}&district={{ district_filter|urlencode }}&neighborhood={{ item.neighborhood|default:''|urlencode }}&address={{ address_filter|urlencode }}" class="btn btn-sm btn-primary rounded-3 px-3">
|
||||
View Households
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="3" class="text-center py-5 text-muted">
|
||||
No data found for the current filters.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -63,6 +63,7 @@ urlpatterns = [
|
||||
path('door-visits/', views.door_visits, name='door_visits'),
|
||||
path('door-visits/log/', views.log_door_visit, name='log_door_visit'),
|
||||
path('door-visits/history/', views.door_visit_history, name='door_visit_history'),
|
||||
path('door-visits/neighborhoods/', views.neighborhood_counts, name='neighborhood_counts'),
|
||||
|
||||
# Call Queue
|
||||
path('call-queue/', views.call_queue, name='call_queue'),
|
||||
|
||||
@ -1588,6 +1588,7 @@ def door_visits(request):
|
||||
|
||||
tenant = get_object_or_404(Tenant, id=selected_tenant_id)
|
||||
|
||||
city_filter = request.GET.get("city", "").strip()
|
||||
# Filters from GET parameters
|
||||
district_filter = request.GET.get('district', '').strip()
|
||||
neighborhood_filter = request.GET.get('neighborhood', '').strip()
|
||||
@ -1596,6 +1597,8 @@ def door_visits(request):
|
||||
# Initial queryset: unvisited targeted voters for this tenant
|
||||
voters = Voter.objects.filter(tenant=tenant, door_visit=False, is_targeted=True)
|
||||
|
||||
if city_filter:
|
||||
voters = voters.filter(city__icontains=city_filter)
|
||||
# Apply filters if provided
|
||||
if district_filter:
|
||||
voters = voters.filter(district=district_filter)
|
||||
@ -1671,6 +1674,7 @@ def door_visits(request):
|
||||
'district_filter': district_filter,
|
||||
'neighborhood_filter': neighborhood_filter,
|
||||
'address_filter': address_filter,
|
||||
"city_filter": city_filter,
|
||||
'map_data_json': json.dumps(map_data),
|
||||
'GOOGLE_MAPS_API_KEY': getattr(settings, 'GOOGLE_MAPS_API_KEY', ''),
|
||||
'visit_form': DoorVisitLogForm(),
|
||||
@ -2076,3 +2080,56 @@ def profile(request):
|
||||
|
||||
if request.method == 'POST':
|
||||
u_form = UserUpdateForm(request.POST, instance=request.user)
|
||||
|
||||
|
||||
@role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin'], permission='core.view_voter')
|
||||
def neighborhood_counts(request):
|
||||
"""
|
||||
Shows household counts by neighborhood after applying filters from door visits.
|
||||
"""
|
||||
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)
|
||||
|
||||
city_filter = request.GET.get("city", "").strip()
|
||||
district_filter = request.GET.get('district', '').strip()
|
||||
neighborhood_filter = request.GET.get('neighborhood', '').strip()
|
||||
address_filter = request.GET.get('address', '').strip()
|
||||
|
||||
voters = Voter.objects.filter(tenant=tenant, door_visit=False, is_targeted=True)
|
||||
|
||||
if city_filter:
|
||||
voters = voters.filter(city__icontains=city_filter)
|
||||
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__icontains=address_filter) | Q(address_street__icontains=address_filter))
|
||||
|
||||
household_qs = voters.values('neighborhood', 'address_street', 'city', 'state', 'zip_code').distinct()
|
||||
|
||||
neighborhood_counts_dict = {}
|
||||
for h in household_qs:
|
||||
nb = h['neighborhood']
|
||||
neighborhood_counts_dict[nb] = neighborhood_counts_dict.get(nb, 0) + 1
|
||||
|
||||
neighborhood_list = [
|
||||
{'neighborhood': nb, 'display_name': nb or "Unknown", 'count': count}
|
||||
for nb, count in neighborhood_counts_dict.items()
|
||||
]
|
||||
|
||||
neighborhood_list.sort(key=lambda x: x['count'], reverse=True)
|
||||
|
||||
context = {
|
||||
'selected_tenant': tenant,
|
||||
'neighborhoods': neighborhood_list,
|
||||
'city_filter': city_filter,
|
||||
'district_filter': district_filter,
|
||||
'neighborhood_filter': neighborhood_filter,
|
||||
'address_filter': address_filter,
|
||||
}
|
||||
return render(request, 'core/neighborhood_counts.html', context)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user