Autosave: 20260124-224650

This commit is contained in:
Flatlogic Bot 2026-01-24 22:46:50 +00:00
parent 7686b1143d
commit 14a93b6b2b
16 changed files with 334 additions and 72 deletions

View File

@ -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")

View File

@ -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', ''),
} }

View File

@ -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):

View File

@ -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),
),
]

View File

@ -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)

View File

@ -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>

View File

@ -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>

View File

@ -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: '&copy; <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 %}

View File

@ -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
View 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).")