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', 'door_visit', 'candidate_support', 'yard_sign', 'window_sticker', 'notes' ] 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}), } 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 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'}) 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: 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') ] 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) # Added email field phone_type = forms.ChoiceField( choices=[('', 'Any')] + Voter.PHONE_TYPE_CHOICES, required=False ) is_targeted = forms.BooleanField(required=False, label="Targeted Only") door_visit = forms.BooleanField(required=False, label="Visited Only") 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 ) 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'}) 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/Excel 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" ) wants_yard_sign = forms.BooleanField( required=False, widget=forms.CheckboxInput(attrs={"class": "form-check-input"}), label="Wants a Yard Sign" ) 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=[], 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 = 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'})