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 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: 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'), (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 ) 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.CharField( required=False, widget=forms.Select(attrs={"class": "form-select"}), label="Voter to Follow Up") 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'} )