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
|
||||
|
||||
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 time
|
||||
from django.conf import settings
|
||||
|
||||
def project_context(request):
|
||||
"""
|
||||
@ -10,4 +11,5 @@ def project_context(request):
|
||||
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
||||
# Used for cache-busting static assets
|
||||
"deployment_timestamp": int(time.time()),
|
||||
}
|
||||
"GOOGLE_MAPS_API_KEY": getattr(settings, 'GOOGLE_MAPS_API_KEY', ''),
|
||||
}
|
||||
@ -12,8 +12,8 @@ class VoterForm(forms.ModelForm):
|
||||
]
|
||||
widgets = {
|
||||
'registration_date': forms.DateInput(attrs={'type': 'date'}),
|
||||
'latitude': forms.TextInput(attrs={'readonly': 'readonly', 'class': 'form-control bg-light'}),
|
||||
'longitude': forms.TextInput(attrs={'readonly': 'readonly', 'class': 'form-control bg-light'}),
|
||||
'latitude': forms.TextInput(attrs={'class': 'form-control bg-light'}),
|
||||
'longitude': forms.TextInput(attrs={'class': 'form-control bg-light'}),
|
||||
}
|
||||
|
||||
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.utils.text import slugify
|
||||
from django.contrib.auth.models import User
|
||||
from django.conf import settings
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import json
|
||||
@ -104,8 +106,8 @@ class Voter(models.Model):
|
||||
state = models.CharField(max_length=100, blank=True)
|
||||
zip_code = models.CharField(max_length=20, blank=True)
|
||||
county = models.CharField(max_length=100, blank=True)
|
||||
latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True)
|
||||
longitude = 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=12, decimal_places=9, null=True, blank=True)
|
||||
phone = models.CharField(max_length=20, blank=True)
|
||||
email = models.EmailField(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)
|
||||
|
||||
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:
|
||||
return
|
||||
return False, "No address provided."
|
||||
|
||||
logger.info(f"Geocoding address: {self.address}")
|
||||
try:
|
||||
query = urllib.parse.quote(self.address)
|
||||
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)'})
|
||||
with urllib.request.urlopen(req, timeout=10) as response:
|
||||
data = json.loads(response.read().decode())
|
||||
if data:
|
||||
self.latitude = data[0]['lat']
|
||||
self.longitude = data[0]['lon']
|
||||
logger.info(f"Geocoding success: {self.latitude}, {self.longitude}")
|
||||
else:
|
||||
logger.warning(f"Geocoding returned no results for: {self.address}")
|
||||
except Exception as e:
|
||||
logger.error(f"Geocoding error for {self.address}: {e}")
|
||||
api_key = getattr(settings, 'GOOGLE_MAPS_API_KEY', None)
|
||||
if not api_key:
|
||||
return False, "Google Maps API Key not configured."
|
||||
|
||||
def _fetch(addr):
|
||||
try:
|
||||
query = urllib.parse.quote(addr)
|
||||
url = f"https://maps.googleapis.com/maps/api/geocode/json?address={query}&key={api_key}"
|
||||
req = urllib.request.Request(url)
|
||||
with urllib.request.urlopen(req, timeout=10) as response:
|
||||
data = json.loads(response.read().decode())
|
||||
if data.get('status') == 'OK':
|
||||
result = data['results'][0]
|
||||
return result['geometry']['location']['lat'], result['geometry']['location']['lng'], None
|
||||
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):
|
||||
# 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
|
||||
parts = [self.address_street, self.city, self.state, self.zip_code]
|
||||
self.address = ", ".join([p for p in parts if p])
|
||||
@ -146,21 +190,30 @@ class Voter(models.Model):
|
||||
should_geocode = False
|
||||
if not self.pk:
|
||||
# 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:
|
||||
orig = Voter.objects.get(pk=self.pk)
|
||||
# If any address component changed
|
||||
if (self.address_street != orig.address_street or
|
||||
self.city != orig.city or
|
||||
self.state != orig.state or
|
||||
self.zip_code != orig.zip_code):
|
||||
# Detect if address components changed
|
||||
address_changed = (self.address_street != orig.address_street or
|
||||
self.city != orig.city or
|
||||
self.state != orig.state or
|
||||
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
|
||||
|
||||
# If coordinates are missing
|
||||
if self.latitude is None or self.longitude is None:
|
||||
# Auto-geocode if coordinates are still missing and were not just provided
|
||||
if (self.latitude is None or self.longitude is None) and not coords_provided:
|
||||
should_geocode = True
|
||||
|
||||
if should_geocode and self.address:
|
||||
# We don't want to block save if geocoding fails, so we just call it
|
||||
self.geocode_address()
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@ -55,6 +55,17 @@
|
||||
</nav>
|
||||
|
||||
<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 %}
|
||||
</main>
|
||||
|
||||
|
||||
@ -33,28 +33,84 @@
|
||||
<div class="col-12 mb-4">
|
||||
<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>
|
||||
<a href="{% url 'index' %}" class="btn btn-outline-secondary btn-sm">Switch Campaign</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 mb-4">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<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 class="d-flex align-items-center">
|
||||
<form action="{% url 'voter_list' %}" method="GET" class="d-flex me-3">
|
||||
<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>
|
||||
</form>
|
||||
<a href="{% url 'index' %}" class="btn btn-outline-secondary btn-sm">Switch Campaign</a>
|
||||
</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-body">
|
||||
<h5 class="card-title mb-4">Quick Voter Search</h5>
|
||||
<form action="{% url 'voter_list' %}" method="GET" class="d-flex">
|
||||
<input type="text" name="q" class="form-control me-2" placeholder="Search by name or voter ID..." required>
|
||||
<button type="submit" class="btn btn-emerald px-4">Search</button>
|
||||
</form>
|
||||
<div class="card-body d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h5 class="fw-bold mb-1">Voter Management</h5>
|
||||
<p class="text-muted mb-0">Access the full registry to manage individual voter profiles.</p>
|
||||
</div>
|
||||
<a href="{% url 'voter_list' %}" class="btn btn-emerald px-4">View Registry</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -2,10 +2,8 @@
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Leaflet CSS -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
|
||||
<!-- Leaflet JS -->
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
|
||||
<!-- Google Maps JS -->
|
||||
<script src="https://maps.googleapis.com/maps/api/js?key={{ GOOGLE_MAPS_API_KEY }}"></script>
|
||||
|
||||
<div class="container py-5">
|
||||
<!-- Breadcrumb -->
|
||||
@ -157,7 +155,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Timeline & Detailed Records -->
|
||||
<!-- Right Column: Detailed Records -->
|
||||
<div class="col-lg-8">
|
||||
<!-- Nav Tabs -->
|
||||
<ul class="nav nav-tabs border-0 mb-4" id="voterTabs" role="tablist">
|
||||
@ -391,15 +389,15 @@
|
||||
</button>
|
||||
<div id="geocodeStatus" class="small mt-2 text-center" style="display: none;"></div>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="col-md-12 mb-3">
|
||||
<label class="form-label fw-medium">County</label>
|
||||
{{ voter_form.county }}
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-medium">Latitude</label>
|
||||
{{ voter_form.latitude }}
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-medium">Longitude</label>
|
||||
{{ voter_form.longitude }}
|
||||
</div>
|
||||
@ -882,16 +880,25 @@
|
||||
{% if voter.latitude and voter.longitude %}
|
||||
// Initialize Map
|
||||
try {
|
||||
var map = L.map('voterMap').setView([{{ voter.latitude }}, {{ voter.longitude }}], 15);
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||
}).addTo(map);
|
||||
L.marker([{{ voter.latitude }}, {{ voter.longitude }}]).addTo(map)
|
||||
.bindPopup('{{ voter.first_name }} {{ voter.last_name }}<br>{{ voter.address_street }}')
|
||||
.openPopup();
|
||||
const position = { lat: {{ voter.latitude }}, lng: {{ voter.longitude }} };
|
||||
const map = new google.maps.Map(document.getElementById("voterMap"), {
|
||||
zoom: 15,
|
||||
center: position,
|
||||
});
|
||||
const marker = new google.maps.Marker({
|
||||
position: position,
|
||||
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) {
|
||||
console.error("Leaflet map initialization failed:", e);
|
||||
console.error("Google Maps initialization failed:", e);
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
@ -934,7 +941,7 @@
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
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>';
|
||||
} else {
|
||||
statusDiv.innerHTML = '<span class="text-danger">' + (data.error || 'Geocoding failed.') + '</span>';
|
||||
@ -954,3 +961,4 @@
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
from django.http import JsonResponse
|
||||
from django.urls import reverse
|
||||
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 .models import Voter, Tenant, Interaction, Donation, VoterLikelihood, EventParticipation, Event, EventType, InteractionType, DonationMethod, ElectionType
|
||||
from .forms import VoterForm, InteractionForm, DonationForm, VoterLikelihoodForm, EventParticipationForm, EventTypeForm
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def index(request):
|
||||
"""
|
||||
@ -14,12 +17,26 @@ def index(request):
|
||||
tenants = Tenant.objects.all()
|
||||
selected_tenant_id = request.session.get('tenant_id')
|
||||
selected_tenant = None
|
||||
metrics = {}
|
||||
|
||||
if selected_tenant_id:
|
||||
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 = {
|
||||
'tenants': tenants,
|
||||
'selected_tenant': selected_tenant,
|
||||
'metrics': metrics,
|
||||
}
|
||||
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)
|
||||
|
||||
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)
|
||||
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.")
|
||||
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)
|
||||
|
||||
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])
|
||||
|
||||
# Use a temporary instance to avoid saving until the user clicks "Save" in the modal
|
||||
temp_voter = Voter(address=full_address)
|
||||
temp_voter.geocode_address()
|
||||
temp_voter = Voter(
|
||||
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({
|
||||
'success': True,
|
||||
'latitude': str(temp_voter.latitude),
|
||||
@ -365,7 +409,7 @@ def voter_geocode(request, voter_id):
|
||||
else:
|
||||
return JsonResponse({
|
||||
'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.'})
|
||||
|
||||
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