From bf2d558e03d328103ed78dc22ff1722a69d5098f Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Tue, 3 Feb 2026 18:42:21 +0000 Subject: [PATCH] Autosave: 20260203-184221 --- config/__pycache__/urls.cpython-311.pyc | Bin 1660 -> 1902 bytes config/urls.py | 3 + core/__pycache__/forms.cpython-311.pyc | Bin 33473 -> 36046 bytes core/__pycache__/views.cpython-311.pyc | Bin 95319 -> 95475 bytes core/forms.py | 77 ++++++++- core/templates/core/volunteer_detail.html | 202 ++++++++++++---------- core/templates/core/volunteer_list.html | 52 +----- core/views.py | 13 +- 8 files changed, 204 insertions(+), 143 deletions(-) diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc index 66debe83dcf9c19f2c097a57dec38461f42a6330..f313ffd1238123aed1772862640df28325e68079 100644 GIT binary patch delta 588 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 M?)pYAug5L@U&D(+o&W#< delta 7859 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 diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 5afc5c83868894f75686cf4b0cb1d93df1ca39b3..f6e7da6653c67cd6be0c789f8fd2cad2b29682aa 100644 GIT binary patch delta 8807 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 Ng@DGn^Wb5>{{rg~t$Y9g diff --git a/core/forms.py b/core/forms.py index f15fd6b..aa2c26d 100644 --- a/core/forms.py +++ b/core/forms.py @@ -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'}) \ No newline at end of file diff --git a/core/templates/core/volunteer_detail.html b/core/templates/core/volunteer_detail.html index a57e425..9ed1aba 100644 --- a/core/templates/core/volunteer_detail.html +++ b/core/templates/core/volunteer_detail.html @@ -1,31 +1,45 @@ {% extends "base.html" %} +{% load static %} -{% block head %} - - - +{% block extra_css %} + {% endblock %} @@ -35,7 +49,7 @@
@@ -52,13 +66,13 @@
-
-
-
+
+
+
Volunteer Information
{% csrf_token %} -
+
{{ form.first_name }} @@ -92,8 +106,8 @@
If enabled, this volunteer will be the default assigned person for new call queue entries.
-
-
+
+
- {{ form.interests }} +
+ {{ form.interests }} +
+
Search and select multiple interest types for this volunteer.
{{ form.notes }}
-
+
Cancel
@@ -122,8 +139,8 @@ {% if volunteer %} -
-
+
+
Event Assignments
{% endif %} +{% endblock %} +{% block extra_js %} + - -{% endblock %} - {% block content %}
@@ -46,9 +16,10 @@
- + {% for interest in interests %} - {% endfor %} @@ -139,12 +110,12 @@
    {% if volunteers.has_previous %}
  • - +
  • - +
  • @@ -154,12 +125,12 @@ {% if volunteers.has_next %}
  • - +
  • - +
  • @@ -202,13 +173,6 @@ -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/core/views.py b/core/views.py index 8364507..bb19060 100644 --- a/core/views.py +++ b/core/views.py @@ -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)