3.0
This commit is contained in:
parent
c5d42d341f
commit
442aec63b6
Binary file not shown.
@ -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',
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -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)
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
14
core_view_fix.tmp
Normal 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):
|
||||
"""
|
||||
Loading…
x
Reference in New Issue
Block a user