From fbcc2964bf5aaea7bbfd62a01d8916d859e2a988 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Sun, 8 Feb 2026 22:15:25 +0000 Subject: [PATCH] 3.2 --- core/__pycache__/urls.cpython-311.pyc | Bin 7287 -> 7414 bytes core/__pycache__/views.cpython-311.pyc | Bin 116723 -> 119823 bytes core/templates/core/door_visits.html | 21 +++-- core/templates/core/neighborhood_counts.html | 76 +++++++++++++++++++ core/urls.py | 1 + core/views.py | 57 ++++++++++++++ 6 files changed, 148 insertions(+), 7 deletions(-) create mode 100644 core/templates/core/neighborhood_counts.html diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 78e4e575b3a2802284eb45d7a835ba47a1095394..c59d399fcdce28894b72d86a1005e9f6e6ba84cc 100644 GIT binary patch delta 719 zcmexv@y(KNIWI340}!0y=*-j>oyaG_ShP{ylVfrKBdca{Dsz@KND>56O4hI~V`gAj z4a5*Ikul09r8Jm9vutw(M<|zpR7!q+k#1RLab`)eeqL&3dPY)yQAU1#O0oVeVPt-M za(-!EN%7<}LK7HmH^&O=Gcpy~Pwo^6;Bp0;%m~EA>nA@J(UWy$K~Y0C+msIF}hCn1(P|V3P5%*m|Q0+4`iPMlkdPJpBP9)4@~-j z$viQT`hGCE0Zd*LQ>X{o_d$h$m9xR+0;k>v)++{1C*&@$L|kNvxWW?gK@}{o29?)d z5qd?};XwTbmhg)#;a6C~Kd6J{HK6jkD@w2EyIk;&xxf;8ktOyDOY8?tumT;Z0)q{z zSB%^)_{LsfiMz-WcZG!|?t`xD%&OMp~% zfZ3}glz{BBVDbZ)6qW>un1RViNqr!@6HM*|i`PqrhaXG{_C}!0dz4N?u?e)-Xc7sJfu^in_xEccd_=Wdtj6Vgf7a zke$JLMM7gm%>@>li!3%*SZqExGl3PjOy-i2;&6p<+$QS)IqndS$7DYl2|-UVr-2&; Ji=2UJ1^}`G(F_0p delta 582 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 View Map + + Neighborhoods + Visit History @@ -25,14 +28,18 @@
-
+
-
+
+
+ + +
@@ -130,12 +137,12 @@
    {% if households.has_previous %}
  • - +
  • - +
  • @@ -145,12 +152,12 @@ {% if households.has_next %}
  • - +
  • - +
  • @@ -450,4 +457,4 @@ } } -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/neighborhood_counts.html b/core/templates/core/neighborhood_counts.html new file mode 100644 index 0000000..79a3edd --- /dev/null +++ b/core/templates/core/neighborhood_counts.html @@ -0,0 +1,76 @@ +{% extends "base.html" %} +{% load static %} + +{% block content %} +
    +
    +
    +

    Neighborhood Summary

    +

    Household counts by neighborhood based on active filters.

    +
    + +
    + + + {% if district_filter or neighborhood_filter or city_filter or address_filter %} +
    + + Active Filters: + {% if district_filter %}District: {{ district_filter }}{% endif %} + {% if neighborhood_filter %}{% if district_filter %}, {% endif %}Neighborhood: {{ neighborhood_filter }}{% endif %} + {% if city_filter %}{% if district_filter or neighborhood_filter %}, {% endif %}City: {{ city_filter }}{% endif %} + {% if address_filter %}{% if district_filter or neighborhood_filter or city_filter %}, {% endif %}Address: {{ address_filter }}{% endif %} +
    + {% endif %} + + +
    +
    +
    Neighborhoods
    + + {{ neighborhoods|length }} Neighborhoods + +
    +
    + + + + + + + + + + {% for item in neighborhoods %} + + + + + + {% empty %} + + + + {% endfor %} + +
    NeighborhoodUnvisited HouseholdsAction
    + {{ item.display_name }} + + + {{ item.count }} + + + + View Households + +
    + No data found for the current filters. +
    +
    +
    +
    +{% endblock %} diff --git a/core/urls.py b/core/urls.py index 0c592a0..a1ec9ee 100644 --- a/core/urls.py +++ b/core/urls.py @@ -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'), diff --git a/core/views.py b/core/views.py index 69c271f..89560a6 100644 --- a/core/views.py +++ b/core/views.py @@ -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)