diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc index 66debe8..f313ffd 100644 Binary files a/config/__pycache__/urls.cpython-311.pyc and b/config/__pycache__/urls.cpython-311.pyc differ diff --git a/config/urls.py b/config/urls.py index 270c09c..0864e06 100644 --- a/config/urls.py +++ b/config/urls.py @@ -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")), ] diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc index e5d5174..9d83c04 100644 Binary files a/core/__pycache__/forms.cpython-311.pyc and b/core/__pycache__/forms.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 5afc5c8..f6e7da6 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ 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)