diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index 8335e4f..9619ec4 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc index fdd70b9..48580fa 100644 Binary files a/core/__pycache__/forms.cpython-311.pyc and b/core/__pycache__/forms.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 1475653..be74b0a 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 0f3b89b..8695930 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index 7ce2f67..1350ddf 100644 --- a/core/admin.py +++ b/core/admin.py @@ -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') \ No newline at end of file diff --git a/core/forms.py b/core/forms.py index 90b9b8c..74dc8ed 100644 --- a/core/forms.py +++ b/core/forms.py @@ -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'}) diff --git a/core/templates/base.html b/core/templates/base.html index d42c9a2..b90e601 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -40,6 +40,12 @@ + +
Admin Panel @@ -105,4 +111,4 @@ }); - \ No newline at end of file + diff --git a/core/templates/core/event_detail.html b/core/templates/core/event_detail.html new file mode 100644 index 0000000..3058175 --- /dev/null +++ b/core/templates/core/event_detail.html @@ -0,0 +1,249 @@ +{% extends "base.html" %} +{% load static %} + +{% block content %} +
+
+ +
+

{{ event.name|default:event.event_type }}

+
+ Edit Event Info + +
+
+
+ +
+ +
+
+
+
Event Details
+
+ + {{ event.event_type }} +
+
+ + {{ event.date|date:"F d, Y" }} +
+
+ + + {% 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 %} + +
+
+ +

{{ event.description|default:"No description provided." }}

+
+
+
+
+ + +
+
+
+
Participants ({{ participations.count }})
+
+
+ + + + + + + + + + {% for p in participations %} + + + + + + {% empty %} + + + + {% endfor %} + +
Voter NameStatusActions
+ + {{ p.voter.first_name }} {{ p.voter.last_name }} + + + + {{ p.participation_status.name }} + + +
+ + +
+
+ No participants yet. +
+
+
+
+
+
+ + + + + +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/event_list.html b/core/templates/core/event_list.html new file mode 100644 index 0000000..369fd5d --- /dev/null +++ b/core/templates/core/event_list.html @@ -0,0 +1,58 @@ +{% extends "base.html" %} + +{% block content %} +
+
+

Campaign Events

+ +
+ +
+
+ + + + + + + + + + + + {% for event in events %} + + + + + + + + {% empty %} + + + + {% endfor %} + +
Event NameTypeDateTimeActions
+ + {{ event.name|default:event.event_type }} + + {{ event.event_type }}{{ event.date|date:"M d, Y" }} + {% 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 %} + + View Details +
+

No events found for this campaign.

+
+
+
+
+{% endblock %} diff --git a/core/templates/core/index.html b/core/templates/core/index.html index e942b7f..bb77b59 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -191,13 +191,14 @@
-
+
Upcoming Events
+ View All
{% for event in upcoming_events %} -
+
{{ event.name|default:event.event_type }}
@@ -207,7 +208,7 @@
{{ event.date|date:"M d, Y" }}
-
+
{% empty %}
No upcoming events. diff --git a/core/templates/core/volunteer_detail.html b/core/templates/core/volunteer_detail.html new file mode 100644 index 0000000..f31a821 --- /dev/null +++ b/core/templates/core/volunteer_detail.html @@ -0,0 +1,339 @@ +{% extends "base.html" %} + +{% block head %} + + + + +{% endblock %} + +{% block content %} +
+
+ +
+

{% if volunteer %}{{ volunteer.first_name }} {{ volunteer.last_name }}{% else %}New Volunteer{% endif %}

+ {% if volunteer %} +
+ {% csrf_token %} + +
+ {% endif %} +
+
+ +
+ +
+
+
+
Volunteer Information
+
+ {% csrf_token %} +
+
+ + {{ form.first_name }} +
+
+ + {{ form.last_name }} +
+
+ + {{ form.email }} +
+
+ + {{ form.phone }} +
+
+
+ +
+ + +
+
+ {{ form.interests }} +
+
+
+ Cancel + +
+
+
+
+
+ + {% if volunteer %} + +
+
+
+
Event Assignments
+ +
+
+ + + + + + + + + + {% for assignment in assignments %} + + + + + + {% empty %} + + + + {% endfor %} + +
EventRoleAction
+ + {{ assignment.event.name|default:assignment.event.event_type }} + +
{{ assignment.event.date|date:"M d, Y" }}
+
{{ assignment.role }} +
+ {% csrf_token %} + +
+
+ No events assigned yet. +
+
+
+
+ {% endif %} +
+
+ + + + + + + +{% if volunteer %} + + +{% endif %} + + +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/volunteer_list.html b/core/templates/core/volunteer_list.html new file mode 100644 index 0000000..a40995a --- /dev/null +++ b/core/templates/core/volunteer_list.html @@ -0,0 +1,107 @@ +{% extends "base.html" %} + +{% block content %} +
+
+

Volunteer Directory

+ +
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ + + + + + + + + + + + {% for volunteer in volunteers %} + + + + + + + + {% empty %} + + + + {% endfor %} + +
NameEmailPhoneInterestsActions
+ + {{ volunteer.first_name }} {{ volunteer.last_name }} + + {{ volunteer.email }}{{ volunteer.phone|default:"-" }} + {% for interest in volunteer.interests.all %} + {{ interest.name }} + {% empty %} + No interests listed + {% endfor %} + + View & Edit +
+

No volunteers found matching your search.

+ Add the first volunteer +
+
+ + {% if volunteers.paginator.num_pages > 1 %} + + {% endif %} +
+
+{% endblock %} diff --git a/core/templates/core/voter_detail.html b/core/templates/core/voter_detail.html index 3e4c973..4e4c1d2 100644 --- a/core/templates/core/voter_detail.html +++ b/core/templates/core/voter_detail.html @@ -204,13 +204,13 @@ @@ -260,77 +260,6 @@
- -
-
-
- - - - - - - - - - {% for record in voting_records %} - - - - - - {% empty %} - - {% endfor %} - -
Election DateDescriptionParty
{{ record.election_date|date:"M d, Y" }}{{ record.election_description }}{{ record.primary_party|default:"-" }}
No voting records found.
-
-
-
- - -
-
-
-
Donation History
- -
-
- - - - - - - - - - - {% for donation in donations %} - - - - - - - {% empty %} - - {% endfor %} - -
DateMethodAmountActions
{{ donation.date|date:"M d, Y" }}{{ donation.method.name }}${{ donation.amount }} - - -
No donations recorded.
-
-
-
-
@@ -385,6 +314,77 @@
+ + +
+
+
+
Donation History
+ +
+
+ + + + + + + + + + + {% for donation in donations %} + + + + + + + {% empty %} + + {% endfor %} + +
DateMethodAmountActions
{{ donation.date|date:"M d, Y" }}{{ donation.method.name }}${{ donation.amount }} + + +
No donations recorded.
+
+
+
+ + +
+
+
+ + + + + + + + + + {% for record in voting_records %} + + + + + + {% empty %} + + {% endfor %} + +
Election DateDescriptionParty
{{ record.election_date|date:"M d, Y" }}{{ record.election_description }}{{ record.primary_party|default:"-" }}
No voting records found.
+
+
+
@@ -1085,4 +1085,4 @@ } }); -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/core/urls.py b/core/urls.py index 39ae92f..aa0c522 100644 --- a/core/urls.py +++ b/core/urls.py @@ -28,4 +28,22 @@ urlpatterns = [ path('voters//event-participation/add/', views.add_event_participation, name='add_event_participation'), path('event-participation//edit/', views.edit_event_participation, name='edit_event_participation'), path('event-participation//delete/', views.delete_event_participation, name='delete_event_participation'), + + # Event Detail and Participant Management + path('events/', views.event_list, name='event_list'), + path('events//', views.event_detail, name='event_detail'), + path('events//participant/add/', views.event_add_participant, name='event_add_participant'), + path('events/participant//edit/', views.event_edit_participant, name='event_edit_participant'), + path('events/participant//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//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//', views.volunteer_detail, name='volunteer_detail'), + path('volunteers//delete/', views.volunteer_delete, name='volunteer_delete'), + path('volunteers//assign-event/', views.volunteer_assign_event, name='volunteer_assign_event'), + path('volunteers/assignment//remove/', views.volunteer_remove_event, name='volunteer_remove_event'), ] diff --git a/core/views.py b/core/views.py index 7013db4..f902649 100644 --- a/core/views.py +++ b/core/views.py @@ -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') \ No newline at end of file + 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.'})