This commit is contained in:
Flatlogic Bot 2026-02-01 06:28:41 +00:00
parent c5d42d341f
commit 442aec63b6
9 changed files with 192 additions and 45 deletions

View File

@ -69,6 +69,7 @@ MIDDLEWARE = [
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'core.middleware.LoginRequiredMiddleware',
'core.middleware.TimezoneMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
# Disable X-Frame-Options middleware to allow Flatlogic preview iframes.
# 'django.middleware.clickjacking.XFrameOptionsMiddleware',

View File

@ -1,6 +1,9 @@
import zoneinfo
from django.shortcuts import redirect
from django.urls import reverse
from django.conf import settings
from django.utils import timezone
from core.models import CampaignSettings, Tenant
class LoginRequiredMiddleware:
def __init__(self, get_response):
@ -11,7 +14,6 @@ class LoginRequiredMiddleware:
path = request.path_info
# Allow access to login, logout, admin, and any other exempted paths
# We use try/except in case URLs are not defined yet
try:
login_url = reverse('login')
logout_url = reverse('logout')
@ -33,3 +35,34 @@ class LoginRequiredMiddleware:
response = self.get_response(request)
return response
class TimezoneMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
tzname = None
# 1. Try to get tenant from session
tenant_id = request.session.get("tenant_id")
if tenant_id:
try:
campaign_settings = CampaignSettings.objects.get(tenant_id=tenant_id)
tzname = campaign_settings.timezone
except CampaignSettings.DoesNotExist:
pass
# 2. If not found and user is authenticated, maybe they are in admin?
# In admin, we might not have tenant_id in session if they went directly there.
# But this is a multi-tenant app, usually they select a campaign first.
# If they are superuser in admin, we might want to default to something or let them see UTC.
if tzname:
try:
timezone.activate(zoneinfo.ZoneInfo(tzname))
except:
timezone.deactivate()
else:
timezone.deactivate()
return self.get_response(request)

View File

@ -18,6 +18,57 @@
</div>
</div>
<!-- Filters and Summary -->
<div class="row g-4 mb-5">
<!-- Date Filter Card -->
<div class="col-lg-5">
<div class="card border-0 shadow-sm rounded-4 h-100">
<div class="card-body p-4">
<h5 class="card-title fw-bold mb-3">Filter by Date Range</h5>
<form method="get" class="row g-2">
<div class="col-sm-5">
<label class="small text-muted mb-1 ms-1">From</label>
<input type="date" name="start_date" class="form-control rounded-pill border-light bg-light" value="{{ start_date|default:'' }}">
</div>
<div class="col-sm-5">
<label class="small text-muted mb-1 ms-1">To</label>
<input type="date" name="end_date" class="form-control rounded-pill border-light bg-light" value="{{ end_date|default:'' }}">
</div>
<div class="col-sm-2 d-flex align-items-end">
<button type="submit" class="btn btn-primary rounded-pill w-100 px-0">Filter</button>
</div>
{% if start_date or end_date %}
<div class="col-12 mt-1">
<a href="{% url 'door_visit_history' %}" class="text-secondary small ms-1">
<i class="bi bi-x-circle me-1"></i>Clear range
</a>
</div>
{% endif %}
</form>
</div>
</div>
</div>
<!-- Volunteer Counts Summary -->
<div class="col-lg-7">
<div class="card border-0 shadow-sm rounded-4 h-100">
<div class="card-body p-4">
<h5 class="card-title fw-bold mb-3">Visits per Volunteer</h5>
<div class="d-flex flex-wrap gap-2">
{% for v_name, count in volunteer_counts %}
<div class="bg-light rounded-pill px-3 py-2 d-flex align-items-center border">
<span class="fw-medium text-dark me-2">{{ v_name }}</span>
<span class="badge bg-primary rounded-pill">{{ count }}</span>
</div>
{% empty %}
<p class="text-muted small mb-0">No volunteer data available for this selection.</p>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
<!-- Households 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">
@ -33,8 +84,9 @@
<th class="ps-4 py-3 text-uppercase small ls-1">Household Address</th>
<th class="py-3 text-uppercase small ls-1">Voters Visited</th>
<th class="py-3 text-uppercase small ls-1">Last Visit</th>
<th class="py-3 text-uppercase small ls-1">Volunteer</th>
<th class="py-3 text-uppercase small ls-1">Outcome</th>
<th class="pe-4 py-3 text-uppercase small ls-1">Interactions</th>
<th class="pe-4 py-3 text-uppercase small ls-1">Comments</th>
</tr>
</thead>
<tbody>
@ -66,25 +118,37 @@
<div class="fw-bold text-dark">{{ household.last_visit_date|date:"M d, Y" }}</div>
<div class="small text-muted">{{ household.last_visit_date|date:"H:i" }}</div>
</td>
<td>
{% if household.last_volunteer %}
<div class="d-flex align-items-center">
<div class="bg-primary bg-opacity-10 text-primary rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 32px; height: 32px;">
{{ household.last_volunteer.first_name|first }}{{ household.last_volunteer.last_name|first }}
</div>
<span class="fw-medium">{{ household.last_volunteer }}</span>
</div>
{% else %}
<span class="text-muted small">N/A</span>
{% endif %}
</td>
<td>
<span class="badge {% if 'Spoke' in household.last_outcome %}bg-success{% elif 'Literature' in household.last_outcome %}bg-info{% else %}bg-secondary{% endif %} bg-opacity-10 {% if 'Spoke' in household.last_outcome %}text-success{% elif 'Literature' in household.last_outcome %}text-info{% else %}text-secondary{% endif %} px-2 py-1">
{{ household.last_outcome }}
</span>
</td>
<td class="pe-4">
<span class="badge rounded-pill bg-light text-dark border">
{{ household.interaction_count }} Visit{{ household.interaction_count|pluralize }}
</span>
<div class="small text-muted text-wrap" style="max-width: 200px;">
{{ household.notes|default:"-" }}
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="5" class="text-center py-5">
<td colspan="6" class="text-center py-5">
<div class="text-muted mb-2">
<i class="bi bi-calendar-x mb-2" style="font-size: 3rem; opacity: 0.3;"></i>
</div>
<p class="mb-0 fw-medium">No door visits logged yet.</p>
<p class="small text-muted">Visit the <a href="{% url 'door_visits' %}">Planned Visits</a> page to start logging visits.</p>
<p class="mb-0 fw-medium">No door visits found for this selection.</p>
<p class="small text-muted">Try clearing your filters or visit the <a href="{% url 'door_visits' %}">Planned Visits</a> page to log more visits.</p>
</td>
</tr>
{% endfor %}
@ -98,12 +162,12 @@
<ul class="pagination justify-content-center mb-0">
{% if history.has_previous %}
<li class="page-item">
<a class="page-link rounded-circle mx-1 border-0 bg-light text-dark" href="?page=1" aria-label="First">
<a class="page-link rounded-circle mx-1 border-0 bg-light text-dark" href="?page=1{% if start_date %}&start_date={{ start_date }}{% endif %}{% if end_date %}&end_date={{ end_date }}{% 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={{ history.previous_page_number }}" aria-label="Previous">
<a class="page-link rounded-circle mx-1 border-0 bg-light text-dark" href="?page={{ history.previous_page_number }}{% if start_date %}&start_date={{ start_date }}{% endif %}{% if end_date %}&end_date={{ end_date }}{% endif %}" aria-label="Previous">
<i class="bi bi-chevron-left small"></i>
</a>
</li>
@ -113,12 +177,12 @@
{% if history.has_next %}
<li class="page-item">
<a class="page-link rounded-circle mx-1 border-0 bg-light text-dark" href="?page={{ history.next_page_number }}" aria-label="Next">
<a class="page-link rounded-circle mx-1 border-0 bg-light text-dark" href="?page={{ history.next_page_number }}{% if start_date %}&start_date={{ start_date }}{% endif %}{% if end_date %}&end_date={{ end_date }}{% 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={{ history.paginator.num_pages }}" aria-label="Last">
<a class="page-link rounded-circle mx-1 border-0 bg-light text-dark" href="?page={{ history.paginator.num_pages }}{% if start_date %}&start_date={{ start_date }}{% endif %}{% if end_date %}&end_date={{ end_date }}{% endif %}" aria-label="Last">
<i class="bi bi-chevron-double-right small"></i>
</a>
</li>

View File

@ -196,6 +196,7 @@
<input type="hidden" name="city" id="modal_city">
<input type="hidden" name="state" id="modal_state">
<input type="hidden" name="zip_code" id="modal_zip_code">
<input type="hidden" name="next_query_string" value="{{ request.GET.urlencode }}">
<div class="mb-4">
<label class="form-label fw-bold text-primary small text-uppercase">Visit Outcome</label>

View File

@ -1,3 +1,5 @@
from django.utils.dateparse import parse_date
from datetime import datetime, time, timedelta
import base64
import re
import urllib.parse
@ -1277,6 +1279,12 @@ def log_door_visit(request):
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()
@ -1317,7 +1325,7 @@ def log_door_visit(request):
if not voters.exists():
messages.warning(request, f"No targeted voters found at {address_street}.")
return redirect('door_visits')
return redirect(redirect_url)
for voter in voters:
# 1) Update voter flags
@ -1347,8 +1355,7 @@ def log_door_visit(request):
else:
messages.error(request, "There was an error in the visit log form.")
return redirect('door_visits')
return redirect(redirect_url)
def door_visit_history(request):
"""
@ -1360,17 +1367,39 @@ def door_visit_history(request):
return redirect("index")
tenant = get_object_or_404(Tenant, id=selected_tenant_id)
# Get all "Door Visit" interactions for this tenant, ordered by date desc
# Date filter
start_date = request.GET.get("start_date")
end_date = request.GET.get("end_date")
# Get all "Door Visit" interactions for this tenant
interactions = Interaction.objects.filter(
voter__tenant=tenant,
type__name="Door Visit"
).select_related('voter', 'volunteer').order_by('-date')
).select_related("voter", "volunteer")
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}")
# Summary of counts per volunteer
# Grouping by household (unique address)
visited_households = {}
for interaction in interactions:
volunteer_counts = {}
for interaction in interactions.order_by("-date"):
v = interaction.voter
# Use concatenated address if available, otherwise build it
addr = v.address.strip() if v.address else f"{v.address_street}, {v.city}, {v.state} {v.zip_code}".strip(", ")
if not addr:
continue
@ -1378,38 +1407,43 @@ def door_visit_history(request):
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
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,
'last_visit_date': interaction.date,
'last_outcome': interaction.description,
'voters_at_address': set(),
'interaction_count': 0,
'latest_interaction': interaction
"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,
"last_visit_date": interaction.date,
"last_outcome": interaction.description,
"last_volunteer": interaction.volunteer,
"notes": interaction.notes,
"voters_at_address": set(),
"latest_interaction": interaction
}
visited_households[key]['voters_at_address'].add(f"{v.first_name} {v.last_name}")
visited_households[key]['interaction_count'] += 1
visited_households[key]["voters_at_address"].add(f"{v.first_name} {v.last_name}")
if interaction.date > visited_households[key]['last_visit_date']:
visited_households[key]['last_visit_date'] = interaction.date
visited_households[key]['last_outcome'] = interaction.description
visited_households[key]['latest_interaction'] = interaction
# Sort volunteer counts by total (descending)
sorted_volunteer_counts = sorted(volunteer_counts.items(), key=lambda x: x[1], reverse=True)
history_list = list(visited_households.values())
history_list.sort(key=lambda x: x['last_visit_date'], reverse=True)
history_list.sort(key=lambda x: x["last_visit_date"], reverse=True)
paginator = Paginator(history_list, 50)
page_number = request.GET.get('page')
page_number = request.GET.get("page")
history_page = paginator.get_page(page_number)
context = {
'selected_tenant': tenant,
'history': history_page,
"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)
return render(request, "core/door_visit_history.html", context)

14
core_view_fix.tmp Normal file
View File

@ -0,0 +1,14 @@
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)
def log_door_visit(request):
"""