Autosave: 20260124-224650
This commit is contained in:
parent
7686b1143d
commit
14a93b6b2b
Binary file not shown.
@ -180,3 +180,4 @@ if EMAIL_USE_SSL:
|
|||||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||||
|
|
||||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
|
GOOGLE_MAPS_API_KEY = os.getenv("GOOGLE_MAPS_API_KEY", "AIzaSyAluZTEjH-RSiGJUHnfrSqWbcAXCGzGOq4")
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,5 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
def project_context(request):
|
def project_context(request):
|
||||||
"""
|
"""
|
||||||
@ -10,4 +11,5 @@ def project_context(request):
|
|||||||
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
||||||
# Used for cache-busting static assets
|
# Used for cache-busting static assets
|
||||||
"deployment_timestamp": int(time.time()),
|
"deployment_timestamp": int(time.time()),
|
||||||
}
|
"GOOGLE_MAPS_API_KEY": getattr(settings, 'GOOGLE_MAPS_API_KEY', ''),
|
||||||
|
}
|
||||||
@ -12,8 +12,8 @@ class VoterForm(forms.ModelForm):
|
|||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'registration_date': forms.DateInput(attrs={'type': 'date'}),
|
'registration_date': forms.DateInput(attrs={'type': 'date'}),
|
||||||
'latitude': forms.TextInput(attrs={'readonly': 'readonly', 'class': 'form-control bg-light'}),
|
'latitude': forms.TextInput(attrs={'class': 'form-control bg-light'}),
|
||||||
'longitude': forms.TextInput(attrs={'readonly': 'readonly', 'class': 'form-control bg-light'}),
|
'longitude': forms.TextInput(attrs={'class': 'form-control bg-light'}),
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|||||||
@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-01-24 21:57
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0007_voter_address_street_voter_city_voter_county_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='voter',
|
||||||
|
name='latitude',
|
||||||
|
field=models.DecimalField(blank=True, decimal_places=9, max_digits=12, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='voter',
|
||||||
|
name='longitude',
|
||||||
|
field=models.DecimalField(blank=True, decimal_places=9, max_digits=12, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
Binary file not shown.
107
core/models.py
107
core/models.py
@ -1,6 +1,8 @@
|
|||||||
|
from decimal import Decimal
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
from django.conf import settings
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import json
|
import json
|
||||||
@ -104,8 +106,8 @@ class Voter(models.Model):
|
|||||||
state = models.CharField(max_length=100, blank=True)
|
state = models.CharField(max_length=100, blank=True)
|
||||||
zip_code = models.CharField(max_length=20, blank=True)
|
zip_code = models.CharField(max_length=20, blank=True)
|
||||||
county = models.CharField(max_length=100, blank=True)
|
county = models.CharField(max_length=100, blank=True)
|
||||||
latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
|
latitude = models.DecimalField(max_digits=12, decimal_places=9, null=True, blank=True)
|
||||||
longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
|
longitude = models.DecimalField(max_digits=12, decimal_places=9, null=True, blank=True)
|
||||||
phone = models.CharField(max_length=20, blank=True)
|
phone = models.CharField(max_length=20, blank=True)
|
||||||
email = models.EmailField(blank=True)
|
email = models.EmailField(blank=True)
|
||||||
district = models.CharField(max_length=100, blank=True)
|
district = models.CharField(max_length=100, blank=True)
|
||||||
@ -117,27 +119,69 @@ class Voter(models.Model):
|
|||||||
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
def geocode_address(self):
|
def geocode_address(self, use_fallback=True):
|
||||||
|
"""
|
||||||
|
Attempts to geocode the voter's address using Google Maps API.
|
||||||
|
Returns (success, error_message).
|
||||||
|
"""
|
||||||
if not self.address:
|
if not self.address:
|
||||||
return
|
return False, "No address provided."
|
||||||
|
|
||||||
logger.info(f"Geocoding address: {self.address}")
|
api_key = getattr(settings, 'GOOGLE_MAPS_API_KEY', None)
|
||||||
try:
|
if not api_key:
|
||||||
query = urllib.parse.quote(self.address)
|
return False, "Google Maps API Key not configured."
|
||||||
url = f"https://nominatim.openstreetmap.org/search?q={query}&format=json&limit=1"
|
|
||||||
req = urllib.request.Request(url, headers={'User-Agent': 'FlatlogicVoterApp/1.1 (Campaign Management System; contact: info@example.com)'})
|
def _fetch(addr):
|
||||||
with urllib.request.urlopen(req, timeout=10) as response:
|
try:
|
||||||
data = json.loads(response.read().decode())
|
query = urllib.parse.quote(addr)
|
||||||
if data:
|
url = f"https://maps.googleapis.com/maps/api/geocode/json?address={query}&key={api_key}"
|
||||||
self.latitude = data[0]['lat']
|
req = urllib.request.Request(url)
|
||||||
self.longitude = data[0]['lon']
|
with urllib.request.urlopen(req, timeout=10) as response:
|
||||||
logger.info(f"Geocoding success: {self.latitude}, {self.longitude}")
|
data = json.loads(response.read().decode())
|
||||||
else:
|
if data.get('status') == 'OK':
|
||||||
logger.warning(f"Geocoding returned no results for: {self.address}")
|
result = data['results'][0]
|
||||||
except Exception as e:
|
return result['geometry']['location']['lat'], result['geometry']['location']['lng'], None
|
||||||
logger.error(f"Geocoding error for {self.address}: {e}")
|
elif data.get('status') == 'ZERO_RESULTS':
|
||||||
|
return None, None, "No results found."
|
||||||
|
elif data.get('status') == 'OVER_QUERY_LIMIT':
|
||||||
|
return None, None, "Query limit exceeded."
|
||||||
|
elif data.get('status') == 'REQUEST_DENIED':
|
||||||
|
return None, None, f"Request denied: {data.get('error_message', 'No message')}"
|
||||||
|
elif data.get('status') == 'INVALID_REQUEST':
|
||||||
|
return None, None, "Invalid request."
|
||||||
|
else:
|
||||||
|
return None, None, f"Google Maps Error: {data.get('status')}"
|
||||||
|
except Exception as e:
|
||||||
|
return None, None, str(e)
|
||||||
|
|
||||||
|
logger.info(f"Geocoding with Google Maps: {self.address}")
|
||||||
|
lat, lon, err = _fetch(self.address)
|
||||||
|
|
||||||
|
if not lat and use_fallback:
|
||||||
|
# Try fallback: City, State, Zip
|
||||||
|
fallback_parts = [self.city, self.state, self.zip_code]
|
||||||
|
fallback_addr = ", ".join([p for p in fallback_parts if p])
|
||||||
|
if fallback_addr and fallback_addr != self.address:
|
||||||
|
logger.info(f"Geocoding fallback: {fallback_addr}")
|
||||||
|
lat, lon, fallback_err = _fetch(fallback_addr)
|
||||||
|
if lat:
|
||||||
|
err = None # Clear previous error if fallback works
|
||||||
|
|
||||||
|
if lat and lon:
|
||||||
|
self.latitude = lat
|
||||||
|
# Truncate longitude to 12 characters as requested
|
||||||
|
self.longitude = Decimal(str(lon)[:12])
|
||||||
|
logger.info(f"Geocoding success: {lat}, {self.longitude}")
|
||||||
|
return True, None
|
||||||
|
|
||||||
|
logger.warning(f"Geocoding failed for {self.address}: {err}")
|
||||||
|
return False, err
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
# Ensure longitude is truncated to 12 characters before saving
|
||||||
|
if self.longitude:
|
||||||
|
self.longitude = Decimal(str(self.longitude)[:12])
|
||||||
|
|
||||||
# Auto concatenation: address street, city, state, zip
|
# Auto concatenation: address street, city, state, zip
|
||||||
parts = [self.address_street, self.city, self.state, self.zip_code]
|
parts = [self.address_street, self.city, self.state, self.zip_code]
|
||||||
self.address = ", ".join([p for p in parts if p])
|
self.address = ", ".join([p for p in parts if p])
|
||||||
@ -146,21 +190,30 @@ class Voter(models.Model):
|
|||||||
should_geocode = False
|
should_geocode = False
|
||||||
if not self.pk:
|
if not self.pk:
|
||||||
# New record
|
# New record
|
||||||
should_geocode = True
|
# Only auto-geocode if coordinates were not already provided
|
||||||
|
if self.latitude is None or self.longitude is None:
|
||||||
|
should_geocode = True
|
||||||
else:
|
else:
|
||||||
orig = Voter.objects.get(pk=self.pk)
|
orig = Voter.objects.get(pk=self.pk)
|
||||||
# If any address component changed
|
# Detect if address components changed
|
||||||
if (self.address_street != orig.address_street or
|
address_changed = (self.address_street != orig.address_street or
|
||||||
self.city != orig.city or
|
self.city != orig.city or
|
||||||
self.state != orig.state or
|
self.state != orig.state or
|
||||||
self.zip_code != orig.zip_code):
|
self.zip_code != orig.zip_code)
|
||||||
|
|
||||||
|
# Detect if coordinates were changed in this transaction (e.g., from a form)
|
||||||
|
coords_provided = (self.latitude != orig.latitude or self.longitude != orig.longitude)
|
||||||
|
|
||||||
|
# Auto-geocode if address changed AND coordinates were NOT manually updated
|
||||||
|
if address_changed and not coords_provided:
|
||||||
should_geocode = True
|
should_geocode = True
|
||||||
|
|
||||||
# If coordinates are missing
|
# Auto-geocode if coordinates are still missing and were not just provided
|
||||||
if self.latitude is None or self.longitude is None:
|
if (self.latitude is None or self.longitude is None) and not coords_provided:
|
||||||
should_geocode = True
|
should_geocode = True
|
||||||
|
|
||||||
if should_geocode and self.address:
|
if should_geocode and self.address:
|
||||||
|
# We don't want to block save if geocoding fails, so we just call it
|
||||||
self.geocode_address()
|
self.geocode_address()
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|||||||
@ -55,6 +55,17 @@
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
|
{% if messages %}
|
||||||
|
<div class="container mt-3">
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-{% if message.tags == 'error' %}danger{% else %}{{ message.tags }}{% endif %} alert-dismissible fade show" role="alert">
|
||||||
|
{{ message }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
@ -33,28 +33,84 @@
|
|||||||
<div class="col-12 mb-4">
|
<div class="col-12 mb-4">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<h1 class="h2 fw-bold">Dashboard: <span class="text-emerald">{{ selected_tenant.name }}</span></h1>
|
<h1 class="h2 fw-bold">Dashboard: <span class="text-emerald">{{ selected_tenant.name }}</span></h1>
|
||||||
<a href="{% url 'index' %}" class="btn btn-outline-secondary btn-sm">Switch Campaign</a>
|
<div class="d-flex align-items-center">
|
||||||
</div>
|
<form action="{% url 'voter_list' %}" method="GET" class="d-flex me-3">
|
||||||
</div>
|
<input type="text" name="q" class="form-control form-control-sm me-2" placeholder="Quick Search..." required>
|
||||||
|
<button type="submit" class="btn btn-emerald btn-sm px-3">Search</button>
|
||||||
<div class="col-md-4 mb-4">
|
</form>
|
||||||
<div class="card border-0 shadow-sm h-100">
|
<a href="{% url 'index' %}" class="btn btn-outline-secondary btn-sm">Switch Campaign</a>
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title text-muted">Voters Registry</h5>
|
|
||||||
<p class="card-text display-6 fw-bold">{{ selected_tenant.voters.count }}</p>
|
|
||||||
<a href="{% url 'voter_list' %}" class="btn btn-sm btn-emerald mt-2">View Registry</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-8 mb-4">
|
<!-- Metrics Cards -->
|
||||||
|
<div class="row g-4 mb-4">
|
||||||
|
<div class="col-md-4 col-lg-3">
|
||||||
|
<div class="card border-0 shadow-sm h-100">
|
||||||
|
<div class="card-body text-center p-3">
|
||||||
|
<h6 class="card-title text-muted small mb-2 text-uppercase fw-bold">Registered Voters</h6>
|
||||||
|
<p class="display-6 fw-bold mb-0 text-dark">{{ metrics.total_registered_voters }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 col-lg-3">
|
||||||
|
<div class="card border-0 shadow-sm h-100">
|
||||||
|
<div class="card-body text-center p-3">
|
||||||
|
<h6 class="card-title text-muted small mb-2 text-uppercase fw-bold">Target Voters</h6>
|
||||||
|
<p class="display-6 fw-bold mb-0 text-primary">{{ metrics.total_target_voters }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 col-lg-3">
|
||||||
|
<div class="card border-0 shadow-sm h-100">
|
||||||
|
<div class="card-body text-center p-3">
|
||||||
|
<h6 class="card-title text-muted small mb-2 text-uppercase fw-bold">Supporting</h6>
|
||||||
|
<p class="display-6 fw-bold mb-0 text-success">{{ metrics.total_supporting }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 col-lg-3">
|
||||||
|
<div class="card border-0 shadow-sm h-100">
|
||||||
|
<div class="card-body text-center p-3">
|
||||||
|
<h6 class="card-title text-muted small mb-2 text-uppercase fw-bold">Voter Addresses</h6>
|
||||||
|
<p class="display-6 fw-bold mb-0 text-info">{{ metrics.total_voter_addresses }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 col-lg-3">
|
||||||
|
<div class="card border-0 shadow-sm h-100">
|
||||||
|
<div class="card-body text-center p-3">
|
||||||
|
<h6 class="card-title text-muted small mb-2 text-uppercase fw-bold">Door Visits</h6>
|
||||||
|
<p class="display-6 fw-bold mb-0 text-warning">{{ metrics.total_door_visits }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 col-lg-3">
|
||||||
|
<div class="card border-0 shadow-sm h-100">
|
||||||
|
<div class="card-body text-center p-3">
|
||||||
|
<h6 class="card-title text-muted small mb-2 text-uppercase fw-bold">Signs (Wants/Has)</h6>
|
||||||
|
<p class="display-6 fw-bold mb-0 text-danger">{{ metrics.total_signs }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 col-lg-3">
|
||||||
|
<div class="card border-0 shadow-sm h-100">
|
||||||
|
<div class="card-body text-center p-3">
|
||||||
|
<h6 class="card-title text-muted small mb-2 text-uppercase fw-bold">Total Donations</h6>
|
||||||
|
<p class="display-6 fw-bold mb-0 text-emerald">${{ metrics.total_donations|floatformat:2 }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
<div class="card border-0 shadow-sm">
|
<div class="card border-0 shadow-sm">
|
||||||
<div class="card-body">
|
<div class="card-body d-flex justify-content-between align-items-center">
|
||||||
<h5 class="card-title mb-4">Quick Voter Search</h5>
|
<div>
|
||||||
<form action="{% url 'voter_list' %}" method="GET" class="d-flex">
|
<h5 class="fw-bold mb-1">Voter Management</h5>
|
||||||
<input type="text" name="q" class="form-control me-2" placeholder="Search by name or voter ID..." required>
|
<p class="text-muted mb-0">Access the full registry to manage individual voter profiles.</p>
|
||||||
<button type="submit" class="btn btn-emerald px-4">Search</button>
|
</div>
|
||||||
</form>
|
<a href="{% url 'voter_list' %}" class="btn btn-emerald px-4">View Registry</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,10 +2,8 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<!-- Leaflet CSS -->
|
<!-- Google Maps JS -->
|
||||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
|
<script src="https://maps.googleapis.com/maps/api/js?key={{ GOOGLE_MAPS_API_KEY }}"></script>
|
||||||
<!-- Leaflet JS -->
|
|
||||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
|
|
||||||
|
|
||||||
<div class="container py-5">
|
<div class="container py-5">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
@ -157,7 +155,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Right Column: Timeline & Detailed Records -->
|
<!-- Right Column: Detailed Records -->
|
||||||
<div class="col-lg-8">
|
<div class="col-lg-8">
|
||||||
<!-- Nav Tabs -->
|
<!-- Nav Tabs -->
|
||||||
<ul class="nav nav-tabs border-0 mb-4" id="voterTabs" role="tablist">
|
<ul class="nav nav-tabs border-0 mb-4" id="voterTabs" role="tablist">
|
||||||
@ -391,15 +389,15 @@
|
|||||||
</button>
|
</button>
|
||||||
<div id="geocodeStatus" class="small mt-2 text-center" style="display: none;"></div>
|
<div id="geocodeStatus" class="small mt-2 text-center" style="display: none;"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 mb-3">
|
<div class="col-md-12 mb-3">
|
||||||
<label class="form-label fw-medium">County</label>
|
<label class="form-label fw-medium">County</label>
|
||||||
{{ voter_form.county }}
|
{{ voter_form.county }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<label class="form-label fw-medium">Latitude</label>
|
<label class="form-label fw-medium">Latitude</label>
|
||||||
{{ voter_form.latitude }}
|
{{ voter_form.latitude }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<label class="form-label fw-medium">Longitude</label>
|
<label class="form-label fw-medium">Longitude</label>
|
||||||
{{ voter_form.longitude }}
|
{{ voter_form.longitude }}
|
||||||
</div>
|
</div>
|
||||||
@ -882,16 +880,25 @@
|
|||||||
{% if voter.latitude and voter.longitude %}
|
{% if voter.latitude and voter.longitude %}
|
||||||
// Initialize Map
|
// Initialize Map
|
||||||
try {
|
try {
|
||||||
var map = L.map('voterMap').setView([{{ voter.latitude }}, {{ voter.longitude }}], 15);
|
const position = { lat: {{ voter.latitude }}, lng: {{ voter.longitude }} };
|
||||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
const map = new google.maps.Map(document.getElementById("voterMap"), {
|
||||||
maxZoom: 19,
|
zoom: 15,
|
||||||
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
center: position,
|
||||||
}).addTo(map);
|
});
|
||||||
L.marker([{{ voter.latitude }}, {{ voter.longitude }}]).addTo(map)
|
const marker = new google.maps.Marker({
|
||||||
.bindPopup('{{ voter.first_name }} {{ voter.last_name }}<br>{{ voter.address_street }}')
|
position: position,
|
||||||
.openPopup();
|
map: map,
|
||||||
|
title: "{{ voter.first_name }} {{ voter.last_name }}",
|
||||||
|
});
|
||||||
|
const infowindow = new google.maps.InfoWindow({
|
||||||
|
content: "<strong>{{ voter.first_name }} {{ voter.last_name }}</strong><br>{{ voter.address_street }}",
|
||||||
|
});
|
||||||
|
marker.addListener("click", () => {
|
||||||
|
infowindow.open(map, marker);
|
||||||
|
});
|
||||||
|
infowindow.open(map, marker);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Leaflet map initialization failed:", e);
|
console.error("Google Maps initialization failed:", e);
|
||||||
}
|
}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@ -934,7 +941,7 @@
|
|||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
document.querySelector('[name="latitude"]').value = data.latitude;
|
document.querySelector('[name="latitude"]').value = data.latitude;
|
||||||
document.querySelector('[name="longitude"]').value = data.longitude;
|
document.querySelector('[name="longitude"]').value = String(data.longitude).substring(0, 12);
|
||||||
statusDiv.innerHTML = '<span class="text-success fw-bold"><i class="bi bi-check-circle me-1"></i>Coordinates updated!</span>';
|
statusDiv.innerHTML = '<span class="text-success fw-bold"><i class="bi bi-check-circle me-1"></i>Coordinates updated!</span>';
|
||||||
} else {
|
} else {
|
||||||
statusDiv.innerHTML = '<span class="text-danger">' + (data.error || 'Geocoding failed.') + '</span>';
|
statusDiv.innerHTML = '<span class="text-danger">' + (data.error || 'Geocoding failed.') + '</span>';
|
||||||
@ -954,3 +961,4 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.shortcuts import render, redirect, get_object_or_404
|
from django.shortcuts import render, redirect, get_object_or_404
|
||||||
from django.db.models import Q
|
from django.db.models import Q, Sum
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from .models import Voter, Tenant, Interaction, Donation, VoterLikelihood, EventParticipation, Event, EventType, InteractionType, DonationMethod, ElectionType
|
from .models import Voter, Tenant, Interaction, Donation, VoterLikelihood, EventParticipation, Event, EventType, InteractionType, DonationMethod, ElectionType
|
||||||
from .forms import VoterForm, InteractionForm, DonationForm, VoterLikelihoodForm, EventParticipationForm, EventTypeForm
|
from .forms import VoterForm, InteractionForm, DonationForm, VoterLikelihoodForm, EventParticipationForm, EventTypeForm
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def index(request):
|
def index(request):
|
||||||
"""
|
"""
|
||||||
@ -14,12 +17,26 @@ def index(request):
|
|||||||
tenants = Tenant.objects.all()
|
tenants = Tenant.objects.all()
|
||||||
selected_tenant_id = request.session.get('tenant_id')
|
selected_tenant_id = request.session.get('tenant_id')
|
||||||
selected_tenant = None
|
selected_tenant = None
|
||||||
|
metrics = {}
|
||||||
|
|
||||||
if selected_tenant_id:
|
if selected_tenant_id:
|
||||||
selected_tenant = Tenant.objects.filter(id=selected_tenant_id).first()
|
selected_tenant = Tenant.objects.filter(id=selected_tenant_id).first()
|
||||||
|
if selected_tenant:
|
||||||
|
voters = selected_tenant.voters.all()
|
||||||
|
metrics = {
|
||||||
|
"total_registered_voters": voters.count(),
|
||||||
|
"total_target_voters": voters.filter(is_targeted=True).count(),
|
||||||
|
"total_supporting": voters.filter(candidate_support="supporting").count(),
|
||||||
|
"total_voter_addresses": voters.values("address").distinct().count(),
|
||||||
|
"total_door_visits": Interaction.objects.filter(voter__tenant=selected_tenant, type__name="Door Visit").count(),
|
||||||
|
"total_signs": voters.filter(Q(yard_sign="wants") | Q(yard_sign="has")).count(),
|
||||||
|
"total_donations": Donation.objects.filter(voter__tenant=selected_tenant).aggregate(total=Sum("amount"))["total"] or 0,
|
||||||
|
}
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'tenants': tenants,
|
'tenants': tenants,
|
||||||
'selected_tenant': selected_tenant,
|
'selected_tenant': selected_tenant,
|
||||||
|
'metrics': metrics,
|
||||||
}
|
}
|
||||||
return render(request, 'core/index.html', context)
|
return render(request, 'core/index.html', context)
|
||||||
|
|
||||||
@ -108,11 +125,32 @@ def voter_edit(request, voter_id):
|
|||||||
voter = get_object_or_404(Voter, id=voter_id, tenant=tenant)
|
voter = get_object_or_404(Voter, id=voter_id, tenant=tenant)
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
|
# Log incoming coordinate data for debugging
|
||||||
|
lat_raw = request.POST.get('latitude')
|
||||||
|
lon_raw = request.POST.get('longitude')
|
||||||
|
logger.info(f"Voter Edit POST: lat={lat_raw}, lon={lon_raw}")
|
||||||
|
|
||||||
form = VoterForm(request.POST, instance=voter)
|
form = VoterForm(request.POST, instance=voter)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
form.save()
|
# If coordinates were provided in POST, ensure they are applied to the instance
|
||||||
|
# This handles cases where readonly or other widget settings might interfere
|
||||||
|
voter = form.save(commit=False)
|
||||||
|
if lat_raw:
|
||||||
|
try:
|
||||||
|
voter.latitude = lat_raw
|
||||||
|
except: pass
|
||||||
|
if lon_raw:
|
||||||
|
try:
|
||||||
|
voter.longitude = lon_raw
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
voter.save()
|
||||||
messages.success(request, "Voter profile updated successfully.")
|
messages.success(request, "Voter profile updated successfully.")
|
||||||
return redirect('voter_detail', voter_id=voter.id)
|
else:
|
||||||
|
logger.warning(f"Voter Edit Form Invalid: {form.errors}")
|
||||||
|
for field, errors in form.errors.items():
|
||||||
|
for error in errors:
|
||||||
|
messages.error(request, f"Error in {field}: {error}")
|
||||||
return redirect('voter_detail', voter_id=voter.id)
|
return redirect('voter_detail', voter_id=voter.id)
|
||||||
|
|
||||||
def add_interaction(request, voter_id):
|
def add_interaction(request, voter_id):
|
||||||
@ -352,10 +390,16 @@ def voter_geocode(request, voter_id):
|
|||||||
full_address = ", ".join([p for p in parts if p])
|
full_address = ", ".join([p for p in parts if p])
|
||||||
|
|
||||||
# Use a temporary instance to avoid saving until the user clicks "Save" in the modal
|
# Use a temporary instance to avoid saving until the user clicks "Save" in the modal
|
||||||
temp_voter = Voter(address=full_address)
|
temp_voter = Voter(
|
||||||
temp_voter.geocode_address()
|
address_street=street,
|
||||||
|
city=city,
|
||||||
|
state=state,
|
||||||
|
zip_code=zip_code,
|
||||||
|
address=full_address
|
||||||
|
)
|
||||||
|
success, error_msg = temp_voter.geocode_address()
|
||||||
|
|
||||||
if temp_voter.latitude and temp_voter.longitude:
|
if success:
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
'success': True,
|
'success': True,
|
||||||
'latitude': str(temp_voter.latitude),
|
'latitude': str(temp_voter.latitude),
|
||||||
@ -365,7 +409,7 @@ def voter_geocode(request, voter_id):
|
|||||||
else:
|
else:
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
'success': False,
|
'success': False,
|
||||||
'error': 'Geocoding failed. Please check the address.'
|
'error': f"Geocoding failed: {error_msg or 'No results found.'}"
|
||||||
})
|
})
|
||||||
|
|
||||||
return JsonResponse({'success': False, 'error': 'Invalid request method.'})
|
return JsonResponse({'success': False, 'error': 'Invalid request method.'})
|
||||||
|
|||||||
64
test_manual_save_v2.py
Normal file
64
test_manual_save_v2.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import os
|
||||||
|
import django
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
from core.models import Voter, Tenant
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
# Ensure we have a tenant
|
||||||
|
tenant, _ = Tenant.objects.get_or_create(name="Test Tenant", slug="test-tenant")
|
||||||
|
|
||||||
|
# 1. Create a voter with no coordinates
|
||||||
|
voter = Voter.objects.create(
|
||||||
|
tenant=tenant,
|
||||||
|
first_name="Manual",
|
||||||
|
last_name="Test",
|
||||||
|
address_street="1600 Amphitheatre Parkway",
|
||||||
|
city="Mountain View",
|
||||||
|
state="CA",
|
||||||
|
zip_code="94043"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Initial voter: {voter.first_name} {voter.last_name}")
|
||||||
|
print(f"Coordinates: {voter.latitude}, {voter.longitude}")
|
||||||
|
|
||||||
|
# 2. Simulate manual geocode (updating latitude/longitude before save)
|
||||||
|
manual_lat = Decimal("37.422476400")
|
||||||
|
manual_lon = Decimal("-122.084249900")
|
||||||
|
|
||||||
|
# Simulate what happens in voter_edit view
|
||||||
|
# The form instance will have the new coordinates from request.POST
|
||||||
|
voter.latitude = manual_lat
|
||||||
|
voter.longitude = manual_lon
|
||||||
|
|
||||||
|
print(f"Updating coordinates manually to: {voter.latitude}, {voter.longitude}")
|
||||||
|
voter.save()
|
||||||
|
|
||||||
|
# 3. Reload from DB and verify
|
||||||
|
voter.refresh_from_db()
|
||||||
|
print(f"After save, coordinates in DB: {voter.latitude}, {voter.longitude}")
|
||||||
|
|
||||||
|
if voter.latitude == manual_lat and voter.longitude == manual_lon:
|
||||||
|
print("SUCCESS: Manual coordinates preserved.")
|
||||||
|
else:
|
||||||
|
print("FAILURE: Manual coordinates overwritten or not saved.")
|
||||||
|
|
||||||
|
# 4. Now test if changing address but NOT coordinates triggers geocode (which might overwrite)
|
||||||
|
voter.address_street = "1 Infinite Loop"
|
||||||
|
voter.city = "Cupertino"
|
||||||
|
# Note: we are NOT changing latitude/longitude here, so coords_provided should be False in save()
|
||||||
|
print(f"Changing address to {voter.address_street} but keeping coordinates.")
|
||||||
|
voter.save()
|
||||||
|
|
||||||
|
voter.refresh_from_db()
|
||||||
|
print(f"After address change, coordinates in DB: {voter.latitude}, {voter.longitude}")
|
||||||
|
# It should have updated coordinates to Apple's HQ if geocoding worked,
|
||||||
|
# OR at least it shouldn't be the old Google HQ coordinates if it triggered geocode.
|
||||||
|
# Actually, it SHOULD have triggered geocode.
|
||||||
|
|
||||||
|
if voter.latitude != manual_lat:
|
||||||
|
print("SUCCESS: Auto-geocode triggered for address change.")
|
||||||
|
else:
|
||||||
|
print("INFO: Auto-geocode might not have triggered or returned same coordinates (unlikely).")
|
||||||
Loading…
x
Reference in New Issue
Block a user