Autosave: 20260129-214513

This commit is contained in:
Flatlogic Bot 2026-01-29 21:45:13 +00:00
parent 3ac4dc73fb
commit 6b464385a5
15 changed files with 1193 additions and 85 deletions

View File

@ -181,7 +181,7 @@ class ParticipationStatusAdmin(admin.ModelAdmin):
class InterestAdmin(admin.ModelAdmin):
list_display = ('name', 'tenant')
list_filter = ('tenant',)
fields = ('tenant', 'donation_goal', 'twilio_account_sid', 'twilio_auth_token', 'twilio_from_number')
fields = ('tenant', 'name')
search_fields = ('name',)
class VotingRecordInline(admin.TabularInline):
@ -724,7 +724,7 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin):
class VolunteerAdmin(BaseImportAdminMixin, admin.ModelAdmin):
list_display = ('first_name', 'last_name', 'email', 'phone', 'tenant', 'user')
list_filter = ('tenant',)
fields = ('tenant', 'donation_goal', 'twilio_account_sid', 'twilio_auth_token', 'twilio_from_number')
fields = ('tenant', 'user', 'first_name', 'last_name', 'email', 'phone', 'interests')
search_fields = ('first_name', 'last_name', 'email', 'phone')
inlines = [VolunteerEventInline, InteractionInline]
filter_horizontal = ('interests',)
@ -1779,4 +1779,4 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin):
class CampaignSettingsAdmin(admin.ModelAdmin):
list_display = ('tenant', 'donation_goal', 'twilio_from_number')
list_filter = ('tenant',)
fields = ('tenant', 'donation_goal', 'twilio_account_sid', 'twilio_auth_token', 'twilio_from_number')
fields = ('tenant', 'donation_goal', 'twilio_account_sid', 'twilio_auth_token', 'twilio_from_number')

View File

@ -1,5 +1,5 @@
from django import forms
from .models import Voter, Interaction, Donation, VoterLikelihood, InteractionType, DonationMethod, ElectionType, Event, EventParticipation, EventType, Tenant, ParticipationStatus
from .models import Voter, Interaction, Donation, VoterLikelihood, InteractionType, DonationMethod, ElectionType, Event, EventParticipation, EventType, Tenant, ParticipationStatus, Volunteer, VolunteerEvent
class VoterForm(forms.ModelForm):
class Meta:
@ -146,6 +146,25 @@ class EventParticipationForm(forms.ModelForm):
self.fields['event'].widget.attrs.update({'class': 'form-select'})
self.fields['participation_status'].widget.attrs.update({'class': 'form-select'})
class EventParticipantAddForm(forms.ModelForm):
class Meta:
model = EventParticipation
fields = ['voter', 'participation_status']
def __init__(self, *args, tenant=None, **kwargs):
super().__init__(*args, **kwargs)
if tenant:
voter_id = self.data.get('voter') or self.initial.get('voter')
if voter_id:
self.fields['voter'].queryset = Voter.objects.filter(tenant=tenant, id=voter_id)
else:
self.fields['voter'].queryset = Voter.objects.none()
self.fields['participation_status'].queryset = ParticipationStatus.objects.filter(tenant=tenant, is_active=True)
for field in self.fields.values():
field.widget.attrs.update({'class': 'form-control'})
self.fields['voter'].widget.attrs.update({'class': 'form-select'})
self.fields['participation_status'].widget.attrs.update({'class': 'form-select'})
class EventForm(forms.ModelForm):
class Meta:
model = Event
@ -227,3 +246,33 @@ class VolunteerImportForm(forms.Form):
super().__init__(*args, **kwargs)
self.fields['tenant'].widget.attrs.update({'class': 'form-control form-select'})
self.fields['file'].widget.attrs.update({'class': 'form-control'})
class VolunteerForm(forms.ModelForm):
class Meta:
model = Volunteer
fields = ['first_name', 'last_name', 'email', 'phone', 'interests']
def __init__(self, *args, tenant=None, **kwargs):
super().__init__(*args, **kwargs)
if tenant:
from .models import Interest
self.fields['interests'].queryset = Interest.objects.filter(tenant=tenant)
for field in self.fields.values():
field.widget.attrs.update({'class': 'form-control'})
# self.fields['interests'].widget = forms.SelectMultiple()
# Re-apply class for checkbox
self.fields['interests'].widget.attrs.update({'class': 'form-select tom-select'})
class VolunteerEventForm(forms.ModelForm):
class Meta:
model = VolunteerEvent
fields = ['event', 'role']
def __init__(self, *args, tenant=None, **kwargs):
super().__init__(*args, **kwargs)
if tenant:
self.fields['event'].queryset = Event.objects.filter(tenant=tenant)
for field in self.fields.values():
field.widget.attrs.update({'class': 'form-control'})
self.fields['event'].widget.attrs.update({'class': 'form-select'})

View File

@ -40,6 +40,12 @@
<li class="nav-item">
<a class="nav-link" href="/voters/">Voters</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/events/">Events</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/volunteers/">Volunteers</a>
</li>
</ul>
<div class="d-flex align-items-center">
<a href="/admin/" class="btn btn-outline-primary btn-sm me-2">Admin Panel</a>
@ -105,4 +111,4 @@
});
</script>
</body>
</html>
</html>

View File

@ -0,0 +1,249 @@
{% extends "base.html" %}
{% load static %}
{% block content %}
<div class="container py-5">
<div class="mb-4">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'event_list' %}">Events</a></li>
<li class="breadcrumb-item active" aria-current="page">{{ event.name|default:event.event_type }}</li>
</ol>
</nav>
<div class="d-flex justify-content-between align-items-center">
<h1 class="h2 mb-0">{{ event.name|default:event.event_type }}</h1>
<div class="d-flex gap-2">
<a href="/admin/core/event/{{ event.id }}/change/" class="btn btn-outline-secondary btn-sm">Edit Event Info</a>
<button type="button" class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#addParticipantModal">
+ Add Participant
</button>
</div>
</div>
</div>
<div class="row g-4">
<!-- Event Details Column -->
<div class="col-lg-4">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
<h5 class="card-title fw-bold mb-4">Event Details</h5>
<div class="mb-3">
<label class="small text-muted text-uppercase fw-bold d-block">Type</label>
<span>{{ event.event_type }}</span>
</div>
<div class="mb-3">
<label class="small text-muted text-uppercase fw-bold d-block">Date</label>
<span>{{ event.date|date:"F d, Y" }}</span>
</div>
<div class="mb-3">
<label class="small text-muted text-uppercase fw-bold d-block">Time</label>
<span>
{% if event.start_time %}
{{ event.start_time|time:"g:i A" }}
{% if event.end_time %} - {{ event.end_time|time:"g:i A" }}{% endif %}
{% else %}
Not specified
{% endif %}
</span>
</div>
<div class="mb-0">
<label class="small text-muted text-uppercase fw-bold d-block">Description</label>
<p class="mb-0 text-muted">{{ event.description|default:"No description provided." }}</p>
</div>
</div>
</div>
</div>
<!-- Participants Column -->
<div class="col-lg-8">
<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">Participants ({{ participations.count }})</h5>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0 align-middle">
<thead class="bg-light">
<tr>
<th class="ps-4">Voter Name</th>
<th>Status</th>
<th class="pe-4 text-end">Actions</th>
</tr>
</thead>
<tbody>
{% for p in participations %}
<tr>
<td class="ps-4">
<a href="{% url 'voter_detail' p.voter.id %}" class="fw-semibold text-primary text-decoration-none">
{{ p.voter.first_name }} {{ p.voter.last_name }}
</a>
</td>
<td>
<span class="badge {% if p.participation_status.name == 'Attended' %}bg-success{% elif p.participation_status.name == 'Canceled' %}bg-danger{% else %}bg-info{% endif %} bg-opacity-10 text-{% if p.participation_status.name == 'Attended' %}success{% elif p.participation_status.name == 'Canceled' %}danger{% else %}info{% endif %} border border-{% if p.participation_status.name == 'Attended' %}success{% elif p.participation_status.name == 'Canceled' %}danger{% else %}info{% endif %}">
{{ p.participation_status.name }}
</span>
</td>
<td class="pe-4 text-end">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
Actions
</button>
<ul class="dropdown-menu dropdown-menu-end shadow-sm border-0">
<li><h6 class="dropdown-header">Update Status</h6></li>
{% for status in participation_statuses %}
{% if status != p.participation_status %}
<li>
<form action="{% url 'event_edit_participant' p.id %}" method="POST">
{% csrf_token %}
<input type="hidden" name="participation_status" value="{{ status.id }}">
<button type="submit" class="dropdown-item">{{ status.name }}</button>
</form>
</li>
{% endif %}
{% endfor %}
<li><hr class="dropdown-divider"></li>
<li>
<form action="{% url 'event_delete_participant' p.id %}" method="POST" onsubmit="return confirm('Remove this participant?')">
{% csrf_token %}
<button type="submit" class="dropdown-item text-danger">Remove from Event</button>
</form>
</li>
</ul>
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="3" class="text-center py-5 text-muted">
No participants yet.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Add Participant Modal -->
<div class="modal fade" id="addParticipantModal" tabindex="-1" aria-labelledby="addParticipantModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content border-0 shadow">
<div class="modal-header border-0 bg-primary text-white">
<h5 class="modal-title fw-bold" id="addParticipantModalLabel">Add Participant</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="{% url 'event_add_participant' event.id %}" method="POST" id="addParticipantForm">
{% csrf_token %}
<div class="modal-body p-4">
<div class="mb-3">
<label class="form-label fw-bold">Search Voter</label>
<div class="input-group">
<input type="text" id="voterSearchInput" class="form-control" placeholder="Type name or ID..." autocomplete="off">
<span class="input-group-text bg-white border-start-0">
<i class="bi bi-search text-muted"></i>
</span>
</div>
<div id="voterSearchResults" class="list-group mt-2 shadow-sm d-none" style="max-height: 200px; overflow-y: auto; position: absolute; width: calc(100% - 3rem); z-index: 1000;">
<!-- Results will appear here -->
</div>
<div id="selectedVoterDisplay" class="mt-2 d-none">
<div class="alert alert-info py-2 px-3 mb-0 d-flex justify-content-between align-items-center rounded-3 border-0">
<span id="voterName" class="fw-semibold"></span>
<button type="button" class="btn-close small" id="clearSelectedVoter"></button>
</div>
</div>
<!-- Hidden field for the actual voter ID -->
<input type="hidden" name="voter" id="voter_id_hidden" required>
</div>
<div class="mb-0">
<label for="{{ add_form.participation_status.id_for_label }}" class="form-label fw-bold">Participation Status</label>
{{ add_form.participation_status }}
</div>
</div>
<div class="modal-footer border-0 p-4 pt-0">
<button type="button" class="btn btn-outline-secondary rounded-pill px-4" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary rounded-pill px-4 shadow-sm" id="submitAddParticipant" disabled>Add Participant</button>
</div>
</form>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const searchInput = document.getElementById('voterSearchInput');
const resultsContainer = document.getElementById('voterSearchResults');
const hiddenVoterId = document.getElementById('voter_id_hidden');
const selectedDisplay = document.getElementById('selectedVoterDisplay');
const voterNameDisplay = document.getElementById('voterName');
const clearBtn = document.getElementById('clearSelectedVoter');
const submitBtn = document.getElementById('submitAddParticipant');
let debounceTimer;
searchInput.addEventListener('input', function() {
clearTimeout(debounceTimer);
const query = this.value.trim();
if (query.length < 2) {
resultsContainer.classList.add('d-none');
return;
}
debounceTimer = setTimeout(() => {
fetch(`/voters/search/json/?q=${encodeURIComponent(query)}`)
.then(response => response.json())
.then(data => {
resultsContainer.innerHTML = '';
if (data.results && data.results.length > 0) {
data.results.forEach(voter => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'list-group-item list-group-item-action py-2';
btn.innerHTML = `<div class="fw-bold">${voter.text}</div><div class="small text-muted">${voter.address || "No address"} | ${voter.phone || "No phone"}</div>`;
btn.addEventListener('click', () => {
selectVoter(voter.id, voter.text, voter.address, voter.phone);
});
resultsContainer.appendChild(btn);
});
resultsContainer.classList.remove('d-none');
} else {
const div = document.createElement('div');
div.className = 'list-group-item text-muted small py-2';
div.textContent = 'No results found';
resultsContainer.appendChild(div);
resultsContainer.classList.remove('d-none');
}
});
}, 300);
});
function selectVoter(id, text, address, phone) {
hiddenVoterId.value = id;
voterNameDisplay.innerHTML = `<div>${text}</div><div class="small fw-normal text-muted">${address || ""} ${phone ? "• " + phone : ""}</div>`;
selectedDisplay.classList.remove('d-none');
searchInput.parentElement.classList.add('d-none');
resultsContainer.classList.add('d-none');
submitBtn.disabled = false;
}
clearBtn.addEventListener('click', () => {
hiddenVoterId.value = '';
voterNameDisplay.textContent = '';
selectedDisplay.classList.add('d-none');
searchInput.parentElement.classList.remove('d-none');
searchInput.value = '';
submitBtn.disabled = true;
});
// Close results when clicking outside
document.addEventListener('click', function(e) {
if (!resultsContainer.contains(e.target) && e.target !== searchInput) {
resultsContainer.classList.add('d-none');
}
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,58 @@
{% extends "base.html" %}
{% block content %}
<div class="container py-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h2">Campaign Events</h1>
<div class="d-flex gap-2">
<a href="/admin/core/event/add/" class="btn btn-primary btn-sm">+ Create New Event</a>
</div>
</div>
<div class="card border-0 shadow-sm">
<div class="table-responsive">
<table class="table table-hover mb-0 align-middle">
<thead class="bg-light">
<tr>
<th class="ps-4">Event Name</th>
<th>Type</th>
<th>Date</th>
<th>Time</th>
<th class="pe-4 text-end">Actions</th>
</tr>
</thead>
<tbody>
{% for event in events %}
<tr>
<td class="ps-4">
<a href="{% url 'event_detail' event.id %}" class="fw-semibold text-primary text-decoration-none d-block">
{{ event.name|default:event.event_type }}
</a>
</td>
<td><span class="badge bg-light text-dark border">{{ event.event_type }}</span></td>
<td>{{ event.date|date:"M d, Y" }}</td>
<td>
{% if event.start_time %}
{{ event.start_time|time:"g:i A" }}
{% if event.end_time %} - {{ event.end_time|time:"g:i A" }}{% endif %}
{% else %}
-
{% endif %}
</td>
<td class="pe-4 text-end">
<a href="{% url 'event_detail' event.id %}" class="btn btn-sm btn-outline-primary">View Details</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="5" class="text-center py-5 text-muted">
<p class="mb-0">No events found for this campaign.</p>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}

View File

@ -191,13 +191,14 @@
<div class="col-lg-4">
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-white border-0 py-3">
<div class="card-header bg-white border-0 py-3 d-flex justify-content-between align-items-center">
<h5 class="mb-0 fw-bold">Upcoming Events</h5>
<a href="{% url 'event_list' %}" class="btn btn-sm btn-outline-primary rounded-pill px-3">View All</a>
</div>
<div class="card-body p-0">
<div class="list-group list-group-flush">
{% for event in upcoming_events %}
<div class="list-group-item px-4 py-3 border-0 border-bottom">
<a href="{% url 'event_detail' event.id %}" class="list-group-item list-group-item-action px-4 py-3 border-0 border-bottom">
<div class="d-flex justify-content-between align-items-center mb-1">
<h6 class="mb-0 fw-bold text-dark">{{ event.name|default:event.event_type }}</h6>
<span class="badge rounded-pill bg-primary bg-opacity-10 text-primary small">
@ -207,7 +208,7 @@
<div class="small fw-medium text-dark">
<i class="bi bi-calendar-event me-1 text-muted"></i>{{ event.date|date:"M d, Y" }}
</div>
</div>
</a>
{% empty %}
<div class="text-center py-5 text-muted">
No upcoming events.

View File

@ -0,0 +1,339 @@
{% 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: #86b7fe !important;
outline: 0 !important;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25) !important;
}
</style>
{% endblock %}
{% block content %}
<div class="container py-5">
<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 active" aria-current="page">{% if volunteer %}{{ volunteer.first_name }} {{ volunteer.last_name }}{% else %}Add New Volunteer{% endif %}</li>
</ol>
</nav>
<div class="d-flex justify-content-between align-items-center">
<h1 class="h2 mb-0">{% if volunteer %}{{ volunteer.first_name }} {{ volunteer.last_name }}{% else %}New Volunteer{% endif %}</h1>
{% if volunteer %}
<form action="{% url 'volunteer_delete' volunteer.id %}" method="POST" onsubmit="return confirm('Are you sure you want to delete this volunteer?')">
{% csrf_token %}
<button type="submit" class="btn btn-outline-danger btn-sm">Delete Volunteer</button>
</form>
{% endif %}
</div>
</div>
<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">
<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="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 }}
</div>
<div class="col-md-6">
<label for="{{ form.last_name.id_for_label }}" class="form-label small text-muted text-uppercase fw-bold">Last Name</label>
{{ form.last_name }}
</div>
<div class="col-md-6">
<label for="{{ form.email.id_for_label }}" class="form-label small text-muted text-uppercase fw-bold">Email Address</label>
{{ form.email }}
</div>
<div class="col-md-6">
<label for="{{ form.phone.id_for_label }}" class="form-label small text-muted text-uppercase fw-bold">Phone Number</label>
{{ form.phone }}
</div>
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-2">
<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">
<small>Manage Types</small>
</button>
<button type="button" class="btn btn-sm btn-outline-primary py-0" data-bs-toggle="modal" data-bs-target="#addInterestModal">
<small>+ Add New Type</small>
</button>
</div>
</div>
{{ form.interests }}
</div>
</div>
<div class="mt-4 pt-3 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>
</form>
</div>
</div>
</div>
{% if volunteer %}
<!-- Event Assignments -->
<div class="col-lg-5">
<div class="card border-0 shadow-sm h-100">
<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">
+ Assign Event
</button>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0 align-middle">
<thead class="bg-light">
<tr>
<th class="ps-4">Event</th>
<th>Role</th>
<th class="pe-4 text-end">Action</th>
</tr>
</thead>
<tbody>
{% for assignment in assignments %}
<tr>
<td class="ps-4">
<a href="{% url 'event_detail' assignment.event.id %}" class="fw-semibold text-primary text-decoration-none">
{{ assignment.event.name|default:assignment.event.event_type }}
</a>
<div class="small text-muted">{{ assignment.event.date|date:"M d, Y" }}</div>
</td>
<td>{{ assignment.role }}</td>
<td class="pe-4 text-end">
<form action="{% url 'volunteer_remove_event' assignment.id %}" method="POST" onsubmit="return confirm('Remove this assignment?')">
{% csrf_token %}
<button type="submit" class="btn btn-sm btn-link text-danger p-0">Remove</button>
</form>
</td>
</tr>
{% empty %}
<tr>
<td colspan="3" class="text-center py-5 text-muted">
No events assigned yet.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
</div>
</div>
<!-- Add Interest Modal -->
<div class="modal fade" id="addInterestModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-sm">
<div class="modal-content border-0 shadow">
<div class="modal-header border-0 pb-0">
<h6 class="modal-title fw-bold">New Interest Type</h6>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<input type="text" id="newInterestName" class="form-control" placeholder="e.g. Phone Banking">
</div>
<div class="modal-footer border-0 pt-0">
<button type="button" class="btn btn-sm btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" id="saveInterestBtn" class="btn btn-sm btn-primary">Add</button>
</div>
</div>
</div>
</div>
<!-- Manage Interests Modal -->
<div class="modal fade" id="manageInterestsModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content border-0 shadow">
<div class="modal-header border-0 bg-light">
<h5 class="modal-title fw-bold">Manage Interest Types</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p class="small text-muted mb-3">Deleting an interest type will remove it from all volunteers in the campaign.</p>
<div class="list-group list-group-flush" id="manageInterestsList">
{% for interest in form.interests.field.queryset %}
<div class="list-group-item d-flex justify-content-between align-items-center px-0 interest-manage-item" data-id="{{ interest.id }}">
<span>{{ interest.name }}</span>
<button type="button" class="btn btn-sm btn-outline-danger border-0 delete-interest-btn" data-id="{{ interest.id }}">
<i class="bi bi-trash"></i>
</button>
</div>
{% endfor %}
</div>
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
{% if volunteer %}
<!-- Assign Event Modal -->
<div class="modal fade" id="assignEventModal" tabindex="-1" aria-labelledby="assignEventModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content border-0 shadow">
<div class="modal-header border-0 bg-primary text-white">
<h5 class="modal-title fw-bold" id="assignEventModalLabel">Assign Volunteer to Event</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="{% url 'volunteer_assign_event' volunteer.id %}" method="POST">
{% csrf_token %}
<div class="modal-body p-4">
<div class="mb-3">
<label for="{{ assign_form.event.id_for_label }}" class="form-label fw-bold">Select Event</label>
{{ assign_form.event }}
</div>
<div class="mb-0">
<label for="{{ assign_form.role.id_for_label }}" class="form-label fw-bold">Role/Task</label>
{{ assign_form.role }}
<div class="form-text">e.g., Door knocker, Registration desk, Driver</div>
</div>
</div>
<div class="modal-footer border-0 p-4 pt-0">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary px-4">Assign Event</button>
</div>
</form>
</div>
</div>
</div>
{% endif %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialize Tom Select
const interestSelect = new TomSelect('#id_interests', {
plugins: ['remove_button'],
placeholder: 'Search for interests...',
create: false,
});
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;
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.');
});
});
function bindDeleteEvents() {
document.querySelectorAll('.delete-interest-btn').forEach(btn => {
btn.onclick = function() {
const id = this.dataset.id;
if (!confirm('Are you sure you want to delete this interest type from the campaign? This will remove it from all volunteers.')) {
return;
}
const formData = new FormData();
formData.append('csrfmiddlewaretoken', '{{ csrf_token }}');
fetch(`/interests/${id}/delete/`, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Remove from Tom Select
interestSelect.removeOption(id);
// Remove from manage list
const item = document.querySelector(`.interest-manage-item[data-id="${id}"]`);
if (item) item.remove();
} else {
alert(data.error || 'Error deleting interest.');
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred.');
});
};
});
}
bindDeleteEvents();
});
</script>
{% endblock %}

View File

@ -0,0 +1,107 @@
{% extends "base.html" %}
{% block content %}
<div class="container py-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h2">Volunteer Directory</h1>
<div class="d-flex gap-2">
<a href="{% url 'volunteer_add' %}" class="btn btn-primary btn-sm">+ Add New Volunteer</a>
</div>
</div>
<div class="card border-0 shadow-sm mb-4">
<div class="card-body p-4">
<form action="." method="GET" class="row g-3">
<div class="col-md-10">
<input type="text" name="q" class="form-control" placeholder="Search by name or email..." value="{{ query|default:'' }}">
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-primary w-100">Search</button>
</div>
</form>
</div>
</div>
<div class="card border-0 shadow-sm">
<div class="table-responsive">
<table class="table table-hover mb-0 align-middle">
<thead class="bg-light">
<tr>
<th class="ps-4">Name</th>
<th>Email</th>
<th>Phone</th>
<th>Interests</th>
<th class="pe-4 text-end">Actions</th>
</tr>
</thead>
<tbody>
{% for volunteer in volunteers %}
<tr>
<td class="ps-4">
<a href="{% url 'volunteer_detail' volunteer.id %}" class="fw-semibold text-primary text-decoration-none d-block">
{{ volunteer.first_name }} {{ volunteer.last_name }}
</a>
</td>
<td>{{ volunteer.email }}</td>
<td>{{ volunteer.phone|default:"-" }}</td>
<td>
{% for interest in volunteer.interests.all %}
<span class="badge bg-info-subtle text-info border border-info-subtle">{{ interest.name }}</span>
{% empty %}
<span class="text-muted small">No interests listed</span>
{% endfor %}
</td>
<td class="pe-4 text-end">
<a href="{% url 'volunteer_detail' volunteer.id %}" class="btn btn-outline-primary btn-sm">View & Edit</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="5" class="text-center py-5 text-muted">
<p class="mb-0">No volunteers found matching your search.</p>
<a href="{% url 'volunteer_add' %}" class="btn btn-link">Add the first volunteer</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if volunteers.paginator.num_pages > 1 %}
<div class="card-footer bg-white border-0 py-3">
<nav aria-label="Page navigation">
<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 %}" aria-label="First">
<span aria-hidden="true">&laquo;&laquo;</span>
</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ volunteers.previous_page_number }}{% if query %}&q={{ query }}{% endif %}" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% endif %}
<li class="page-item active"><span class="page-link">{{ volunteers.number }} of {{ volunteers.paginator.num_pages }}</span></li>
{% if volunteers.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ volunteers.next_page_number }}{% if query %}&q={{ query }}{% endif %}" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ volunteers.paginator.num_pages }}{% if query %}&q={{ query }}{% endif %}" aria-label="Last">
<span aria-hidden="true">&raquo;&raquo;</span>
</a>
</li>
{% endif %}
</ul>
</nav>
</div>
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -204,13 +204,13 @@
<button class="nav-link active border-0 py-3 px-4" id="interactions-tab" data-bs-toggle="tab" data-bs-target="#interactions" type="button" role="tab">Interactions</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link border-0 py-3 px-4" id="voting-tab" data-bs-toggle="tab" data-bs-target="#voting" type="button" role="tab">Voting History</button>
<button class="nav-link border-0 py-3 px-4" id="events-tab" data-bs-toggle="tab" data-bs-target="#events" type="button" role="tab">Events</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link border-0 py-3 px-4" id="donations-tab" data-bs-toggle="tab" data-bs-target="#donations" type="button" role="tab">Donations</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link border-0 py-3 px-4" id="events-tab" data-bs-toggle="tab" data-bs-target="#events" type="button" role="tab">Events</button>
<button class="nav-link border-0 py-3 px-4" id="voting-tab" data-bs-toggle="tab" data-bs-target="#voting" type="button" role="tab">Voting History</button>
</li>
</ul>
@ -260,77 +260,6 @@
</div>
</div>
<!-- Voting History Tab -->
<div class="tab-pane fade" id="voting" role="tabpanel">
<div class="card border-0 shadow-sm">
<div class="table-responsive">
<table class="table align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">Election Date</th>
<th>Description</th>
<th class="pe-4">Party</th>
</tr>
</thead>
<tbody>
{% for record in voting_records %}
<tr>
<td class="ps-4 text-nowrap">{{ record.election_date|date:"M d, Y" }}</td>
<td>{{ record.election_description }}</td>
<td class="pe-4">{{ record.primary_party|default:"-" }}</td>
</tr>
{% empty %}
<tr><td colspan="3" class="text-center py-4 text-muted">No voting records found.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Donations Tab -->
<div class="tab-pane fade" id="donations" role="tabpanel">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
<h6 class="mb-0">Donation History</h6>
<button class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#addDonationModal">
<i class="bi bi-plus-lg me-1"></i>Add Donation
</button>
</div>
<div class="table-responsive">
<table class="table align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">Date</th>
<th>Method</th>
<th class="text-end">Amount</th>
<th class="pe-4 text-end">Actions</th>
</tr>
</thead>
<tbody>
{% for donation in donations %}
<tr>
<td class="ps-4 text-nowrap">{{ donation.date|date:"M d, Y" }}</td>
<td>{{ donation.method.name }}</td>
<td class="text-end fw-semibold text-success">${{ donation.amount }}</td>
<td class="pe-4 text-end">
<button class="btn btn-sm btn-link text-primary p-0 me-2" data-bs-toggle="modal" data-bs-target="#editDonationModal{{ donation.id }}">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-link text-danger p-0" data-bs-toggle="modal" data-bs-target="#deleteDonationModal{{ donation.id }}">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
{% empty %}
<tr><td colspan="4" class="text-center py-4 text-muted">No donations recorded.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Events Tab -->
<div class="tab-pane fade" id="events" role="tabpanel">
<div class="card border-0 shadow-sm">
@ -385,6 +314,77 @@
</div>
</div>
</div>
<!-- Donations Tab -->
<div class="tab-pane fade" id="donations" role="tabpanel">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
<h6 class="mb-0">Donation History</h6>
<button class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#addDonationModal">
<i class="bi bi-plus-lg me-1"></i>Add Donation
</button>
</div>
<div class="table-responsive">
<table class="table align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">Date</th>
<th>Method</th>
<th class="text-end">Amount</th>
<th class="pe-4 text-end">Actions</th>
</tr>
</thead>
<tbody>
{% for donation in donations %}
<tr>
<td class="ps-4 text-nowrap">{{ donation.date|date:"M d, Y" }}</td>
<td>{{ donation.method.name }}</td>
<td class="text-end fw-semibold text-success">${{ donation.amount }}</td>
<td class="pe-4 text-end">
<button class="btn btn-sm btn-link text-primary p-0 me-2" data-bs-toggle="modal" data-bs-target="#editDonationModal{{ donation.id }}">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-link text-danger p-0" data-bs-toggle="modal" data-bs-target="#deleteDonationModal{{ donation.id }}">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
{% empty %}
<tr><td colspan="4" class="text-center py-4 text-muted">No donations recorded.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Voting History Tab -->
<div class="tab-pane fade" id="voting" role="tabpanel">
<div class="card border-0 shadow-sm">
<div class="table-responsive">
<table class="table align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">Election Date</th>
<th>Description</th>
<th class="pe-4">Party</th>
</tr>
</thead>
<tbody>
{% for record in voting_records %}
<tr>
<td class="ps-4 text-nowrap">{{ record.election_date|date:"M d, Y" }}</td>
<td>{{ record.election_description }}</td>
<td class="pe-4">{{ record.primary_party|default:"-" }}</td>
</tr>
{% empty %}
<tr><td colspan="3" class="text-center py-4 text-muted">No voting records found.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
@ -1085,4 +1085,4 @@
}
});
</script>
{% endblock %}
{% endblock %}

View File

@ -28,4 +28,22 @@ urlpatterns = [
path('voters/<int:voter_id>/event-participation/add/', views.add_event_participation, name='add_event_participation'),
path('event-participation/<int:participation_id>/edit/', views.edit_event_participation, name='edit_event_participation'),
path('event-participation/<int:participation_id>/delete/', views.delete_event_participation, name='delete_event_participation'),
# Event Detail and Participant Management
path('events/', views.event_list, name='event_list'),
path('events/<int:event_id>/', views.event_detail, name='event_detail'),
path('events/<int:event_id>/participant/add/', views.event_add_participant, name='event_add_participant'),
path('events/participant/<int:participation_id>/edit/', views.event_edit_participant, name='event_edit_participant'),
path('events/participant/<int:participation_id>/delete/', views.event_delete_participant, name='event_delete_participant'),
path('voters/search/json/', views.voter_search_json, name='voter_search_json'),
# Volunteer Management
path('interests/add/', views.interest_add, name='interest_add'),
path('interests/<int:interest_id>/delete/', views.interest_delete, name='interest_delete'),
path('volunteers/', views.volunteer_list, name='volunteer_list'),
path('volunteers/add/', views.volunteer_add, name='volunteer_add'),
path('volunteers/<int:volunteer_id>/', views.volunteer_detail, name='volunteer_detail'),
path('volunteers/<int:volunteer_id>/delete/', views.volunteer_delete, name='volunteer_delete'),
path('volunteers/<int:volunteer_id>/assign-event/', views.volunteer_assign_event, name='volunteer_assign_event'),
path('volunteers/assignment/<int:assignment_id>/remove/', views.volunteer_remove_event, name='volunteer_remove_event'),
]

View File

@ -10,8 +10,8 @@ from django.shortcuts import render, redirect, get_object_or_404
from django.db.models import Q, Sum
from django.contrib import messages
from django.core.paginator import Paginator
from .models import Voter, Tenant, Interaction, Donation, VoterLikelihood, EventParticipation, Event, EventType, InteractionType, DonationMethod, ElectionType, CampaignSettings, Volunteer
from .forms import VoterForm, InteractionForm, DonationForm, VoterLikelihoodForm, EventParticipationForm, VoterImportForm, AdvancedVoterSearchForm
from .models import Voter, Tenant, Interaction, Donation, VoterLikelihood, EventParticipation, Event, EventType, InteractionType, DonationMethod, ElectionType, CampaignSettings, Volunteer, ParticipationStatus, VolunteerEvent, Interest
from .forms import VoterForm, InteractionForm, DonationForm, VoterLikelihoodForm, EventParticipationForm, VoterImportForm, AdvancedVoterSearchForm, EventParticipantAddForm, EventForm, VolunteerForm, VolunteerEventForm
import logging
from django.utils import timezone
@ -661,4 +661,285 @@ def bulk_send_sms(request):
fail_count += 1
messages.success(request, f"Bulk SMS process completed: {success_count} successful, {fail_count} failed/skipped.")
return redirect('voter_advanced_search')
return redirect('voter_advanced_search')
def event_list(request):
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)
events = Event.objects.filter(tenant=tenant).order_by('-date')
context = {
'tenant': tenant,
'events': events,
'selected_tenant': tenant,
}
return render(request, 'core/event_list.html', context)
def event_detail(request, event_id):
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)
event = get_object_or_404(Event, id=event_id, tenant=tenant)
participations = event.participations.all().select_related('voter', 'participation_status').order_by('voter__last_name', 'voter__first_name')
# Form for adding a new participant
add_form = EventParticipantAddForm(tenant=tenant)
participation_statuses = ParticipationStatus.objects.filter(tenant=tenant, is_active=True)
context = {
'tenant': tenant,
'selected_tenant': tenant,
'event': event,
'participations': participations,
'add_form': add_form,
'participation_statuses': participation_statuses,
}
return render(request, 'core/event_detail.html', context)
def event_add_participant(request, event_id):
tenant_id = request.session.get("tenant_id")
tenant = get_object_or_404(Tenant, id=tenant_id)
event = get_object_or_404(Event, id=event_id, tenant=tenant)
if request.method == 'POST':
form = EventParticipantAddForm(request.POST, tenant=tenant)
if form.is_valid():
participation = form.save(commit=False)
participation.event = event
if not EventParticipation.objects.filter(event=event, voter=participation.voter).exists():
participation.save()
messages.success(request, f"{participation.voter} added to event.")
else:
messages.warning(request, "Voter is already a participant.")
else:
messages.error(request, "Error adding participant.")
return redirect('event_detail', event_id=event.id)
def event_edit_participant(request, participation_id):
tenant_id = request.session.get("tenant_id")
tenant = get_object_or_404(Tenant, id=tenant_id)
participation = get_object_or_404(EventParticipation, id=participation_id, event__tenant=tenant)
if request.method == 'POST':
status_id = request.POST.get('participation_status')
if status_id:
status = get_object_or_404(ParticipationStatus, id=status_id, tenant=tenant)
participation.participation_status = status
participation.save()
messages.success(request, f"Participation updated for {participation.voter}.")
else:
messages.error(request, "Invalid status.")
return redirect('event_detail', event_id=participation.event.id)
def event_delete_participant(request, participation_id):
tenant_id = request.session.get("tenant_id")
tenant = get_object_or_404(Tenant, id=tenant_id)
participation = get_object_or_404(EventParticipation, id=participation_id, event__tenant=tenant)
event_id = participation.event.id
voter_name = str(participation.voter)
participation.delete()
messages.success(request, f"{voter_name} removed from event.")
return redirect('event_detail', event_id=event_id)
def voter_search_json(request):
"""
JSON endpoint for voter search, used by autocomplete/search UI.
"""
selected_tenant_id = request.session.get("tenant_id")
if not selected_tenant_id:
return JsonResponse({"results": []})
query = request.GET.get("q", "").strip()
if len(query) < 2:
return JsonResponse({"results": []})
tenant = get_object_or_404(Tenant, id=selected_tenant_id)
voters = Voter.objects.filter(tenant=tenant)
search_filter = Q(first_name__icontains=query) | Q(last_name__icontains=query) | Q(voter_id__icontains=query)
if "," in query:
parts = [p.strip() for p in query.split(",") ]
if len(parts) >= 2:
search_filter |= Q(last_name__icontains=parts[0], first_name__icontains=parts[1])
results = voters.filter(search_filter).order_by("last_name", "first_name")[:20]
data = []
for v in results:
data.append({
"id": v.id,
"text": f"{v.last_name}, {v.first_name} ({v.voter_id})",
"address": v.address,
"phone": v.phone
})
return JsonResponse({"results": data})
def volunteer_list(request):
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)
volunteers = Volunteer.objects.filter(tenant=tenant).order_by('last_name', 'first_name')
# Simple search
query = request.GET.get("q")
if query:
volunteers = volunteers.filter(
Q(first_name__icontains=query) | Q(last_name__icontains=query) | Q(email__icontains=query)
)
paginator = Paginator(volunteers, 50)
page_number = request.GET.get('page')
volunteers_page = paginator.get_page(page_number)
context = {
'tenant': tenant,
'selected_tenant': tenant,
'volunteers': volunteers_page,
'query': query,
}
return render(request, 'core/volunteer_list.html', context)
def volunteer_add(request):
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)
if request.method == 'POST':
form = VolunteerForm(request.POST, tenant=tenant)
if form.is_valid():
volunteer = form.save(commit=False)
volunteer.tenant = tenant
volunteer.save()
form.save_m2m() # Save interests
messages.success(request, f"Volunteer {volunteer} added successfully.")
return redirect('volunteer_detail', volunteer_id=volunteer.id)
else:
form = VolunteerForm(tenant=tenant)
context = {
'form': form,
'tenant': tenant,
'selected_tenant': tenant,
}
return render(request, 'core/volunteer_detail.html', context)
def volunteer_detail(request, volunteer_id):
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)
volunteer = get_object_or_404(Volunteer, id=volunteer_id, tenant=tenant)
if request.method == 'POST':
form = VolunteerForm(request.POST, instance=volunteer, tenant=tenant)
if form.is_valid():
form.save()
messages.success(request, f"Volunteer {volunteer} updated successfully.")
return redirect('volunteer_detail', volunteer_id=volunteer.id)
else:
form = VolunteerForm(instance=volunteer, tenant=tenant)
assignments = volunteer.event_assignments.all().select_related('event')
assign_form = VolunteerEventForm(tenant=tenant)
context = {
'volunteer': volunteer,
'form': form,
'assignments': assignments,
'assign_form': assign_form,
'tenant': tenant,
'selected_tenant': tenant,
}
return render(request, 'core/volunteer_detail.html', context)
def volunteer_delete(request, volunteer_id):
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)
volunteer = get_object_or_404(Volunteer, id=volunteer_id, tenant=tenant)
if request.method == 'POST':
volunteer.delete()
messages.success(request, "Volunteer deleted.")
return redirect('volunteer_list')
return redirect('volunteer_detail', volunteer_id=volunteer_id)
def volunteer_assign_event(request, volunteer_id):
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)
volunteer = get_object_or_404(Volunteer, id=volunteer_id, tenant=tenant)
if request.method == 'POST':
form = VolunteerEventForm(request.POST, tenant=tenant)
if form.is_valid():
assignment = form.save(commit=False)
assignment.volunteer = volunteer
assignment.save()
messages.success(request, f"Assigned to {assignment.event}.")
else:
messages.error(request, "Error assigning to event.")
return redirect('volunteer_detail', volunteer_id=volunteer.id)
def volunteer_remove_event(request, assignment_id):
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)
assignment = get_object_or_404(VolunteerEvent, id=assignment_id, volunteer__tenant=tenant)
volunteer_id = assignment.volunteer.id
assignment.delete()
messages.success(request, "Assignment removed.")
return redirect('volunteer_detail', volunteer_id=volunteer_id)
def interest_add(request):
selected_tenant_id = request.session.get("tenant_id")
if not selected_tenant_id:
return JsonResponse({'success': False, 'error': 'No campaign selected.'})
tenant = get_object_or_404(Tenant, id=selected_tenant_id)
if request.method == 'POST':
name = request.POST.get('name', '').strip()
if name:
interest, created = Interest.objects.get_or_create(tenant=tenant, name=name)
if created:
return JsonResponse({'success': True, 'id': interest.id, 'name': interest.name})
else:
return JsonResponse({'success': False, 'error': 'Interest already exists.'})
return JsonResponse({'success': False, 'error': 'Invalid request.'})
def interest_delete(request, interest_id):
selected_tenant_id = request.session.get("tenant_id")
if not selected_tenant_id:
return JsonResponse({'success': False, 'error': 'No campaign selected.'})
tenant = get_object_or_404(Tenant, id=selected_tenant_id)
interest = get_object_or_404(Interest, id=interest_id, tenant=tenant)
if request.method == 'POST':
interest.delete()
return JsonResponse({'success': True})
return JsonResponse({'success': False, 'error': 'Invalid request.'})