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: 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) 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.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): 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 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.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'}) class VolunteerProfileForm(forms.ModelForm): class Meta: model = Volunteer fields = ['phone'] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) for field in self.fields.values(): field.widget.attrs.update({'class': 'form-control'})