494 lines
22 KiB
Python
494 lines
22 KiB
Python
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.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'})
|