Autosave: 20260203-133539

This commit is contained in:
Flatlogic Bot 2026-02-03 13:35:39 +00:00
parent c3568101a3
commit ac80c84fbd
19 changed files with 718 additions and 56 deletions

View File

@ -54,6 +54,7 @@ VOTER_MAPPABLE_FIELDS = [
('longitude', 'Longitude'), ('longitude', 'Longitude'),
('secondary_phone', 'Secondary Phone'), ('secondary_phone', 'Secondary Phone'),
('secondary_phone_type', 'Secondary Phone Type'), ('secondary_phone_type', 'Secondary Phone Type'),
('door_visit', 'Door Visit'),
] ]
EVENT_MAPPABLE_FIELDS = [ EVENT_MAPPABLE_FIELDS = [
@ -428,7 +429,7 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin):
if val == "" and not created: continue # Skip empty updates for existing records unless specifically desired? if val == "" and not created: continue # Skip empty updates for existing records unless specifically desired?
# Type conversion and normalization # Type conversion and normalization
if field_name == "is_targeted": if field_name in ["is_targeted", "door_visit"]:
val = val.lower() in ["true", "1", "yes"] val = val.lower() in ["true", "1", "yes"]
elif field_name in ["birthdate", "registration_date"]: elif field_name in ["birthdate", "registration_date"]:
parsed_date = None parsed_date = None
@ -794,7 +795,7 @@ class VolunteerAdmin(BaseImportAdminMixin, admin.ModelAdmin):
list_filter = ('tenant',) list_filter = ('tenant',)
fields = ('tenant', 'user', 'first_name', 'last_name', 'email', 'phone', 'notes', 'interests') fields = ('tenant', 'user', 'first_name', 'last_name', 'email', 'phone', 'notes', 'interests')
search_fields = ('first_name', 'last_name', 'email', 'phone') search_fields = ('first_name', 'last_name', 'email', 'phone')
inlines = [VolunteerEventInline, InteractionInline] inlines = [VolunteerEventInline]
filter_horizontal = ('interests',) filter_horizontal = ('interests',)
change_list_template = "admin/volunteer_change_list.html" change_list_template = "admin/volunteer_change_list.html"

View File

@ -1,4 +1,5 @@
from django import forms from django import forms
from django.contrib.auth.models import User
from .models import Voter, Interaction, Donation, VoterLikelihood, InteractionType, DonationMethod, ElectionType, Event, EventParticipation, EventType, Tenant, ParticipationStatus, Volunteer, VolunteerEvent, VolunteerRole, ScheduledCall from .models import Voter, Interaction, Donation, VoterLikelihood, InteractionType, DonationMethod, ElectionType, Event, EventParticipation, EventType, Tenant, ParticipationStatus, Volunteer, VolunteerEvent, VolunteerRole, ScheduledCall
class VoterForm(forms.ModelForm): class VoterForm(forms.ModelForm):
@ -44,6 +45,7 @@ class AdvancedVoterSearchForm(forms.Form):
first_name = forms.CharField(required=False) first_name = forms.CharField(required=False)
last_name = forms.CharField(required=False) last_name = forms.CharField(required=False)
address = forms.CharField(required=False)
voter_id = forms.CharField(required=False, label="Voter ID") voter_id = forms.CharField(required=False, label="Voter ID")
birth_month = forms.ChoiceField(choices=MONTH_CHOICES, required=False, label="Birth Month") birth_month = forms.ChoiceField(choices=MONTH_CHOICES, required=False, label="Birth Month")
city = forms.CharField(required=False) city = forms.CharField(required=False)
@ -310,6 +312,7 @@ class VotingRecordImportForm(forms.Form):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['tenant'].widget.attrs.update({'class': 'form-control form-select'}) self.fields['tenant'].widget.attrs.update({'class': 'form-control form-select'})
self.fields['file'].widget.attrs.update({'class': 'form-control'}) self.fields['file'].widget.attrs.update({'class': 'form-control'})
class DoorVisitLogForm(forms.Form): class DoorVisitLogForm(forms.Form):
OUTCOME_CHOICES = [ OUTCOME_CHOICES = [
("No Answer Left Literature", "No Answer Left Literature"), ("No Answer Left Literature", "No Answer Left Literature"),
@ -337,6 +340,17 @@ class DoorVisitLogForm(forms.Form):
widget=forms.Select(attrs={"class": "form-select"}), widget=forms.Select(attrs={"class": "form-select"}),
label="Candidate Support" label="Candidate Support"
) )
follow_up = forms.BooleanField(
required=False,
widget=forms.CheckboxInput(attrs={"class": "form-check-input"}),
label="Follow Up"
)
follow_up_voter = forms.CharField( required=False, widget=forms.Select(attrs={"class": "form-select"}), label="Voter to Follow Up")
call_notes = forms.CharField(
widget=forms.Textarea(attrs={"class": "form-control", "rows": 2}),
required=False,
label="Call Notes"
)
class ScheduledCallForm(forms.ModelForm): class ScheduledCallForm(forms.ModelForm):
class Meta: class Meta:
@ -356,3 +370,23 @@ class ScheduledCallForm(forms.ModelForm):
for field in self.fields.values(): for field in self.fields.values():
field.widget.attrs.update({'class': 'form-control'}) field.widget.attrs.update({'class': 'form-control'})
self.fields['volunteer'].widget.attrs.update({'class': 'form-select'}) self.fields['volunteer'].widget.attrs.update({'class': 'form-select'})
class UserUpdateForm(forms.ModelForm):
class Meta:
model = User
fields = ['first_name', 'last_name', 'email']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field in self.fields.values():
field.widget.attrs.update({'class': 'form-control'})
class VolunteerProfileForm(forms.ModelForm):
class Meta:
model = Volunteer
fields = ['phone']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field in self.fields.values():
field.widget.attrs.update({'class': 'form-control'})

View File

@ -47,15 +47,27 @@
{% endif %} {% endif %}
</ul> </ul>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<a href="/admin/" class="btn btn-outline-primary btn-sm me-2">Admin Panel</a> <a href="/admin/" class="btn btn-outline-primary btn-sm me-3">Admin Panel</a>
{% if user.is_authenticated %} {% if user.is_authenticated %}
<span class="text-muted small me-3">{{ user.username }}</span> <div class="dropdown">
<form method="post" action="{% url 'logout' %}" class="d-inline"> <button class="btn btn-link nav-link dropdown-toggle text-white d-flex align-items-center p-0" type="button" id="userDropdown" data-bs-toggle="dropdown" aria-expanded="false" style="text-decoration: none;">
{% csrf_token %} <i class="bi bi-person-circle me-1"></i>
<button type="submit" class="btn btn-link nav-link d-inline p-0" style="text-decoration: none;">Logout</button> <span class="small">{{ user.username }}</span>
</form> </button>
<ul class="dropdown-menu dropdown-menu-end shadow border-0" aria-labelledby="userDropdown">
<li><a class="dropdown-item small" href="{% url 'profile' %}"><i class="bi bi-person me-2"></i>My Profile</a></li>
<li><a class="dropdown-item small" href="{% url 'password_change' %}"><i class="bi bi-shield-lock me-2"></i>Change Password</a></li>
<li><hr class="dropdown-divider"></li>
<li>
<form method="post" action="{% url 'logout' %}" class="px-3">
{% csrf_token %}
<button type="submit" class="btn btn-link dropdown-item small p-0 m-0"><i class="bi bi-box-arrow-right me-2"></i>Logout</button>
</form>
</li>
</ul>
</div>
{% else %} {% else %}
<a href="{% url 'login' %}" class="btn btn-link nav-link">Login</a> <a href="{% url 'login' %}" class="btn btn-link nav-link text-white">Login</a>
{% endif %} {% endif %}
</div> </div>
</div> </div>

View File

@ -72,7 +72,8 @@
data-address="{{ household.address_street }}" data-address="{{ household.address_street }}"
data-city="{{ household.city }}" data-city="{{ household.city }}"
data-state="{{ household.state }}" data-state="{{ household.state }}"
data-zip="{{ household.zip_code }}"> data-zip="{{ household.zip_code }}"
data-voters="{{ household.voters_json_str }}">
Log Visit Log Visit
</button> </button>
</td> </td>
@ -217,7 +218,7 @@
{{ visit_form.notes }} {{ visit_form.notes }}
</div> </div>
<div class="row g-3"> <div class="row g-3 mb-4">
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<label class="form-label fw-bold text-primary small text-uppercase">Support Status</label> <label class="form-label fw-bold text-primary small text-uppercase">Support Status</label>
{{ visit_form.candidate_support }} {{ visit_form.candidate_support }}
@ -231,6 +232,28 @@
</div> </div>
</div> </div>
</div> </div>
<hr class="my-4">
<div class="bg-light p-3 rounded-3">
<div class="form-check mb-3">
{{ visit_form.follow_up }}
<label class="form-check-label fw-bold text-dark" for="{{ visit_form.follow_up.id_for_label }}">
Schedule a Follow-up Call
</label>
</div>
<div id="callNotesContainer" style="display: none;">
<div class="mb-3">
<label for="{{ visit_form.follow_up_voter.id_for_label }}" class="form-label fw-bold text-primary small text-uppercase">Recipient of the Call</label>
{{ visit_form.follow_up_voter }}
</div>
<div class="mb-3">
<label for="{{ visit_form.call_notes.id_for_label }}" class="form-label fw-bold text-primary small text-uppercase">Call Queue Notes</label>
{{ visit_form.call_notes }}
<div class="form-text small">These notes will be added to the call queue for the default caller.</div>
</div>
</div>
</div>
</div> </div>
<div class="modal-footer bg-light border-0 py-3"> <div class="modal-footer bg-light border-0 py-3">
<button type="button" class="btn btn-outline-secondary px-4" data-bs-dismiss="modal">Cancel</button> <button type="button" class="btn btn-outline-secondary px-4" data-bs-dismiss="modal">Cancel</button>
@ -327,6 +350,31 @@
document.getElementById('modal_city').value = city; document.getElementById('modal_city').value = city;
document.getElementById('modal_state').value = state; document.getElementById('modal_state').value = state;
document.getElementById('modal_zip_code').value = zip; document.getElementById('modal_zip_code').value = zip;
// Populate voters dropdown
var votersJson = button.getAttribute('data-voters');
if (votersJson) {
var voters = JSON.parse(votersJson);
var voterSelect = document.getElementById('{{ visit_form.follow_up_voter.id_for_label }}');
if (voterSelect) {
voterSelect.innerHTML = '';
voters.forEach(function(voter) {
var option = document.createElement('option');
option.value = voter.id;
option.textContent = voter.name;
voterSelect.appendChild(option);
});
}
}
});
}
// Toggle call notes visibility
const followUpCheckbox = document.getElementById('{{ visit_form.follow_up.id_for_label }}');
const callNotesContainer = document.getElementById('callNotesContainer');
if (followUpCheckbox && callNotesContainer) {
followUpCheckbox.addEventListener('change', function() {
callNotesContainer.style.display = this.checked ? 'block' : 'none';
}); });
} }
}); });

View File

@ -0,0 +1,73 @@
{% extends "base.html" %}
{% load static %}
{% block content %}
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card border-0 shadow-sm rounded-4 overflow-hidden mb-4">
<div class="card-header bg-white border-0 py-4 px-4">
<div class="d-flex align-items-center">
<div class="bg-primary bg-opacity-10 p-3 rounded-4 me-3">
<i class="bi bi-person-circle text-primary fs-3"></i>
</div>
<div>
<h2 class="h4 fw-bold mb-0">My Profile</h2>
<p class="text-muted small mb-0">Manage your account information</p>
</div>
</div>
</div>
<div class="card-body p-4">
<form method="post">
{% csrf_token %}
<h5 class="fw-bold mb-3">User Information</h5>
<div class="row g-3 mb-4">
<div class="col-md-6">
<label class="form-label small fw-bold text-muted">First Name</label>
{{ u_form.first_name }}
{% if u_form.first_name.errors %}
<div class="text-danger small">{{ u_form.first_name.errors }}</div>
{% endif %}
</div>
<div class="col-md-6">
<label class="form-label small fw-bold text-muted">Last Name</label>
{{ u_form.last_name }}
{% if u_form.last_name.errors %}
<div class="text-danger small">{{ u_form.last_name.errors }}</div>
{% endif %}
</div>
<div class="col-12">
<label class="form-label small fw-bold text-muted">Email Address</label>
{{ u_form.email }}
{% if u_form.email.errors %}
<div class="text-danger small">{{ u_form.email.errors }}</div>
{% endif %}
</div>
</div>
{% if v_form %}
<hr class="my-4 opacity-10">
<h5 class="fw-bold mb-3">Volunteer Details</h5>
<div class="row g-3 mb-4">
<div class="col-md-12">
<label class="form-label small fw-bold text-muted">Phone Number</label>
{{ v_form.phone }}
{% if v_form.phone.errors %}
<div class="text-danger small">{{ v_form.phone.errors }}</div>
{% endif %}
</div>
</div>
{% endif %}
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary px-4 rounded-3">Save Changes</button>
<a href="{% url 'password_change' %}" class="btn btn-outline-secondary px-4 rounded-3">Change Password</a>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,5 +1,35 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block head %}
<!-- Tom Select CSS/JS -->
<link href="https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/css/tom-select.bootstrap5.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/js/tom-select.complete.min.js"></script>
<style>
.ts-control {
border: 1px solid #dee2e6 !important;
padding: 0.5rem 0.75rem !important;
border-radius: 0.375rem !important;
box-shadow: none !important;
}
.ts-control .item {
background: #e9ecef !important;
border: 1px solid #dee2e6 !important;
color: #212529 !important;
border-radius: 4px !important;
padding: 2px 8px !important;
}
.ts-dropdown {
border-radius: 0.375rem !important;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
}
.ts-control.focus {
border-color: #0d6efd !important;
outline: 0 !important;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.1) !important;
}
</style>
{% endblock %}
{% block content %} {% block content %}
<div class="container py-5"> <div class="container py-5">
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
@ -16,17 +46,16 @@
<input type="text" name="q" class="form-control" placeholder="Search by name or email..." value="{{ query|default:'' }}"> <input type="text" name="q" class="form-control" placeholder="Search by name or email..." value="{{ query|default:'' }}">
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<select name="interest" class="form-select"> <select name="interest" id="interest-filter" class="form-select" multiple>
<option value="">All Interests</option>
{% for interest in interests %} {% for interest in interests %}
<option value="{{ interest.id }}" {% if selected_interest == interest.id|stringformat:"s" %}selected{% endif %}> <option value="{{ interest.id }}" {% if interest.id|stringformat:"s" in selected_interests %}selected{% endif %}>
{{ interest.name }} {{ interest.name }}
</option> </option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<div class="col-md-2"> <div class="col-md-2">
<button type="submit" class="btn btn-primary w-100">Filter</button> <button type="submit" class="btn btn-primary w-100 h-100">Filter</button>
</div> </div>
</form> </form>
</div> </div>
@ -110,12 +139,12 @@
<ul class="pagination justify-content-center mb-0"> <ul class="pagination justify-content-center mb-0">
{% if volunteers.has_previous %} {% if volunteers.has_previous %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="?page=1{% if query %}&q={{ query }}{% endif %}{% if selected_interest %}&interest={{ selected_interest }}{% endif %}" aria-label="First"> <a class="page-link" href="?page=1{% if query %}&q={{ query }}{% endif %}{% for i in selected_interests %}&interest={{ i }}{% endfor %}" aria-label="First">
<span aria-hidden="true">&laquo;&laquo;</span> <span aria-hidden="true">&laquo;&laquo;</span>
</a> </a>
</li> </li>
<li class="page-item"> <li class="page-item">
<a class="page-link" href="?page={{ volunteers.previous_page_number }}{% if query %}&q={{ query }}{% endif %}{% if selected_interest %}&interest={{ selected_interest }}{% endif %}" aria-label="Previous"> <a class="page-link" href="?page={{ volunteers.previous_page_number }}{% if query %}&q={{ query }}{% endif %}{% for i in selected_interests %}&interest={{ i }}{% endfor %}" aria-label="Previous">
<span aria-hidden="true">&laquo;</span> <span aria-hidden="true">&laquo;</span>
</a> </a>
</li> </li>
@ -125,12 +154,12 @@
{% if volunteers.has_next %} {% if volunteers.has_next %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="?page={{ volunteers.next_page_number }}{% if query %}&q={{ query }}{% endif %}{% if selected_interest %}&interest={{ selected_interest }}{% endif %}" aria-label="Next"> <a class="page-link" href="?page={{ volunteers.next_page_number }}{% if query %}&q={{ query }}{% endif %}{% for i in selected_interests %}&interest={{ i }}{% endfor %}" aria-label="Next">
<span aria-hidden="true">&raquo;</span> <span aria-hidden="true">&raquo;</span>
</a> </a>
</li> </li>
<li class="page-item"> <li class="page-item">
<a class="page-link" href="?page={{ volunteers.paginator.num_pages }}{% if query %}&q={{ query }}{% endif %}{% if selected_interest %}&interest={{ selected_interest }}{% endif %}" aria-label="Last"> <a class="page-link" href="?page={{ volunteers.paginator.num_pages }}{% if query %}&q={{ query }}{% endif %}{% for i in selected_interests %}&interest={{ i }}{% endfor %}" aria-label="Last">
<span aria-hidden="true">&raquo;&raquo;</span> <span aria-hidden="true">&raquo;&raquo;</span>
</a> </a>
</li> </li>
@ -173,6 +202,13 @@
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Initialize Tom Select for interest filter
new TomSelect('#interest-filter', {
plugins: ['remove_button'],
placeholder: 'All Interests',
allowEmptyOption: true,
});
const selectAll = document.getElementById('select-all'); const selectAll = document.getElementById('select-all');
const checkboxes = document.querySelectorAll('.volunteer-checkbox'); const checkboxes = document.querySelectorAll('.volunteer-checkbox');
const bulkActions = document.getElementById('bulk-actions'); const bulkActions = document.getElementById('bulk-actions');
@ -223,4 +259,4 @@ document.addEventListener('DOMContentLoaded', function() {
} }
}); });
</script> </script>
{% endblock %} {% endblock %}

View File

@ -28,6 +28,10 @@
<label class="form-label small fw-bold text-muted">Voter ID</label> <label class="form-label small fw-bold text-muted">Voter ID</label>
{{ form.voter_id }} {{ form.voter_id }}
</div> </div>
<div class="col-md-4">
<label class="form-label small fw-bold text-muted">Address</label>
{{ form.address }}
</div>
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label small fw-bold text-muted">Birth Month</label> <label class="form-label small fw-bold text-muted">Birth Month</label>
{{ form.birth_month }} {{ form.birth_month }}

View File

@ -0,0 +1,21 @@
{% extends "base.html" %}
{% load static %}
{% block content %}
<div class="container py-5 text-center">
<div class="row justify-content-center">
<div class="col-lg-6">
<div class="card border-0 shadow-sm rounded-4 py-5 px-4">
<div class="bg-success bg-opacity-10 d-inline-block p-4 rounded-circle mb-4 mx-auto" style="width: 100px; height: 100px;">
<i class="bi bi-check-lg text-success fs-1"></i>
</div>
<h2 class="h3 fw-bold mb-3">Password Changed!</h2>
<p class="text-muted mb-4">Your password has been successfully updated. You can now use your new password to log in.</p>
<div>
<a href="{% url 'profile' %}" class="btn btn-primary px-5 py-2 rounded-3">Back to Profile</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,53 @@
{% extends "base.html" %}
{% load static %}
{% block content %}
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-lg-6">
<div class="card border-0 shadow-sm rounded-4 overflow-hidden">
<div class="card-header bg-white border-0 py-4 px-4 text-center">
<div class="bg-primary bg-opacity-10 d-inline-block p-3 rounded-4 mb-3">
<i class="bi bi-shield-lock text-primary fs-3"></i>
</div>
<h2 class="h4 fw-bold mb-0">Change Password</h2>
<p class="text-muted small mb-0">Secure your account by updating your password</p>
</div>
<div class="card-body p-4">
<form method="post">
{% csrf_token %}
{% for field in form %}
<div class="mb-3">
<label for="{{ field.id_for_label }}" class="form-label small fw-bold text-muted">{{ field.label }}</label>
{{ field }}
{% if field.help_text %}
<div class="form-text small">{{ field.help_text }}</div>
{% endif %}
{% for error in field.errors %}
<div class="text-danger small">{{ error }}</div>
{% endfor %}
</div>
{% endfor %}
<div class="d-grid gap-2 mt-4">
<button type="submit" class="btn btn-primary py-2 rounded-3">Update Password</button>
<a href="{% url 'profile' %}" class="btn btn-light py-2 rounded-3">Cancel</a>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<style>
/* Styling for password help text lists */
.form-text ul {
padding-left: 1rem;
margin-top: 0.5rem;
}
input.form-control {
border-radius: 0.75rem;
padding: 0.75rem 1rem;
}
</style>
{% endblock %}

View File

@ -64,4 +64,5 @@ urlpatterns = [
path('call-queue/', views.call_queue, name='call_queue'), path('call-queue/', views.call_queue, name='call_queue'),
path('call-queue/<int:call_id>/complete/', views.complete_call, name='complete_call'), path('call-queue/<int:call_id>/complete/', views.complete_call, name='complete_call'),
path('call-queue/<int:call_id>/delete/', views.delete_call, name='delete_call'), path('call-queue/<int:call_id>/delete/', views.delete_call, name='delete_call'),
path('profile/', views.profile, name='profile'),
] ]

View File

@ -1,3 +1,5 @@
from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import PasswordChangeForm
from django.utils.dateparse import parse_date from django.utils.dateparse import parse_date
from datetime import datetime, time, timedelta from datetime import datetime, time, timedelta
import base64 import base64
@ -15,7 +17,7 @@ from django.contrib import messages
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.conf import settings from django.conf import settings
from .models import Voter, Tenant, Interaction, Donation, VoterLikelihood, EventParticipation, Event, EventType, InteractionType, DonationMethod, ElectionType, CampaignSettings, Volunteer, ParticipationStatus, VolunteerEvent, Interest, VolunteerRole, ScheduledCall from .models import Voter, Tenant, Interaction, Donation, VoterLikelihood, EventParticipation, Event, EventType, InteractionType, DonationMethod, ElectionType, CampaignSettings, Volunteer, ParticipationStatus, VolunteerEvent, Interest, VolunteerRole, ScheduledCall
from .forms import VoterForm, InteractionForm, DonationForm, VoterLikelihoodForm, EventParticipationForm, VoterImportForm, AdvancedVoterSearchForm, EventParticipantAddForm, EventForm, VolunteerForm, VolunteerEventForm, VolunteerEventAddForm, DoorVisitLogForm, ScheduledCallForm from .forms import VoterForm, InteractionForm, DonationForm, VoterLikelihoodForm, EventParticipationForm, VoterImportForm, AdvancedVoterSearchForm, EventParticipantAddForm, EventForm, VolunteerForm, VolunteerEventForm, VolunteerEventAddForm, DoorVisitLogForm, ScheduledCallForm, UserUpdateForm, VolunteerProfileForm
import logging import logging
import zoneinfo import zoneinfo
from django.utils import timezone from django.utils import timezone
@ -446,6 +448,8 @@ def voter_advanced_search(request):
voters = voters.filter(first_name__icontains=data['first_name']) voters = voters.filter(first_name__icontains=data['first_name'])
if data.get('last_name'): if data.get('last_name'):
voters = voters.filter(last_name__icontains=data['last_name']) voters = voters.filter(last_name__icontains=data['last_name'])
if data.get('address'):
voters = voters.filter(Q(address__icontains=data['address']) | Q(address_street__icontains=data['address']))
if data.get('voter_id'): if data.get('voter_id'):
voters = voters.filter(voter_id__icontains=data['voter_id']) voters = voters.filter(voter_id__icontains=data['voter_id'])
if data.get('birth_month'): if data.get('birth_month'):
@ -520,6 +524,8 @@ def export_voters_csv(request):
voters = voters.filter(first_name__icontains=data['first_name']) voters = voters.filter(first_name__icontains=data['first_name'])
if data.get('last_name'): if data.get('last_name'):
voters = voters.filter(last_name__icontains=data['last_name']) voters = voters.filter(last_name__icontains=data['last_name'])
if data.get('address'):
voters = voters.filter(Q(address__icontains=data['address']) | Q(address_street__icontains=data['address']))
if data.get('voter_id'): if data.get('voter_id'):
voters = voters.filter(voter_id__icontains=data['voter_id']) voters = voters.filter(voter_id__icontains=data['voter_id'])
if data.get('birth_month'): if data.get('birth_month'):
@ -580,7 +586,7 @@ def voter_delete(request, voter_id):
return redirect('voter_detail', voter_id=voter.id) return redirect('voter_detail', voter_id=voter.id)
@role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin']) @role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin'], permission='core.view_voter')
def bulk_send_sms(request): def bulk_send_sms(request):
""" """
Sends bulk SMS to selected voters using Twilio API. Sends bulk SMS to selected voters using Twilio API.
@ -839,23 +845,23 @@ def volunteer_list(request):
) )
# Interest filter # Interest filter
interest_id = request.GET.get("interest") interest_ids = request.GET.getlist("interest")
if interest_id: if interest_ids:
volunteers = volunteers.filter(interests__id=interest_id) volunteers = volunteers.filter(interests__id__in=interest_ids).distinct()
interests = Interest.objects.filter(tenant=tenant).order_by('name')
paginator = Paginator(volunteers, 50) paginator = Paginator(volunteers, 50)
page_number = request.GET.get('page') page_number = request.GET.get('page')
volunteers_page = paginator.get_page(page_number) volunteers_page = paginator.get_page(page_number)
interests = Interest.objects.filter(tenant=tenant).order_by('name')
context = { context = {
'tenant': tenant, 'tenant': tenant,
'selected_tenant': tenant, 'selected_tenant': tenant,
'volunteers': volunteers_page, 'volunteers': volunteers_page,
'query': query, 'query': query,
'interests': interests, 'interests': interests,
'selected_interest': interest_id, 'selected_interests': interest_ids,
} }
return render(request, 'core/volunteer_list.html', context) return render(request, 'core/volunteer_list.html', context)
@ -1113,7 +1119,7 @@ def event_remove_volunteer(request, assignment_id):
messages.success(request, f"{volunteer_name} removed from event volunteers.") messages.success(request, f"{volunteer_name} removed from event volunteers.")
return redirect('event_detail', event_id=event_id) return redirect('event_detail', event_id=event_id)
@role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin']) @role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin'], permission='core.view_volunteer')
def volunteer_bulk_send_sms(request): def volunteer_bulk_send_sms(request):
""" """
Sends bulk SMS to selected volunteers using Twilio API. Sends bulk SMS to selected volunteers using Twilio API.
@ -1196,6 +1202,7 @@ def volunteer_bulk_send_sms(request):
return redirect('volunteer_list') return redirect('volunteer_list')
@role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin'], permission='core.view_voter') @role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin'], permission='core.view_voter')
def door_visits(request): def door_visits(request):
""" """
Manage door knocking visits. Groups unvisited targeted voters by household. Manage door knocking visits. Groups unvisited targeted voters by household.
@ -1253,11 +1260,16 @@ def door_visits(request):
'longitude': float(voter.longitude) if voter.longitude else None, 'longitude': float(voter.longitude) if voter.longitude else None,
'street_name_sort': street_name.lower(), 'street_name_sort': street_name.lower(),
'street_number_sort': street_number_sort, 'street_number_sort': street_number_sort,
'target_voters': [] 'target_voters': [],
'voters_json': []
} }
households_dict[key]['target_voters'].append(voter) households_dict[key]['target_voters'].append(voter)
households_dict[key]['voters_json'].append({'id': voter.id, 'name': f"{voter.first_name} {voter.last_name}"})
households_list = list(households_dict.values()) households_list = list(households_dict.values())
for h in households_list:
h['voters_json_str'] = json.dumps(h['voters_json'])
households_list.sort(key=lambda x: ( households_list.sort(key=lambda x: (
(x['neighborhood'] or '').lower(), (x['neighborhood'] or '').lower(),
x['street_name_sort'], x['street_name_sort'],
@ -1299,39 +1311,42 @@ def log_door_visit(request):
""" """
selected_tenant_id = request.session.get("tenant_id") selected_tenant_id = request.session.get("tenant_id")
if not selected_tenant_id: if not selected_tenant_id:
return redirect('index') return redirect("index")
tenant = get_object_or_404(Tenant, id=selected_tenant_id) tenant = get_object_or_404(Tenant, id=selected_tenant_id)
campaign_settings, _ = CampaignSettings.objects.get_or_create(tenant=tenant) campaign_settings, _ = CampaignSettings.objects.get_or_create(tenant=tenant)
# Capture query string for redirecting back with filters # Capture query string for redirecting back with filters
next_qs = request.POST.get('next_query_string', '') next_qs = request.POST.get("next_query_string", "")
redirect_url = reverse('door_visits') redirect_url = reverse("door_visits")
if next_qs: if next_qs:
redirect_url += f"?{next_qs}" redirect_url += f"?{next_qs}"
# Get the volunteer linked to the current user # Get the volunteer linked to the current user
volunteer = Volunteer.objects.filter(user=request.user, tenant=tenant).first() volunteer = Volunteer.objects.filter(user=request.user, tenant=tenant).first()
if request.method == 'POST': if request.method == "POST":
form = DoorVisitLogForm(request.POST) form = DoorVisitLogForm(request.POST)
if form.is_valid(): if form.is_valid():
address_street = request.POST.get('address_street') address_street = request.POST.get("address_street")
city = request.POST.get('city') city = request.POST.get("city")
state = request.POST.get('state') state = request.POST.get("state")
zip_code = request.POST.get('zip_code') zip_code = request.POST.get("zip_code")
outcome = form.cleaned_data['outcome'] outcome = form.cleaned_data["outcome"]
notes = form.cleaned_data['notes'] notes = form.cleaned_data["notes"]
wants_yard_sign = form.cleaned_data['wants_yard_sign'] wants_yard_sign = form.cleaned_data["wants_yard_sign"]
candidate_support = form.cleaned_data['candidate_support'] candidate_support = form.cleaned_data["candidate_support"]
follow_up = form.cleaned_data["follow_up"]
follow_up_voter_id = form.cleaned_data.get("follow_up_voter")
call_notes = form.cleaned_data["call_notes"]
# Determine date/time in campaign timezone # Determine date/time in campaign timezone
campaign_tz_name = campaign_settings.timezone or 'America/Chicago' campaign_tz_name = campaign_settings.timezone or "America/Chicago"
try: try:
tz = zoneinfo.ZoneInfo(campaign_tz_name) tz = zoneinfo.ZoneInfo(campaign_tz_name)
except: except:
tz = zoneinfo.ZoneInfo('America/Chicago') tz = zoneinfo.ZoneInfo("America/Chicago")
interaction_date = timezone.now().astimezone(tz) interaction_date = timezone.now().astimezone(tz)
@ -1352,16 +1367,21 @@ def log_door_visit(request):
messages.warning(request, f"No targeted voters found at {address_street}.") messages.warning(request, f"No targeted voters found at {address_street}.")
return redirect(redirect_url) return redirect(redirect_url)
# Get default caller for follow-ups
default_caller = None
if follow_up:
default_caller = Volunteer.objects.filter(tenant=tenant, is_default_caller=True).first()
for voter in voters: for voter in voters:
# 1) Update voter flags # 1) Update voter flags
voter.door_visit = True voter.door_visit = True
# 2) If "Wants a Yard Sign" checkbox is selected # 2) If "Wants a Yard Sign" checkbox is selected
if wants_yard_sign: if wants_yard_sign:
voter.yard_sign = 'wants' voter.yard_sign = "wants"
# 3) Update support status if Supporting or Not Supporting # 3) Update support status if Supporting or Not Supporting
if candidate_support in ['supporting', 'not_supporting']: if candidate_support in ["supporting", "not_supporting"]:
voter.candidate_support = candidate_support voter.candidate_support = candidate_support
voter.save() voter.save()
@ -1375,14 +1395,26 @@ def log_door_visit(request):
description=outcome, description=outcome,
notes=notes notes=notes
) )
# 5) Create ScheduledCall if follow_up is checked and this is the selected voter
if follow_up and follow_up_voter_id and str(voter.id) == follow_up_voter_id:
ScheduledCall.objects.create(
tenant=tenant,
voter=voter,
volunteer=default_caller,
comments=call_notes,
status="pending"
)
messages.success(request, f"Door visit logged for {address_street}.") if follow_up:
messages.success(request, f"Door visit logged and follow-up call scheduled for {address_street}.")
else:
messages.success(request, f"Door visit logged for {address_street}.")
else: else:
messages.error(request, "There was an error in the visit log form.") messages.error(request, "There was an error in the visit log form.")
return redirect(redirect_url) return redirect(redirect_url)
@role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin'], permission='core.view_voter')
def door_visit_history(request): def door_visit_history(request):
""" """
Shows a distinct list of Door visit interactions for addresses. Shows a distinct list of Door visit interactions for addresses.
@ -1474,7 +1506,7 @@ def door_visit_history(request):
} }
return render(request, "core/door_visit_history.html", context) return render(request, "core/door_visit_history.html", context)
@role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin']) @role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin'], permission='core.add_scheduledcall')
def schedule_call(request, voter_id): def schedule_call(request, voter_id):
selected_tenant_id = request.session.get("tenant_id") selected_tenant_id = request.session.get("tenant_id")
tenant = get_object_or_404(Tenant, id=selected_tenant_id) tenant = get_object_or_404(Tenant, id=selected_tenant_id)
@ -1496,7 +1528,7 @@ def schedule_call(request, voter_id):
return redirect(referer) return redirect(referer)
return redirect('voter_detail', voter_id=voter.id) return redirect('voter_detail', voter_id=voter.id)
@role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin']) @role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin'], permission='core.add_scheduledcall')
def bulk_schedule_calls(request): def bulk_schedule_calls(request):
if request.method != 'POST': if request.method != 'POST':
return redirect('voter_advanced_search') return redirect('voter_advanced_search')
@ -1530,7 +1562,7 @@ def bulk_schedule_calls(request):
messages.success(request, f"{count} calls added to queue.") messages.success(request, f"{count} calls added to queue.")
return redirect(request.META.get('HTTP_REFERER', 'voter_advanced_search')) return redirect(request.META.get('HTTP_REFERER', 'voter_advanced_search'))
@role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin']) @role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin'], permission='core.view_scheduledcall')
def call_queue(request): def call_queue(request):
selected_tenant_id = request.session.get("tenant_id") selected_tenant_id = request.session.get("tenant_id")
if not selected_tenant_id: if not selected_tenant_id:
@ -1550,8 +1582,7 @@ def call_queue(request):
} }
return render(request, 'core/call_queue.html', context) return render(request, 'core/call_queue.html', context)
@role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin']) @role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin'], permission='core.change_scheduledcall')
@role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin'])
def complete_call(request, call_id): def complete_call(request, call_id):
selected_tenant_id = request.session.get("tenant_id") selected_tenant_id = request.session.get("tenant_id")
tenant = get_object_or_404(Tenant, id=selected_tenant_id) tenant = get_object_or_404(Tenant, id=selected_tenant_id)
@ -1589,7 +1620,7 @@ def complete_call(request, call_id):
return redirect('call_queue') return redirect('call_queue')
@role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin']) @role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin'], permission='core.delete_scheduledcall')
def delete_call(request, call_id): def delete_call(request, call_id):
selected_tenant_id = request.session.get("tenant_id") selected_tenant_id = request.session.get("tenant_id")
tenant = get_object_or_404(Tenant, id=selected_tenant_id) tenant = get_object_or_404(Tenant, id=selected_tenant_id)
@ -1600,3 +1631,31 @@ def delete_call(request, call_id):
messages.success(request, "Call removed from queue.") messages.success(request, "Call removed from queue.")
return redirect('call_queue') return redirect('call_queue')
@login_required
def profile(request):
try:
volunteer = request.user.volunteer_profile
except:
volunteer = None
if request.method == 'POST':
u_form = UserUpdateForm(request.POST, instance=request.user)
v_form = VolunteerProfileForm(request.POST, instance=volunteer) if volunteer else None
if u_form.is_valid() and (not v_form or v_form.is_valid()):
u_form.save()
if v_form:
v_form.save()
messages.success(request, f'Your profile has been updated!')
return redirect('profile')
else:
u_form = UserUpdateForm(instance=request.user)
v_form = VolunteerProfileForm(instance=volunteer) if volunteer else None
context = {
'u_form': u_form,
'v_form': v_form
}
return render(request, 'core/profile.html', context)

111
core/views_new.py Normal file
View File

@ -0,0 +1,111 @@
@role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin'], permission='core.view_voter')
def log_door_visit(request):
"""
Mark all targeted voters at a specific address as visited, update their flags,
and create interaction records.
"""
selected_tenant_id = request.session.get("tenant_id")
if not selected_tenant_id:
return redirect("index")
tenant = get_object_or_404(Tenant, id=selected_tenant_id)
campaign_settings, _ = CampaignSettings.objects.get_or_create(tenant=tenant)
# Capture query string for redirecting back with filters
next_qs = request.POST.get("next_query_string", "")
redirect_url = reverse("door_visits")
if next_qs:
redirect_url += f"?{next_qs}"
# Get the volunteer linked to the current user
volunteer = Volunteer.objects.filter(user=request.user, tenant=tenant).first()
if request.method == "POST":
form = DoorVisitLogForm(request.POST)
if form.is_valid():
address_street = request.POST.get("address_street")
city = request.POST.get("city")
state = request.POST.get("state")
zip_code = request.POST.get("zip_code")
outcome = form.cleaned_data["outcome"]
notes = form.cleaned_data["notes"]
wants_yard_sign = form.cleaned_data["wants_yard_sign"]
candidate_support = form.cleaned_data["candidate_support"]
follow_up = form.cleaned_data["follow_up"]
follow_up_voter_id = form.cleaned_data.get("follow_up_voter")
call_notes = form.cleaned_data["call_notes"]
# Determine date/time in campaign timezone
campaign_tz_name = campaign_settings.timezone or "America/Chicago"
try:
tz = zoneinfo.ZoneInfo(campaign_tz_name)
except:
tz = zoneinfo.ZoneInfo("America/Chicago")
interaction_date = timezone.now().astimezone(tz)
# Get or create InteractionType
interaction_type, _ = InteractionType.objects.get_or_create(tenant=tenant, name="Door Visit")
# Find targeted voters at this exact address
voters = Voter.objects.filter(
tenant=tenant,
address_street=address_street,
city=city,
state=state,
zip_code=zip_code,
is_targeted=True
)
if not voters.exists():
messages.warning(request, f"No targeted voters found at {address_street}.")
return redirect(redirect_url)
# Get default caller for follow-ups
default_caller = None
if follow_up:
default_caller = Volunteer.objects.filter(tenant=tenant, is_default_caller=True).first()
for voter in voters:
# 1) Update voter flags
voter.door_visit = True
# 2) If "Wants a Yard Sign" checkbox is selected
if wants_yard_sign:
voter.yard_sign = "wants"
# 3) Update support status if Supporting or Not Supporting
if candidate_support in ["supporting", "not_supporting"]:
voter.candidate_support = candidate_support
voter.save()
# 4) Create interaction
Interaction.objects.create(
voter=voter,
volunteer=volunteer,
type=interaction_type,
date=interaction_date,
description=outcome,
notes=notes
)
# 5) Create ScheduledCall if follow_up is checked and this is the selected voter
if follow_up and follow_up_voter_id and str(voter.id) == follow_up_voter_id:
ScheduledCall.objects.create(
tenant=tenant,
voter=voter,
volunteer=default_caller,
comments=call_notes,
status="pending"
)
if follow_up:
messages.success(request, f"Door visit logged and follow-up call scheduled for {address_street}.")
else:
messages.success(request, f"Door visit logged for {address_street}.")
else:
messages.error(request, "There was an error in the visit log form.")
return redirect(redirect_url)

211
door_views_update.py Normal file
View File

@ -0,0 +1,211 @@
def door_visits(request):
"""
Manage door knocking visits. Groups unvisited targeted voters by household.
"""
selected_tenant_id = request.session.get("tenant_id")
if not selected_tenant_id:
messages.warning(request, "Please select a campaign first.")
return redirect("index")
tenant = get_object_or_404(Tenant, id=selected_tenant_id)
# Filters from GET parameters
district_filter = request.GET.get('district', '').strip()
neighborhood_filter = request.GET.get('neighborhood', '').strip()
address_filter = request.GET.get('address', '').strip()
# Initial queryset: unvisited targeted voters for this tenant
voters = Voter.objects.filter(tenant=tenant, door_visit=False, is_targeted=True)
# Apply filters if provided
if district_filter:
voters = voters.filter(district=district_filter)
if neighborhood_filter:
voters = voters.filter(neighborhood__icontains=neighborhood_filter)
if address_filter:
voters = voters.filter(Q(address_street__icontains=address_filter) | Q(address__icontains=address_filter))
# Grouping by household (unique address)
households_dict = {}
for voter in voters:
# Key for grouping is the unique address components
key = (voter.address_street, voter.city, voter.state, voter.zip_code)
if key not in households_dict:
# Parse street name and number for sorting
street_number = ""
street_name = voter.address_street
match = re.match(r'^(\d+)\s+(.*)$', voter.address_street)
if match:
street_number = match.group(1)
street_name = match.group(2)
try:
street_number_sort = int(street_number)
except ValueError:
street_number_sort = 0
households_dict[key] = {
'address_street': voter.address_street,
'city': voter.city,
'state': voter.state,
'zip_code': voter.zip_code,
'neighborhood': voter.neighborhood,
'district': voter.district,
'latitude': float(voter.latitude) if voter.latitude else None,
'longitude': float(voter.longitude) if voter.longitude else None,
'street_name_sort': street_name.lower(),
'street_number_sort': street_number_sort,
'target_voters': [],
'voters_json': []
}
households_dict[key]['target_voters'].append(voter)
households_dict[key]['voters_json'].append({'id': voter.id, 'name': f"{voter.first_name} {voter.last_name}"})
households_list = list(households_dict.values())
for h in households_list:
h['voters_json_str'] = json.dumps(h['voters_json'])
households_list.sort(key=lambda x: (
(x['neighborhood'] or '').lower(),
x['street_name_sort'],
x['street_number_sort']
))
# Prepare data for Google Map (all filtered households with coordinates)
map_data = [
{
'lat': h['latitude'],
'lng': h['longitude'],
'address': f"{h['address_street']}, {h['city']}, {h['state']}",
'voters': ", ".join([f"{v.first_name} {v.last_name}" for v in h['target_voters']])
}
for h in households_list if h['latitude'] and h['longitude']
]
paginator = Paginator(households_list, 50)
page_number = request.GET.get('page')
households_page = paginator.get_page(page_number)
context = {
'selected_tenant': tenant,
'households': households_page,
'district_filter': district_filter,
'neighborhood_filter': neighborhood_filter,
'address_filter': address_filter,
'map_data_json': json.dumps(map_data),
'google_maps_api_key': getattr(settings, 'GOOGLE_MAPS_API_KEY', ''),
'visit_form': DoorVisitLogForm(),
}
return render(request, 'core/door_visits.html', context)
@role_required(['admin', 'campaign_manager', 'campaign_staff', 'system_admin', 'campaign_admin'], permission='core.view_voter')
def log_door_visit(request):
"""
Mark all targeted voters at a specific address as visited, update their flags,
and create interaction records.
"""
selected_tenant_id = request.session.get("tenant_id")
if not selected_tenant_id:
return redirect("index")
tenant = get_object_or_404(Tenant, id=selected_tenant_id)
campaign_settings, _ = CampaignSettings.objects.get_or_create(tenant=tenant)
# Capture query string for redirecting back with filters
next_qs = request.POST.get("next_query_string", "")
redirect_url = reverse("door_visits")
if next_qs:
redirect_url += f"?{next_qs}"
# Get the volunteer linked to the current user
volunteer = Volunteer.objects.filter(user=request.user, tenant=tenant).first()
if request.method == "POST":
form = DoorVisitLogForm(request.POST)
if form.is_valid():
address_street = request.POST.get("address_street")
city = request.POST.get("city")
state = request.POST.get("state")
zip_code = request.POST.get("zip_code")
outcome = form.cleaned_data["outcome"]
notes = form.cleaned_data["notes"]
wants_yard_sign = form.cleaned_data["wants_yard_sign"]
candidate_support = form.cleaned_data["candidate_support"]
follow_up = form.cleaned_data["follow_up"]
follow_up_voter_id = form.cleaned_data.get("follow_up_voter")
call_notes = form.cleaned_data["call_notes"]
# Determine date/time in campaign timezone
campaign_tz_name = campaign_settings.timezone or "America/Chicago"
try:
tz = zoneinfo.ZoneInfo(campaign_tz_name)
except:
tz = zoneinfo.ZoneInfo("America/Chicago")
interaction_date = timezone.now().astimezone(tz)
# Get or create InteractionType
interaction_type, _ = InteractionType.objects.get_or_create(tenant=tenant, name="Door Visit")
# Find targeted voters at this exact address
voters = Voter.objects.filter(
tenant=tenant,
address_street=address_street,
city=city,
state=state,
zip_code=zip_code,
is_targeted=True
)
if not voters.exists():
messages.warning(request, f"No targeted voters found at {address_street}.")
return redirect(redirect_url)
# Get default caller for follow-ups
default_caller = None
if follow_up:
default_caller = Volunteer.objects.filter(tenant=tenant, is_default_caller=True).first()
for voter in voters:
# 1) Update voter flags
voter.door_visit = True
# 2) If "Wants a Yard Sign" checkbox is selected
if wants_yard_sign:
voter.yard_sign = "wants"
# 3) Update support status if Supporting or Not Supporting
if candidate_support in ["supporting", "not_supporting"]:
voter.candidate_support = candidate_support
voter.save()
# 4) Create interaction
Interaction.objects.create(
voter=voter,
volunteer=volunteer,
type=interaction_type,
date=interaction_date,
description=outcome,
notes=notes
)
# 5) Create ScheduledCall if follow_up is checked and this is the selected voter
if follow_up and follow_up_voter_id and str(voter.id) == follow_up_voter_id:
ScheduledCall.objects.create(
tenant=tenant,
voter=voter,
volunteer=default_caller,
comments=call_notes,
status="pending"
)
if follow_up:
messages.success(request, f"Door visit logged and follow-up call scheduled for {address_street}.")
else:
messages.success(request, f"Door visit logged for {address_street}.")
else:
messages.error(request, "There was an error in the visit log form.")
return redirect(redirect_url)

View File

@ -22,7 +22,6 @@ h1, h2, h3, h4, h5, h6 {
} }
.navbar { .navbar {
background-color: #ffffff;
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
padding: 1rem 0; padding: 1rem 0;
} }

View File

@ -22,7 +22,6 @@ h1, h2, h3, h4, h5, h6 {
} }
.navbar { .navbar {
background-color: #ffffff;
border-bottom: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color);
padding: 1rem 0; padding: 1rem 0;
} }