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 core.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: model = Voter fields = [ 'first_name', 'last_name', 'nickname', 'birthdate', 'address_street', 'city', 'state', 'prior_state', 'zip_code', 'county', 'neighborhood', 'latitude', 'longitude', 'phone', 'phone_type', 'secondary_phone', 'secondary_phone_type', 'email', 'voter_id', 'district', 'precinct', 'registration_date', 'is_targeted', 'is_inactive', 'target_door_visit', 'door_visit', 'voted', 'candidate_support', 'yard_sign', 'window_sticker', 'notes', 'call_queue_status' ] widgets = { 'birthdate': forms.DateInput(attrs={'type': 'date'}), 'registration_date': forms.DateInput(attrs={'type': 'date'}), 'latitude': forms.TextInput(attrs={'class': 'form-control bg-light'}), 'longitude': forms.TextInput(attrs={'class': 'form-control bg-light'}), 'notes': forms.Textarea(attrs={'rows': 3}), 'call_queue_status': forms.Select(attrs={'class': 'form-select'}), } def __init__(self, *args, user=None, tenant=None, **kwargs): self.user = user self.tenant = tenant super().__init__(*args, **kwargs) # Always make call_queue_status readonly as it's automated # 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 if isinstance(field.widget, forms.CheckboxInput): field.widget.attrs.update({'class': 'form-check-input'}) else: field.widget.attrs.update({'class': 'form-control'}) self.fields['candidate_support'].widget.attrs.update({'class': 'form-select'}) self.fields['yard_sign'].widget.attrs.update({'class': 'form-select'}) self.fields['window_sticker'].widget.attrs.update({'class': 'form-select'}) self.fields['phone_type'].widget.attrs.update({'class': 'form-select'}) self.fields['secondary_phone_type'].widget.attrs.update({'class': 'form-select'}) self.fields['call_queue_status'].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) if self.user: if self.user.is_superuser: is_admin = True elif self.tenant: 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'), (1, 'January'), (2, 'February'), (3, 'March'), (4, 'April'), (5, 'May'), (6, 'June'), (7, 'July'), (8, 'August'), (9, 'September'), (10, 'October'), (11, 'November'), (12, 'December') ] BOOLEAN_CHOICES = [('', 'Any'), ('True', 'Yes'), ('False', 'No')] first_name = forms.CharField(required=False) last_name = forms.CharField(required=False) address = forms.CharField(required=False) voter_id = forms.CharField(required=False, label="Voter ID") birth_month = forms.ChoiceField(choices=MONTH_CHOICES, required=False, label="Birth Month") city = forms.CharField(required=False) zip_code = forms.CharField(required=False) neighborhood = forms.CharField(required=False) district = forms.CharField(required=False) precinct = forms.CharField(required=False) email = forms.EmailField(required=False) phone = forms.CharField(required=False, label="Phone Number") phone_type = forms.ChoiceField( choices=[('', 'Any')] + Voter.PHONE_TYPE_CHOICES, required=False ) is_targeted = forms.ChoiceField(choices=BOOLEAN_CHOICES, required=False, label="Is Targeted") target_door_visit = forms.ChoiceField(choices=BOOLEAN_CHOICES, required=False, label="Target Door Visit") voted = forms.ChoiceField(choices=BOOLEAN_CHOICES, required=False, label="Voted") door_visit = forms.ChoiceField(choices=BOOLEAN_CHOICES, required=False, label="Door Visited") ever_had_yard_sign = forms.ChoiceField(choices=BOOLEAN_CHOICES, required=False, label="Ever Had Yard Sign") ever_had_large_sign = forms.ChoiceField(choices=BOOLEAN_CHOICES, required=False, label="Ever Had Large Sign") candidate_support = forms.ChoiceField( choices=[('', 'Any')] + Voter.CANDIDATE_SUPPORT_CHOICES, required=False ) yard_sign = forms.ChoiceField( choices=[('', 'Any')] + Voter.YARD_SIGN_CHOICES, required=False ) window_sticker = forms.ChoiceField( choices=[('', 'Any')] + Voter.WINDOW_STICKER_CHOICES, required=False ) call_queue_status = forms.ChoiceField( choices=[('', 'Any')] + Voter.CALL_QUEUE_STATUS_CHOICES, required=False, label="Call Queue Status" ) min_total_donation = forms.DecimalField(required=False, min_value=0, label="Min Total Donation") max_total_donation = forms.DecimalField(required=False, min_value=0, label="Max Total Donation") def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) for field in self.fields.values(): if isinstance(field.widget, forms.CheckboxInput): field.widget.attrs.update({'class': 'form-check-input'}) else: field.widget.attrs.update({'class': 'form-control'}) self.fields['birth_month'].widget.attrs.update({'class': 'form-select'}) self.fields['candidate_support'].widget.attrs.update({'class': 'form-select'}) self.fields['yard_sign'].widget.attrs.update({'class': 'form-select'}) self.fields['window_sticker'].widget.attrs.update({'class': 'form-select'}) self.fields['phone_type'].widget.attrs.update({'class': 'form-select'}) self.fields['call_queue_status'].widget.attrs.update({'class': 'form-select'}) self.fields['is_targeted'].widget.attrs.update({'class': 'form-select'}) self.fields['target_door_visit'].widget.attrs.update({'class': 'form-select'}) self.fields['door_visit'].widget.attrs.update({'class': 'form-select'}) self.fields['voted'].widget.attrs.update({'class': 'form-select'}) class InteractionForm(forms.ModelForm): class Meta: model = Interaction fields = ['type', 'volunteer', 'date', 'description', 'notes'] widgets = { 'date': forms.DateTimeInput(attrs={'type': 'datetime-local'}, format='%Y-%m-%dT%H:%M'), 'notes': forms.Textarea(attrs={'rows': 2}), } def __init__(self, *args, tenant=None, **kwargs): super().__init__(*args, **kwargs) if tenant: self.fields['type'].queryset = InteractionType.objects.filter(tenant=tenant, is_active=True) self.fields['volunteer'].queryset = Volunteer.objects.filter(tenant=tenant) for field in self.fields.values(): field.widget.attrs.update({'class': 'form-control'}) self.fields['type'].widget.attrs.update({'class': 'form-select'}) self.fields['volunteer'].widget.attrs.update({'class': 'form-select'}) if self.instance and self.instance.date: self.initial['date'] = self.instance.date.strftime('%Y-%m-%dT%H:%M') class DonationForm(forms.ModelForm): class Meta: model = Donation fields = ['date', 'method', 'amount'] widgets = { 'date': forms.DateInput(attrs={'type': 'date'}), } def __init__(self, *args, tenant=None, **kwargs): super().__init__(*args, **kwargs) if tenant: self.fields['method'].queryset = DonationMethod.objects.filter(tenant=tenant, is_active=True) for field in self.fields.values(): field.widget.attrs.update({'class': 'form-control'}) self.fields['method'].widget.attrs.update({'class': 'form-select'}) class VoterLikelihoodForm(forms.ModelForm): class Meta: model = VoterLikelihood fields = ['election_type', 'likelihood'] def __init__(self, *args, tenant=None, **kwargs): super().__init__(*args, **kwargs) if tenant: self.fields['election_type'].queryset = ElectionType.objects.filter(tenant=tenant, is_active=True) for field in self.fields.values(): field.widget.attrs.update({'class': 'form-control'}) self.fields['election_type'].widget.attrs.update({'class': 'form-select'}) self.fields['likelihood'].widget.attrs.update({'class': 'form-select'}) class EventParticipationForm(forms.ModelForm): class Meta: model = EventParticipation fields = ['event', 'participation_status'] def __init__(self, *args, tenant=None, **kwargs): super().__init__(*args, **kwargs) if tenant: self.fields['event'].queryset = Event.objects.filter(tenant=tenant) 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['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 fields = ['name', 'date', 'start_time', 'end_time', 'event_type', 'default_volunteer_role', 'description', 'location_name', 'address', 'city', 'state', 'zip_code', 'latitude', 'longitude'] widgets = { 'date': forms.DateInput(attrs={'type': 'date'}), 'start_time': forms.TimeInput(attrs={'type': 'time'}), 'end_time': forms.TimeInput(attrs={'type': 'time'}), 'description': forms.Textarea(attrs={'rows': 2}), } def __init__(self, *args, tenant=None, **kwargs): super().__init__(*args, **kwargs) if tenant: self.fields['event_type'].queryset = EventType.objects.filter(tenant=tenant, is_active=True) self.fields['default_volunteer_role'].queryset = VolunteerRole.objects.filter(tenant=tenant, is_active=True) for field in self.fields.values(): field.widget.attrs.update({'class': 'form-control'}) self.fields['event_type'].widget.attrs.update({'class': 'form-select'}) self.fields['default_volunteer_role'].widget.attrs.update({'class': 'form-select'}) class VoterImportForm(forms.Form): tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), label="Campaign") file = forms.FileField(label="Select CSV file") def __init__(self, *args, **kwargs): 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 EventImportForm(forms.Form): tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), label="Campaign") file = forms.FileField(label="Select CSV file") def __init__(self, *args, **kwargs): 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 EventParticipationImportForm(forms.Form): file = forms.FileField(label="Select CSV file") def __init__(self, *args, event=None, **kwargs): super().__init__(*args, **kwargs) # No tenant field needed as event_id is passed directly self.fields['file'].widget.attrs.update({'class': 'form-control'}) class ParticipantMappingForm(forms.Form): def __init__(self, *args, headers, tenant, **kwargs): super().__init__(*args, **kwargs) self.fields['email_column'] = forms.ChoiceField( choices=[(header, header) for header in headers], label="Column for Email Address", required=True, widget=forms.Select(attrs={'class': 'form-select'}) ) name_choices = [('', '-- Select Name Column (Optional) --')] + [(header, header) for header in headers] self.fields['name_column'] = forms.ChoiceField( choices=name_choices, label="Column for Participant Name", required=False, widget=forms.Select(attrs={'class': 'form-select'}) ) phone_choices = [('', '-- Select Phone Column (Optional) --')] + [(header, header) for header in headers] self.fields['phone_column'] = forms.ChoiceField( choices=phone_choices, label="Column for Phone Number", required=False, widget=forms.Select(attrs={'class': 'form-select'}) ) participation_status_choices = [('', '-- Select Status Column (Optional) --')] + [(header, header) for header in headers] self.fields['participation_status_column'] = forms.ChoiceField( choices=participation_status_choices, label="Column for Participation Status", required=False, widget=forms.Select(attrs={'class': 'form-select'}) ) # Optional: Add a default participation status if no column is mapped self.fields['default_participation_status'] = forms.ModelChoiceField( queryset=ParticipationStatus.objects.filter(tenant=tenant, is_active=True), label="Default Participation Status (if no column mapped or column is empty)", required=False, empty_label="-- Select a Default Status --", widget=forms.Select(attrs={'class': 'form-select'}) ) class DonationImportForm(forms.Form): tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), label="Campaign") file = forms.FileField(label="Select CSV file") def __init__(self, *args, **kwargs): 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 InteractionImportForm(forms.Form): tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), label="Campaign") file = forms.FileField(label="Select CSV file") def __init__(self, *args, **kwargs): 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 VoterLikelihoodImportForm(forms.Form): tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), label="Campaign") file = forms.FileField(label="Select CSV file") def __init__(self, *args, **kwargs): 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 VolunteerImportForm(forms.Form): tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), label="Campaign") file = forms.FileField(label="Select CSV file") def __init__(self, *args, **kwargs): 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', 'is_default_caller', 'notes', 'interests'] widgets = { 'notes': forms.Textarea(attrs={'rows': 3}), 'interests': Select2MultipleWidget(), } 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(): if not isinstance(field.widget, forms.CheckboxInput): field.widget.attrs.update({'class': 'form-control'}) else: field.widget.attrs.update({'class': 'form-check-input'}) class VolunteerEventForm(forms.ModelForm): class Meta: model = VolunteerEvent fields = ['event', 'role_type'] 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'}) class VolunteerEventAddForm(forms.ModelForm): class Meta: model = VolunteerEvent fields = ['volunteer', 'role_type'] def __init__(self, *args, tenant=None, **kwargs): super().__init__(*args, **kwargs) if tenant: volunteer_id = self.data.get('volunteer') or self.initial.get('volunteer') if volunteer_id: self.fields['volunteer'].queryset = Volunteer.objects.filter(tenant=tenant, id=volunteer_id) else: self.fields['volunteer'].queryset = Volunteer.objects.none() for field in self.fields.values(): field.widget.attrs.update({'class': 'form-control'}) self.fields['volunteer'].widget.attrs.update({'class': 'form-select'}) class VotingRecordImportForm(forms.Form): tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), label="Campaign") file = forms.FileField(label="Select CSV file") def __init__(self, *args, **kwargs): 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 DoorVisitLogForm(forms.Form): OUTCOME_CHOICES = [ ("No Answer Left Literature", "No Answer Left Literature"), ("Spoke to voter", "Spoke to voter"), ("No Access to House", "No Access to House"), ] outcome = forms.ChoiceField( choices=OUTCOME_CHOICES, widget=forms.RadioSelect(attrs={"class": "btn-check"}), label="Outcome" ) notes = forms.CharField( widget=forms.Textarea(attrs={"class": "form-control", "rows": 3}), required=False, label="Notes" ) yard_sign_status = forms.ChoiceField( choices=[('no_change', 'No Change'), ('none', 'No Sign'), ('wants', 'Wants Yard Sign'), ('wants_large', 'Wants Large Sign')], initial='no_change', widget=forms.Select(attrs={"class": "form-select"}), label="Yard Sign Status" ) candidate_support = forms.ChoiceField( choices=Voter.CANDIDATE_SUPPORT_CHOICES, initial="unknown", widget=forms.Select(attrs={"class": "form-select"}), label="Candidate Support" ) follow_up = forms.BooleanField( required=False, widget=forms.CheckboxInput(attrs={"class": "form-check-input"}), label="Follow Up" ) follow_up_voter = forms.ChoiceField(choices=[('', '-- Select Voter --')], required=False, widget=forms.Select(attrs={"class": "form-select"}), label="Voter to Follow Up") def __init__(self, *args, voter_choices=None, **kwargs): super().__init__(*args, **kwargs) if voter_choices: self.fields["follow_up_voter"].choices = [('', '-- Select Voter --')] + list(voter_choices) call_notes = forms.CharField( widget=forms.Textarea(attrs={"class": "form-control", "rows": 2}), required=False, label="Call Notes" ) class ScheduledCallForm(forms.ModelForm): class Meta: model = ScheduledCall fields = ['volunteer', 'comments'] widgets = { 'comments': forms.Textarea(attrs={'rows': 3}), } def __init__(self, *args, tenant=None, **kwargs): super().__init__(*args, **kwargs) if tenant: self.fields['volunteer'].queryset = Volunteer.objects.filter(tenant=tenant) default_caller = Volunteer.objects.filter(tenant=tenant, is_default_caller=True).first() if default_caller: self.initial['volunteer'] = default_caller for field in self.fields.values(): field.widget.attrs.update({'class': 'form-control'}) self.fields['volunteer'].widget.attrs.update({'class': 'form-select'}) class UserUpdateForm(forms.ModelForm): class Meta: model = User fields = ['first_name', 'last_name', 'email'] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) for field in self.fields.values(): field.widget.attrs.update({'class': 'form-control'})