Autosave: 20260124-205044
This commit is contained in:
parent
14bf8d7295
commit
7686b1143d
12
ERD.md
12
ERD.md
@ -83,11 +83,19 @@ erDiagram
|
||||
string first_name
|
||||
string last_name
|
||||
text address
|
||||
string address_street
|
||||
string city
|
||||
string state
|
||||
string zip_code
|
||||
string county
|
||||
decimal latitude
|
||||
decimal longitude
|
||||
string phone
|
||||
string email
|
||||
string district
|
||||
string precinct
|
||||
date registration_date
|
||||
boolean is_targeted
|
||||
string candidate_support
|
||||
string yard_sign
|
||||
datetime created_at
|
||||
@ -96,7 +104,6 @@ erDiagram
|
||||
VotingRecord {
|
||||
int id PK
|
||||
int voter_id FK
|
||||
string participation_type
|
||||
date election_date
|
||||
string election_description
|
||||
string primary_party
|
||||
@ -120,7 +127,6 @@ erDiagram
|
||||
Donation {
|
||||
int id PK
|
||||
int voter_id FK
|
||||
string participation_type
|
||||
date date
|
||||
int method_id FK
|
||||
decimal amount
|
||||
@ -129,7 +135,6 @@ erDiagram
|
||||
Interaction {
|
||||
int id PK
|
||||
int voter_id FK
|
||||
string participation_type
|
||||
int type_id FK
|
||||
date date
|
||||
string description
|
||||
@ -139,7 +144,6 @@ erDiagram
|
||||
VoterLikelihood {
|
||||
int id PK
|
||||
int voter_id FK
|
||||
string participation_type
|
||||
int election_type_id FK
|
||||
string likelihood
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -62,9 +62,9 @@ class VoterLikelihoodInline(admin.TabularInline):
|
||||
|
||||
@admin.register(Voter)
|
||||
class VoterAdmin(admin.ModelAdmin):
|
||||
list_display = ('first_name', 'last_name', 'voter_id', 'tenant', 'district', 'candidate_support')
|
||||
list_filter = ('tenant', 'candidate_support', 'yard_sign', 'district')
|
||||
search_fields = ('first_name', 'last_name', 'voter_id')
|
||||
list_display = ('first_name', 'last_name', 'voter_id', 'tenant', 'district', 'candidate_support', 'is_targeted', 'city', 'state')
|
||||
list_filter = ('tenant', 'candidate_support', 'is_targeted', 'yard_sign', 'district', 'city', 'state')
|
||||
search_fields = ('first_name', 'last_name', 'voter_id', 'address', 'city', 'state', 'zip_code', 'county')
|
||||
inlines = [VotingRecordInline, DonationInline, InteractionInline, VoterLikelihoodInline]
|
||||
|
||||
@admin.register(Event)
|
||||
@ -75,4 +75,4 @@ class EventAdmin(admin.ModelAdmin):
|
||||
@admin.register(EventParticipation)
|
||||
class EventParticipationAdmin(admin.ModelAdmin):
|
||||
list_display = ('voter', 'event', 'participation_type')
|
||||
list_filter = ('event__tenant', 'event', 'participation_type')
|
||||
list_filter = ('event__tenant', 'event', 'participation_type')
|
||||
@ -5,23 +5,32 @@ class VoterForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Voter
|
||||
fields = [
|
||||
'first_name', 'last_name', 'address', 'phone', 'email',
|
||||
'voter_id', 'district', 'precinct', 'registration_date',
|
||||
'candidate_support', 'yard_sign'
|
||||
'first_name', 'last_name', 'address_street', 'city', 'state',
|
||||
'zip_code', 'county', 'latitude', 'longitude',
|
||||
'phone', 'email', 'voter_id', 'district', 'precinct',
|
||||
'registration_date', 'is_targeted', 'candidate_support', 'yard_sign'
|
||||
]
|
||||
widgets = {
|
||||
'registration_date': forms.DateInput(attrs={'type': 'date'}),
|
||||
'address': forms.Textarea(attrs={'rows': 2}),
|
||||
'latitude': forms.TextInput(attrs={'readonly': 'readonly', 'class': 'form-control bg-light'}),
|
||||
'longitude': forms.TextInput(attrs={'readonly': 'readonly', 'class': 'form-control bg-light'}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
for field in self.fields.values():
|
||||
field.widget.attrs.update({'class': 'form-control'})
|
||||
for name, field in self.fields.items():
|
||||
if name in ['latitude', 'longitude']:
|
||||
continue
|
||||
if isinstance(field.widget, forms.CheckboxInput):
|
||||
field.widget.attrs.update({'class': 'form-check-input'})
|
||||
else:
|
||||
field.widget.attrs.update({'class': 'form-control'})
|
||||
|
||||
self.fields['candidate_support'].widget.attrs.update({'class': 'form-select'})
|
||||
self.fields['yard_sign'].widget.attrs.update({'class': 'form-select'})
|
||||
|
||||
class InteractionForm(forms.ModelForm):
|
||||
# ... (rest of the file remains the same)
|
||||
class Meta:
|
||||
model = Interaction
|
||||
fields = ['type', 'date', 'description', 'notes']
|
||||
@ -109,4 +118,4 @@ class EventTypeForm(forms.ModelForm):
|
||||
for field in self.fields.values():
|
||||
if not isinstance(field.widget, forms.CheckboxInput):
|
||||
field.widget.attrs.update({'class': 'form-control'})
|
||||
self.fields['is_active'].widget.attrs.update({'class': 'form-check-input'})
|
||||
self.fields['is_active'].widget.attrs.update({'class': 'form-check-input'})
|
||||
18
core/migrations/0006_voter_is_targeted.py
Normal file
18
core/migrations/0006_voter_is_targeted.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-24 16:33
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0005_eventparticipation_participation_type'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='voter',
|
||||
name='is_targeted',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,48 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-24 16:59
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0006_voter_is_targeted'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='voter',
|
||||
name='address_street',
|
||||
field=models.CharField(blank=True, max_length=255),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='voter',
|
||||
name='city',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='voter',
|
||||
name='county',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='voter',
|
||||
name='latitude',
|
||||
field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='voter',
|
||||
name='longitude',
|
||||
field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='voter',
|
||||
name='state',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='voter',
|
||||
name='zip_code',
|
||||
field=models.CharField(blank=True, max_length=20),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
@ -1,6 +1,12 @@
|
||||
from django.db import models
|
||||
from django.utils.text import slugify
|
||||
from django.contrib.auth.models import User
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import json
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class Tenant(models.Model):
|
||||
name = models.CharField(max_length=255)
|
||||
@ -93,16 +99,72 @@ class Voter(models.Model):
|
||||
first_name = models.CharField(max_length=100)
|
||||
last_name = models.CharField(max_length=100)
|
||||
address = models.TextField(blank=True)
|
||||
address_street = models.CharField(max_length=255, blank=True)
|
||||
city = models.CharField(max_length=100, blank=True)
|
||||
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)
|
||||
phone = models.CharField(max_length=20, blank=True)
|
||||
email = models.EmailField(blank=True)
|
||||
district = models.CharField(max_length=100, blank=True)
|
||||
precinct = models.CharField(max_length=100, blank=True)
|
||||
registration_date = models.DateField(null=True, blank=True)
|
||||
is_targeted = models.BooleanField(default=False)
|
||||
candidate_support = models.CharField(max_length=20, choices=SUPPORT_CHOICES, default='unknown')
|
||||
yard_sign = models.CharField(max_length=20, choices=YARD_SIGN_CHOICES, default='none')
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def geocode_address(self):
|
||||
if not self.address:
|
||||
return
|
||||
|
||||
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}")
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# 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])
|
||||
|
||||
# Change detection
|
||||
should_geocode = False
|
||||
if not self.pk:
|
||||
# New record
|
||||
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):
|
||||
should_geocode = True
|
||||
|
||||
# If coordinates are missing
|
||||
if self.latitude is None or self.longitude is None:
|
||||
should_geocode = True
|
||||
|
||||
if should_geocode and self.address:
|
||||
self.geocode_address()
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.first_name} {self.last_name}"
|
||||
|
||||
@ -171,4 +233,4 @@ class VoterLikelihood(models.Model):
|
||||
unique_together = ('voter', 'election_type')
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.voter} - {self.election_type}: {self.get_likelihood_display()}"
|
||||
return f"{self.voter} - {self.election_type}: {self.get_likelihood_display()}"
|
||||
@ -2,6 +2,11 @@
|
||||
{% 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>
|
||||
|
||||
<div class="container py-5">
|
||||
<!-- Breadcrumb -->
|
||||
<nav aria-label="breadcrumb" class="mb-4">
|
||||
@ -38,6 +43,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-auto text-md-end mt-3 mt-md-0">
|
||||
{% if voter.is_targeted %}
|
||||
<div class="badge bg-primary mb-2 px-3 py-2 text-uppercase fw-bold" style="font-size: 0.75rem;"><i class="bi bi-bullseye me-2"></i>Targeted Voter</div>
|
||||
{% endif %}
|
||||
{% if voter.candidate_support == 'supporting' %}
|
||||
<div class="h4 text-success mb-0"><i class="bi bi-check-circle-fill me-2"></i>Supporting</div>
|
||||
{% elif voter.candidate_support == 'not_supporting' %}
|
||||
@ -53,6 +61,22 @@
|
||||
<div class="row">
|
||||
<!-- Left Column: Quick Stats & Likelihood -->
|
||||
<div class="col-lg-4">
|
||||
<!-- Map Card -->
|
||||
<div class="card border-0 shadow-sm mb-4 overflow-hidden">
|
||||
<div class="card-header bg-white py-3">
|
||||
<h5 class="card-title mb-0">Location Map</h5>
|
||||
</div>
|
||||
<div id="voterMap" style="height: 250px; background-color: #f8f9fa;">
|
||||
{% if not voter.latitude or not voter.longitude %}
|
||||
<div class="d-flex align-items-center justify-content-center h-100 flex-column text-muted p-4 text-center">
|
||||
<i class="bi bi-map mb-2" style="font-size: 2rem;"></i>
|
||||
<p class="small mb-0">No coordinates available for this address.</p>
|
||||
<p class="small mb-0">Edit profile to trigger auto-geocoding.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header bg-white py-3">
|
||||
<h5 class="card-title mb-0">Contact Information</h5>
|
||||
@ -333,7 +357,7 @@
|
||||
<h5 class="modal-title" id="editVoterModalLabel">Edit Voter Profile</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form action="{% url 'voter_edit' voter.id %}" method="POST">
|
||||
<form id="editVoterForm" action="{% url 'voter_edit' voter.id %}" method="POST">
|
||||
{% csrf_token %}
|
||||
<div class="modal-body p-4">
|
||||
<div class="row">
|
||||
@ -346,8 +370,38 @@
|
||||
{{ voter_form.last_name }}
|
||||
</div>
|
||||
<div class="col-12 mb-3">
|
||||
<label class="form-label fw-medium">{{ voter_form.address.label }}</label>
|
||||
{{ voter_form.address }}
|
||||
<label class="form-label fw-medium">Address Street</label>
|
||||
{{ voter_form.address_street }}
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-medium">City</label>
|
||||
{{ voter_form.city }}
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<label class="form-label fw-medium">State</label>
|
||||
{{ voter_form.state }}
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<label class="form-label fw-medium">Zip Code</label>
|
||||
{{ voter_form.zip_code }}
|
||||
</div>
|
||||
<div class="col-12 mb-4">
|
||||
<button type="button" id="manualGeocodeBtn" class="btn btn-outline-primary w-100 fw-bold py-2 shadow-sm">
|
||||
<i class="bi bi-geo-alt-fill me-2"></i>Manual Geocode Address
|
||||
</button>
|
||||
<div id="geocodeStatus" class="small mt-2 text-center" style="display: none;"></div>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label fw-medium">County</label>
|
||||
{{ voter_form.county }}
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label fw-medium">Latitude</label>
|
||||
{{ voter_form.latitude }}
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label fw-medium">Longitude</label>
|
||||
{{ voter_form.longitude }}
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-medium">{{ voter_form.phone.label }}</label>
|
||||
@ -373,6 +427,14 @@
|
||||
<label class="form-label fw-medium">{{ voter_form.registration_date.label }}</label>
|
||||
{{ voter_form.registration_date }}
|
||||
</div>
|
||||
<div class="col-md-6 mb-3 d-flex align-items-center">
|
||||
<div class="form-check">
|
||||
{{ voter_form.is_targeted }}
|
||||
<label class="form-check-label fw-medium" for="{{ voter_form.is_targeted.id_for_label }}">
|
||||
Targeted Voter
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label fw-medium">{{ voter_form.candidate_support.label }}</label>
|
||||
{{ voter_form.candidate_support }}
|
||||
@ -816,6 +878,79 @@
|
||||
tab.show();
|
||||
}
|
||||
}
|
||||
|
||||
{% 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();
|
||||
} catch (e) {
|
||||
console.error("Leaflet map initialization failed:", e);
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
// Manual Geocode Logic
|
||||
const geocodeBtn = document.getElementById('manualGeocodeBtn');
|
||||
const statusDiv = document.getElementById('geocodeStatus');
|
||||
|
||||
if (geocodeBtn) {
|
||||
geocodeBtn.addEventListener('click', function() {
|
||||
const street = document.querySelector('[name="address_street"]').value;
|
||||
const city = document.querySelector('[name="city"]').value;
|
||||
const state = document.querySelector('[name="state"]').value;
|
||||
const zip = document.querySelector('[name="zip_code"]').value;
|
||||
|
||||
if (!street && !city) {
|
||||
statusDiv.innerHTML = '<span class="text-danger">Please enter at least a street or city.</span>';
|
||||
statusDiv.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
geocodeBtn.disabled = true;
|
||||
geocodeBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Geocoding...';
|
||||
statusDiv.style.display = 'none';
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('address_street', street);
|
||||
formData.append('city', city);
|
||||
formData.append('state', state);
|
||||
formData.append('zip_code', zip);
|
||||
formData.append('csrfmiddlewaretoken', '{{ csrf_token }}');
|
||||
|
||||
fetch('{% url "voter_geocode" voter.id %}', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
document.querySelector('[name="latitude"]').value = data.latitude;
|
||||
document.querySelector('[name="longitude"]').value = data.longitude;
|
||||
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>';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
statusDiv.innerHTML = '<span class="text-danger">An error occurred during geocoding.</span>';
|
||||
})
|
||||
.finally(() => {
|
||||
geocodeBtn.disabled = false;
|
||||
geocodeBtn.innerHTML = '<i class="bi bi-geo-alt-fill me-2"></i>Manual Geocode Address';
|
||||
statusDiv.style.display = 'block';
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@ -7,6 +7,7 @@ urlpatterns = [
|
||||
path('voters/', views.voter_list, name='voter_list'),
|
||||
path('voters/<int:voter_id>/', views.voter_detail, name='voter_detail'),
|
||||
path('voters/<int:voter_id>/edit/', views.voter_edit, name='voter_edit'),
|
||||
path('voters/<int:voter_id>/geocode/', views.voter_geocode, name='voter_geocode'),
|
||||
|
||||
path('voters/<int:voter_id>/interaction/add/', views.add_interaction, name='add_interaction'),
|
||||
path('interaction/<int:interaction_id>/edit/', views.edit_interaction, name='edit_interaction'),
|
||||
@ -29,4 +30,4 @@ urlpatterns = [
|
||||
path('maintenance/event-types/add/', views.event_type_add, name='event_type_add'),
|
||||
path('maintenance/event-types/<int:type_id>/edit/', views.event_type_edit, name='event_type_edit'),
|
||||
path('maintenance/event-types/<int:type_id>/delete/', views.event_type_delete, name='event_type_delete'),
|
||||
]
|
||||
]
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
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
|
||||
@ -331,4 +332,40 @@ def event_type_delete(request, type_id):
|
||||
messages.success(request, "Event type deleted.")
|
||||
except Exception as e:
|
||||
messages.error(request, f"Cannot delete event type: {e}")
|
||||
return redirect('event_type_list')
|
||||
return redirect('event_type_list')
|
||||
|
||||
def voter_geocode(request, voter_id):
|
||||
"""
|
||||
Manually trigger geocoding for a voter, potentially using values from the request.
|
||||
"""
|
||||
selected_tenant_id = request.session.get('tenant_id')
|
||||
tenant = get_object_or_404(Tenant, id=selected_tenant_id)
|
||||
voter = get_object_or_404(Voter, id=voter_id, tenant=tenant)
|
||||
|
||||
if request.method == 'POST':
|
||||
street = request.POST.get('address_street', voter.address_street)
|
||||
city = request.POST.get('city', voter.city)
|
||||
state = request.POST.get('state', voter.state)
|
||||
zip_code = request.POST.get('zip_code', voter.zip_code)
|
||||
|
||||
parts = [street, city, state, zip_code]
|
||||
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()
|
||||
|
||||
if temp_voter.latitude and temp_voter.longitude:
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'latitude': str(temp_voter.latitude),
|
||||
'longitude': str(temp_voter.longitude),
|
||||
'address': full_address
|
||||
})
|
||||
else:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Geocoding failed. Please check the address.'
|
||||
})
|
||||
|
||||
return JsonResponse({'success': False, 'error': 'Invalid request method.'})
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
Django==5.2.7
|
||||
mysqlclient==2.2.7
|
||||
python-dotenv==1.1.1
|
||||
geopy==2.4.1
|
||||
Loading…
x
Reference in New Issue
Block a user