Autosave: 20260203-184221
This commit is contained in:
parent
ac80c84fbd
commit
bf2d558e03
Binary file not shown.
@ -12,15 +12,18 @@ Class-based views
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
from django.views.i18n import JavaScriptCatalog
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
from django.views.i18n import JavaScriptCatalog
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
path("jsi18n/", JavaScriptCatalog.as_view(), name="jsi18n"),
|
||||
path("", include("core.urls")),
|
||||
path("accounts/", include("django.contrib.auth.urls")),
|
||||
]
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -1,6 +1,17 @@
|
||||
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 .permissions import get_user_role
|
||||
|
||||
class Select2MultipleWidget(forms.SelectMultiple):
|
||||
"""
|
||||
Custom widget to mark fields for Select2 initialization in the template.
|
||||
"""
|
||||
def __init__(self, attrs=None, choices=()):
|
||||
default_attrs = {"multiple": "multiple"}
|
||||
if attrs:
|
||||
default_attrs.update(attrs)
|
||||
super().__init__(attrs=default_attrs, choices=choices)
|
||||
|
||||
class VoterForm(forms.ModelForm):
|
||||
class Meta:
|
||||
@ -19,8 +30,30 @@ class VoterForm(forms.ModelForm):
|
||||
'notes': forms.Textarea(attrs={'rows': 3}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
def __init__(self, *args, user=None, tenant=None, **kwargs):
|
||||
self.user = user
|
||||
self.tenant = tenant
|
||||
super().__init__(*args, **kwargs)
|
||||
# Restrict fields for non-admin users
|
||||
is_admin = False
|
||||
if user:
|
||||
if user.is_superuser:
|
||||
is_admin = True
|
||||
elif tenant:
|
||||
role = get_user_role(user, tenant)
|
||||
if role in ["admin", "system_admin", "campaign_admin"]:
|
||||
is_admin = True
|
||||
|
||||
if not is_admin:
|
||||
restricted_fields = [
|
||||
"first_name", "last_name", "voter_id", "district", "precinct",
|
||||
"registration_date", "address_street", "city", "state", "zip_code"
|
||||
]
|
||||
for field_name in restricted_fields:
|
||||
if field_name in self.fields:
|
||||
self.fields[field_name].widget.attrs["readonly"] = True
|
||||
self.fields[field_name].widget.attrs["class"] = self.fields[field_name].widget.attrs.get("class", "") + " bg-light"
|
||||
|
||||
for name, field in self.fields.items():
|
||||
if name in ['latitude', 'longitude']:
|
||||
continue
|
||||
@ -35,6 +68,39 @@ class VoterForm(forms.ModelForm):
|
||||
self.fields['phone_type'].widget.attrs.update({'class': 'form-select'})
|
||||
self.fields['secondary_phone_type'].widget.attrs.update({'class': 'form-select'})
|
||||
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
|
||||
# Backend protection for restricted fields
|
||||
is_admin = False
|
||||
user = getattr(self, "user", None)
|
||||
tenant = getattr(self, "tenant", None)
|
||||
|
||||
# We need to set these on the form instance if we want to use them in clean
|
||||
# or we can pass them in __init__ and store them
|
||||
|
||||
if self.user:
|
||||
if self.user.is_superuser:
|
||||
is_admin = True
|
||||
elif self.tenant:
|
||||
from .permissions import get_user_role
|
||||
role = get_user_role(self.user, self.tenant)
|
||||
if role in ["admin", "system_admin", "campaign_admin"]:
|
||||
is_admin = True
|
||||
|
||||
if not is_admin and self.instance.pk:
|
||||
restricted_fields = [
|
||||
"first_name", "last_name", "voter_id", "district", "precinct",
|
||||
"registration_date", "address_street", "city", "state", "zip_code"
|
||||
]
|
||||
for field in restricted_fields:
|
||||
if field in self.changed_data:
|
||||
# Revert to original value
|
||||
cleaned_data[field] = getattr(self.instance, field)
|
||||
|
||||
return cleaned_data
|
||||
|
||||
class AdvancedVoterSearchForm(forms.Form):
|
||||
MONTH_CHOICES = [
|
||||
('', 'Any Month'),
|
||||
@ -259,7 +325,10 @@ class VolunteerForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Volunteer
|
||||
fields = ['first_name', 'last_name', 'email', 'phone', 'is_default_caller', 'notes', 'interests']
|
||||
widgets = {'notes': forms.Textarea(attrs={'rows': 3})}
|
||||
widgets = {
|
||||
'notes': forms.Textarea(attrs={'rows': 3}),
|
||||
'interests': Select2MultipleWidget(),
|
||||
}
|
||||
|
||||
def __init__(self, *args, tenant=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@ -271,8 +340,6 @@ class VolunteerForm(forms.ModelForm):
|
||||
field.widget.attrs.update({'class': 'form-control'})
|
||||
else:
|
||||
field.widget.attrs.update({'class': 'form-check-input'})
|
||||
|
||||
self.fields['interests'].widget.attrs.update({'class': 'form-select tom-select'})
|
||||
|
||||
class VolunteerEventForm(forms.ModelForm):
|
||||
class Meta:
|
||||
@ -389,4 +456,4 @@ class VolunteerProfileForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
for field in self.fields.values():
|
||||
field.widget.attrs.update({'class': 'form-control'})
|
||||
field.widget.attrs.update({'class': 'form-control'})
|
||||
@ -1,31 +1,45 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% 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>
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" href="{% static 'admin/css/vendor/select2/select2.min.css' %}">
|
||||
<style>
|
||||
.ts-control {
|
||||
border: 1px solid #dee2e6 !important;
|
||||
padding: 0.5rem 0.75rem !important;
|
||||
border-radius: 0.375rem !important;
|
||||
box-shadow: none !important;
|
||||
/* Select2 Bootstrap 5 compatibility or similar overrides */
|
||||
.select2-container--default .select2-selection--multiple {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
min-height: 45px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
.ts-control .item {
|
||||
background: #e9ecef !important;
|
||||
border: 1px solid #dee2e6 !important;
|
||||
color: #212529 !important;
|
||||
border-radius: 4px !important;
|
||||
padding: 2px 8px !important;
|
||||
.select2-container--default.select2-container--focus .select2-selection--multiple {
|
||||
border-color: #86b7fe;
|
||||
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
||||
}
|
||||
.ts-dropdown {
|
||||
border-radius: 0.375rem !important;
|
||||
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
|
||||
.select2-container--default .select2-selection--multiple .select2-selection__choice {
|
||||
background-color: #0d6efd;
|
||||
border: none;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
padding: 2px 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.ts-control.focus {
|
||||
border-color: #059669 !important;
|
||||
outline: 0 !important;
|
||||
box-shadow: 0 0 0 0.25rem rgba(5, 150, 105, 0.1) !important;
|
||||
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
|
||||
color: white;
|
||||
margin-right: 5px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover {
|
||||
color: #f8f9fa;
|
||||
background: transparent;
|
||||
}
|
||||
.select2-container {
|
||||
width: 100% !important;
|
||||
}
|
||||
.select2-search__field {
|
||||
margin-top: 4px !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@ -35,7 +49,7 @@
|
||||
<div class="mb-4">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'volunteer_list' %}">Volunteers</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'volunteer_list' %}" class="text-decoration-none">Volunteers</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">{% if volunteer %}{{ volunteer.first_name }} {{ volunteer.last_name }}{% else %}Add New Volunteer{% endif %}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
@ -52,13 +66,13 @@
|
||||
|
||||
<div class="row g-4">
|
||||
<!-- Volunteer Info & Edit Form -->
|
||||
<div class="col-lg-7">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<div class="card-body p-4">
|
||||
<div class="col-lg-12">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body p-4 p-md-5">
|
||||
<h5 class="card-title fw-bold mb-4">Volunteer Information</h5>
|
||||
<form action="{% if volunteer %}{% url 'volunteer_detail' volunteer.id %}{% else %}{% url 'volunteer_add' %}{% endif %}" method="POST" id="volunteerForm">
|
||||
{% csrf_token %}
|
||||
<div class="row g-3">
|
||||
<div class="row g-4">
|
||||
<div class="col-md-6">
|
||||
<label for="{{ form.first_name.id_for_label }}" class="form-label small text-muted text-uppercase fw-bold">First Name</label>
|
||||
{{ form.first_name }}
|
||||
@ -92,8 +106,8 @@
|
||||
<div class="form-text">If enabled, this volunteer will be the default assigned person for new call queue entries.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<div class="col-12 mt-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<label for="{{ form.interests.id_for_label }}" class="form-label small text-muted text-uppercase fw-bold mb-0">Interests</label>
|
||||
<div>
|
||||
<button type="button" class="btn btn-sm btn-link text-decoration-none py-0 me-2" data-bs-toggle="modal" data-bs-target="#manageInterestsModal">
|
||||
@ -104,14 +118,17 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{ form.interests }}
|
||||
<div class="select2-container-wrapper">
|
||||
{{ form.interests }}
|
||||
</div>
|
||||
<div class="form-text small text-muted mt-2">Search and select multiple interest types for this volunteer.</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label for="{{ form.notes.id_for_label }}" class="form-label small text-muted text-uppercase fw-bold">Notes</label>
|
||||
{{ form.notes }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 pt-3 border-top text-end">
|
||||
<div class="mt-5 pt-4 border-top text-end">
|
||||
<a href="{% url 'volunteer_list' %}" class="btn btn-outline-secondary px-4 me-2">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary px-4">{% if volunteer %}Save Changes{% else %}Create Volunteer{% endif %}</button>
|
||||
</div>
|
||||
@ -122,8 +139,8 @@
|
||||
|
||||
{% if volunteer %}
|
||||
<!-- Event Assignments -->
|
||||
<div class="col-lg-5">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<div class="col-lg-12 mt-4">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-white border-0 py-3 d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0 fw-bold">Event Assignments</h5>
|
||||
<button type="button" class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#assignEventModal">
|
||||
@ -250,70 +267,78 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script src="{% static 'admin/js/vendor/select2/select2.full.min.js' %}"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize Tom Select
|
||||
const interestSelect = new TomSelect('#id_interests', {
|
||||
plugins: ['remove_button'],
|
||||
placeholder: 'Search for interests...',
|
||||
create: false,
|
||||
$(document).ready(function() {
|
||||
const $interestsSelect = $('#id_interests');
|
||||
|
||||
// Initialize Select2
|
||||
$interestsSelect.select2({
|
||||
placeholder: "Select interests...",
|
||||
allowClear: true,
|
||||
multiple: true,
|
||||
width: '100%'
|
||||
});
|
||||
|
||||
const saveInterestBtn = document.getElementById('saveInterestBtn');
|
||||
const newInterestNameInput = document.getElementById('newInterestName');
|
||||
const manageInterestsList = document.getElementById('manageInterestsList');
|
||||
|
||||
saveInterestBtn.addEventListener('click', function() {
|
||||
const name = newInterestNameInput.value.trim();
|
||||
if (!name) return;
|
||||
if (saveInterestBtn) {
|
||||
saveInterestBtn.addEventListener('click', function() {
|
||||
const name = newInterestNameInput.value.trim();
|
||||
if (!name) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('name', name);
|
||||
formData.append('csrfmiddlewaretoken', '{{ csrf_token }}');
|
||||
const formData = new FormData();
|
||||
formData.append('name', name);
|
||||
formData.append('csrfmiddlewaretoken', '{{ csrf_token }}');
|
||||
|
||||
fetch('{% url "interest_add" %}', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// Add new option to Tom Select and select it
|
||||
interestSelect.addOption({value: data.id, text: data.name});
|
||||
interestSelect.addItem(data.id);
|
||||
|
||||
// Add to manage list
|
||||
const item = document.createElement('div');
|
||||
item.className = 'list-group-item d-flex justify-content-between align-items-center px-0 interest-manage-item';
|
||||
item.dataset.id = data.id;
|
||||
item.innerHTML = `
|
||||
<span>${data.name}</span>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger border-0 delete-interest-btn" data-id="${data.id}">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
`;
|
||||
manageInterestsList.appendChild(item);
|
||||
|
||||
// Close modal and reset input
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('addInterestModal'));
|
||||
modal.hide();
|
||||
newInterestNameInput.value = '';
|
||||
|
||||
// Re-bind delete events
|
||||
bindDeleteEvents();
|
||||
} else {
|
||||
alert(data.error || 'Error adding interest.');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('An error occurred.');
|
||||
fetch('{% url "interest_add" %}', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// Add to Select2
|
||||
const newOption = new Option(data.name, data.id, false, false);
|
||||
$interestsSelect.append(newOption).trigger('change');
|
||||
|
||||
// Add to manage list
|
||||
const item = document.createElement('div');
|
||||
item.className = 'list-group-item d-flex justify-content-between align-items-center px-0 interest-manage-item';
|
||||
item.dataset.id = data.id;
|
||||
item.innerHTML = `
|
||||
<span>${data.name}</span>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger border-0 delete-interest-btn" data-id="${data.id}">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
`;
|
||||
manageInterestsList.appendChild(item);
|
||||
|
||||
// Close modal and reset input
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('addInterestModal'));
|
||||
modal.hide();
|
||||
newInterestNameInput.value = '';
|
||||
|
||||
// Re-bind delete events
|
||||
bindDeleteEvents();
|
||||
} else {
|
||||
alert(data.error || 'Error adding interest.');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('An error occurred.');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function bindDeleteEvents() {
|
||||
document.querySelectorAll('.delete-interest-btn').forEach(btn => {
|
||||
@ -336,8 +361,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// Remove from Tom Select
|
||||
interestSelect.removeOption(id);
|
||||
// Remove from Select2
|
||||
$interestsSelect.find(`option[value="${id}"]`).remove();
|
||||
$interestsSelect.trigger('change');
|
||||
|
||||
// Remove from manage list
|
||||
const item = document.querySelector(`.interest-manage-item[data-id="${id}"]`);
|
||||
|
||||
@ -1,35 +1,5 @@
|
||||
{% 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 %}
|
||||
<div class="container py-5">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
@ -46,9 +16,10 @@
|
||||
<input type="text" name="q" class="form-control" placeholder="Search by name or email..." value="{{ query|default:'' }}">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<select name="interest" id="interest-filter" class="form-select" multiple>
|
||||
<select name="interest" id="interest-filter" class="form-select">
|
||||
<option value="">All Interests</option>
|
||||
{% for interest in interests %}
|
||||
<option value="{{ interest.id }}" {% if interest.id|stringformat:"s" in selected_interests %}selected{% endif %}>
|
||||
<option value="{{ interest.id }}" {% if interest.id|stringformat:"s" == selected_interest %}selected{% endif %}>
|
||||
{{ interest.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
@ -139,12 +110,12 @@
|
||||
<ul class="pagination justify-content-center mb-0">
|
||||
{% if volunteers.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=1{% if query %}&q={{ query }}{% endif %}{% for i in selected_interests %}&interest={{ i }}{% endfor %}" aria-label="First">
|
||||
<a class="page-link" href="?page=1{% if query %}&q={{ query }}{% endif %}{% if selected_interest %}&interest={{ selected_interest }}{% endif %}" aria-label="First">
|
||||
<span aria-hidden="true">««</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<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">
|
||||
<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">
|
||||
<span aria-hidden="true">«</span>
|
||||
</a>
|
||||
</li>
|
||||
@ -154,12 +125,12 @@
|
||||
|
||||
{% if volunteers.has_next %}
|
||||
<li class="page-item">
|
||||
<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">
|
||||
<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">
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<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">
|
||||
<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">
|
||||
<span aria-hidden="true">»»</span>
|
||||
</a>
|
||||
</li>
|
||||
@ -202,13 +173,6 @@
|
||||
|
||||
<script>
|
||||
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 checkboxes = document.querySelectorAll('.volunteer-checkbox');
|
||||
const bulkActions = document.getElementById('bulk-actions');
|
||||
@ -259,4 +223,4 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@ -175,7 +175,7 @@ def voter_detail(request, voter_id):
|
||||
'interactions': voter.interactions.all().order_by('-date'),
|
||||
'event_participations': voter.event_participations.all().order_by('-event__date'),
|
||||
'likelihoods': voter.likelihoods.all(),
|
||||
'voter_form': VoterForm(instance=voter),
|
||||
'voter_form': VoterForm(instance=voter, user=request.user, tenant=tenant),
|
||||
'interaction_form': InteractionForm(tenant=tenant),
|
||||
'donation_form': DonationForm(tenant=tenant),
|
||||
'likelihood_form': VoterLikelihoodForm(tenant=tenant),
|
||||
@ -184,6 +184,7 @@ def voter_detail(request, voter_id):
|
||||
}
|
||||
return render(request, 'core/voter_detail.html', context)
|
||||
|
||||
@role_required(["admin", "campaign_manager", "campaign_staff", "system_admin", "campaign_admin"], permission="core.change_voter")
|
||||
def voter_edit(request, voter_id):
|
||||
"""
|
||||
Update voter core demographics.
|
||||
@ -198,7 +199,7 @@ def voter_edit(request, voter_id):
|
||||
lon_raw = request.POST.get('longitude')
|
||||
logger.info(f"Voter Edit POST: lat={lat_raw}, lon={lon_raw}")
|
||||
|
||||
form = VoterForm(request.POST, instance=voter)
|
||||
form = VoterForm(request.POST, instance=voter, user=request.user, tenant=tenant)
|
||||
if form.is_valid():
|
||||
# If coordinates were provided in POST, ensure they are applied to the instance
|
||||
# This handles cases where readonly or other widget settings might interfere
|
||||
@ -845,9 +846,9 @@ def volunteer_list(request):
|
||||
)
|
||||
|
||||
# Interest filter
|
||||
interest_ids = request.GET.getlist("interest")
|
||||
if interest_ids:
|
||||
volunteers = volunteers.filter(interests__id__in=interest_ids).distinct()
|
||||
interest_id = request.GET.get("interest")
|
||||
if interest_id:
|
||||
volunteers = volunteers.filter(interests__id=interest_id).distinct()
|
||||
|
||||
interests = Interest.objects.filter(tenant=tenant).order_by('name')
|
||||
|
||||
@ -861,7 +862,7 @@ def volunteer_list(request):
|
||||
'volunteers': volunteers_page,
|
||||
'query': query,
|
||||
'interests': interests,
|
||||
'selected_interests': interest_ids,
|
||||
'selected_interest': interest_id,
|
||||
}
|
||||
return render(request, 'core/volunteer_list.html', context)
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user