Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88ee4c2516 | ||
|
|
fbcc2964bf | ||
|
|
8de72675e5 | ||
|
|
c92857d73b | ||
|
|
01c62eb11d | ||
|
|
d3537b6427 | ||
|
|
935ecf1b68 | ||
|
|
0d11fc7d5d | ||
|
|
d244ac9d3f | ||
|
|
e39a0343c7 | ||
|
|
e0f6e045f3 | ||
|
|
bf2d558e03 | ||
|
|
ac80c84fbd | ||
|
|
c3568101a3 | ||
|
|
63faa21a4f | ||
|
|
442aec63b6 | ||
|
|
c5d42d341f | ||
|
|
77709c3744 | ||
|
|
f7bc2da356 | ||
|
|
181163257f | ||
|
|
6b464385a5 | ||
|
|
3ac4dc73fb | ||
|
|
d8bf0cd82c | ||
|
|
ae3d7f9f2e | ||
|
|
9c3c5219c6 | ||
|
|
4056b17780 | ||
|
|
2e087bcd88 | ||
|
|
e0d1690e97 | ||
|
|
1dfb7ebbf1 | ||
|
|
ac90cc59f4 | ||
|
|
dc2bd62142 | ||
|
|
c95591245a | ||
|
|
d756ed7a8a | ||
|
|
443505ace5 | ||
|
|
0ef73ff181 | ||
|
|
e4aeae1b74 | ||
|
|
14a93b6b2b | ||
|
|
7686b1143d | ||
|
|
14bf8d7295 | ||
|
|
e2ad03e446 | ||
|
|
aade4cc131 | ||
|
|
0ade1911eb | ||
|
|
5073ef280c |
159
ERD.md
Normal file
159
ERD.md
Normal file
@ -0,0 +1,159 @@
|
||||
# Entity Relationship Diagram
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
Tenant ||--o{ TenantUserRole : has
|
||||
Tenant ||--o{ InteractionType : defines
|
||||
Tenant ||--o{ DonationMethod : defines
|
||||
Tenant ||--o{ ElectionType : defines
|
||||
Tenant ||--o{ EventType : defines
|
||||
Tenant ||--o{ ParticipationStatus : defines
|
||||
Tenant ||--o{ Voter : belongs_to
|
||||
Tenant ||--o{ Event : organizes
|
||||
|
||||
User ||--o{ TenantUserRole : assigned_to
|
||||
|
||||
Voter ||--o{ VotingRecord : has
|
||||
Voter ||--o{ EventParticipation : participates
|
||||
Voter ||--o{ Donation : makes
|
||||
Voter ||--o{ Interaction : receives
|
||||
Voter ||--o{ VoterLikelihood : has
|
||||
|
||||
Event ||--o{ EventParticipation : includes
|
||||
EventType ||--o{ Event : categorizes
|
||||
ParticipationStatus ||--o{ EventParticipation : defines_status
|
||||
|
||||
InteractionType ||--o{ Interaction : categorizes
|
||||
DonationMethod ||--o{ Donation : categorizes
|
||||
ElectionType ||--o{ VoterLikelihood : categorizes
|
||||
|
||||
Tenant {
|
||||
int id PK
|
||||
string name
|
||||
string slug
|
||||
text description
|
||||
datetime created_at
|
||||
}
|
||||
|
||||
User {
|
||||
int id PK
|
||||
string username
|
||||
string email
|
||||
string first_name
|
||||
string last_name
|
||||
}
|
||||
|
||||
TenantUserRole {
|
||||
int id PK
|
||||
int user_id FK
|
||||
int tenant_id FK
|
||||
string role
|
||||
}
|
||||
|
||||
InteractionType {
|
||||
int id PK
|
||||
int tenant_id FK
|
||||
string name
|
||||
boolean is_active
|
||||
}
|
||||
|
||||
DonationMethod {
|
||||
int id PK
|
||||
int tenant_id FK
|
||||
string name
|
||||
boolean is_active
|
||||
}
|
||||
|
||||
ElectionType {
|
||||
int id PK
|
||||
int tenant_id FK
|
||||
string name
|
||||
boolean is_active
|
||||
}
|
||||
|
||||
EventType {
|
||||
int id PK
|
||||
int tenant_id FK
|
||||
string name
|
||||
boolean is_active
|
||||
}
|
||||
|
||||
ParticipationStatus {
|
||||
int id PK
|
||||
int tenant_id FK
|
||||
string name
|
||||
boolean is_active
|
||||
}
|
||||
|
||||
Voter {
|
||||
int id PK
|
||||
int tenant_id FK
|
||||
string voter_id
|
||||
string first_name
|
||||
string last_name
|
||||
text address
|
||||
string address_street
|
||||
string city
|
||||
string state
|
||||
string zip_code
|
||||
string county
|
||||
decimal latitude
|
||||
decimal longitude
|
||||
string phone
|
||||
string email
|
||||
string district
|
||||
string precinct
|
||||
date registration_date
|
||||
boolean is_targeted
|
||||
string candidate_support
|
||||
string yard_sign
|
||||
datetime created_at
|
||||
}
|
||||
|
||||
VotingRecord {
|
||||
int id PK
|
||||
int voter_id FK
|
||||
date election_date
|
||||
string election_description
|
||||
string primary_party
|
||||
}
|
||||
|
||||
Event {
|
||||
int id PK
|
||||
int tenant_id FK
|
||||
date date
|
||||
int event_type_id FK
|
||||
text description
|
||||
}
|
||||
|
||||
EventParticipation {
|
||||
int id PK
|
||||
int event_id FK
|
||||
int voter_id FK
|
||||
int participation_status_id FK
|
||||
}
|
||||
|
||||
Donation {
|
||||
int id PK
|
||||
int voter_id FK
|
||||
date date
|
||||
int method_id FK
|
||||
decimal amount
|
||||
}
|
||||
|
||||
Interaction {
|
||||
int id PK
|
||||
int voter_id FK
|
||||
int type_id FK
|
||||
date date
|
||||
string description
|
||||
text notes
|
||||
}
|
||||
|
||||
VoterLikelihood {
|
||||
int id PK
|
||||
int voter_id FK
|
||||
int election_type_id FK
|
||||
string likelihood
|
||||
}
|
||||
```
|
||||
0
assets/.gitkeep
Normal file
0
assets/.gitkeep
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -23,10 +23,14 @@ DEBUG = os.getenv("DJANGO_DEBUG", "true").lower() == "true"
|
||||
ALLOWED_HOSTS = [
|
||||
"127.0.0.1",
|
||||
"localhost",
|
||||
"grassrootscrm.flatlogic.app",
|
||||
os.getenv("HOST_FQDN", ""),
|
||||
]
|
||||
|
||||
CSRF_TRUSTED_ORIGINS = [
|
||||
"https://grassrootscrm.flatlogic.app",
|
||||
]
|
||||
CSRF_TRUSTED_ORIGINS += [
|
||||
origin for origin in [
|
||||
os.getenv("HOST_FQDN", ""),
|
||||
os.getenv("CSRF_TRUSTED_ORIGIN", "")
|
||||
@ -64,6 +68,8 @@ MIDDLEWARE = [
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'core.middleware.LoginRequiredMiddleware',
|
||||
'core.middleware.TimezoneMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
# Disable X-Frame-Options middleware to allow Flatlogic preview iframes.
|
||||
# 'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
@ -180,3 +186,7 @@ if EMAIL_USE_SSL:
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
GOOGLE_MAPS_API_KEY = os.getenv("GOOGLE_MAPS_API_KEY", "AIzaSyAluZTEjH-RSiGJUHnfrSqWbcAXCGzGOq4")
|
||||
LOGIN_URL = 'login'
|
||||
LOGIN_REDIRECT_URL = 'index'
|
||||
LOGOUT_REDIRECT_URL = 'login'
|
||||
@ -12,16 +12,20 @@ Class-based views
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
from django.views.i18n import JavaScriptCatalog
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
from django.views.i18n import JavaScriptCatalog
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
path("jsi18n/", JavaScriptCatalog.as_view(), name="jsi18n"),
|
||||
path("", include("core.urls")),
|
||||
path("accounts/", include("django.contrib.auth.urls")),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
core/__pycache__/forms.cpython-311.pyc
Normal file
BIN
core/__pycache__/forms.cpython-311.pyc
Normal file
Binary file not shown.
BIN
core/__pycache__/middleware.cpython-311.pyc
Normal file
BIN
core/__pycache__/middleware.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
core/__pycache__/permissions.cpython-311.pyc
Normal file
BIN
core/__pycache__/permissions.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
2003
core/admin.py
2003
core/admin.py
File diff suppressed because it is too large
Load Diff
@ -1,13 +1,31 @@
|
||||
import os
|
||||
import time
|
||||
from django.conf import settings
|
||||
from .models import Tenant
|
||||
from .permissions import can_view_donations, can_edit_voter, get_user_role, can_view_volunteers, can_edit_volunteer, can_view_voters
|
||||
|
||||
def project_context(request):
|
||||
"""
|
||||
Adds project-specific environment variables to the template context globally.
|
||||
"""
|
||||
return {
|
||||
context = {
|
||||
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
|
||||
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
||||
# Used for cache-busting static assets
|
||||
"deployment_timestamp": int(time.time()),
|
||||
"GOOGLE_MAPS_API_KEY": getattr(settings, 'GOOGLE_MAPS_API_KEY', ''),
|
||||
}
|
||||
|
||||
if request.user.is_authenticated:
|
||||
tenant_id = request.session.get('tenant_id')
|
||||
if tenant_id:
|
||||
tenant = Tenant.objects.filter(id=tenant_id).first()
|
||||
if tenant:
|
||||
context['can_view_donations'] = can_view_donations(request.user, tenant)
|
||||
context['can_edit_voter'] = can_edit_voter(request.user, tenant)
|
||||
context['can_view_voters'] = can_view_voters(request.user, tenant)
|
||||
context['can_view_volunteers'] = can_view_volunteers(request.user, tenant)
|
||||
context['can_edit_volunteer'] = can_edit_volunteer(request.user, tenant)
|
||||
context['user_role'] = get_user_role(request.user, tenant)
|
||||
|
||||
return context
|
||||
|
||||
498
core/forms.py
Normal file
498
core/forms.py
Normal file
@ -0,0 +1,498 @@
|
||||
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'})
|
||||
68
core/middleware.py
Normal file
68
core/middleware.py
Normal file
@ -0,0 +1,68 @@
|
||||
import zoneinfo
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from core.models import CampaignSettings, Tenant
|
||||
|
||||
class LoginRequiredMiddleware:
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
if not request.user.is_authenticated:
|
||||
path = request.path_info
|
||||
|
||||
# Allow access to login, logout, admin, and any other exempted paths
|
||||
try:
|
||||
login_url = reverse('login')
|
||||
logout_url = reverse('logout')
|
||||
except:
|
||||
login_url = '/accounts/login/'
|
||||
logout_url = '/accounts/logout/'
|
||||
|
||||
exempt_urls = [
|
||||
login_url,
|
||||
logout_url,
|
||||
'/admin/',
|
||||
]
|
||||
|
||||
# Check if path starts with any of the exempt URLs
|
||||
is_exempt = any(path.startswith(url) for url in exempt_urls)
|
||||
|
||||
if not is_exempt:
|
||||
return redirect(f"{login_url}?next={path}")
|
||||
|
||||
response = self.get_response(request)
|
||||
return response
|
||||
|
||||
class TimezoneMiddleware:
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
tzname = None
|
||||
|
||||
# 1. Try to get tenant from session
|
||||
tenant_id = request.session.get("tenant_id")
|
||||
if tenant_id:
|
||||
try:
|
||||
campaign_settings = CampaignSettings.objects.get(tenant_id=tenant_id)
|
||||
tzname = campaign_settings.timezone
|
||||
except CampaignSettings.DoesNotExist:
|
||||
pass
|
||||
|
||||
# 2. If not found and user is authenticated, maybe they are in admin?
|
||||
# In admin, we might not have tenant_id in session if they went directly there.
|
||||
# But this is a multi-tenant app, usually they select a campaign first.
|
||||
# If they are superuser in admin, we might want to default to something or let them see UTC.
|
||||
|
||||
if tzname:
|
||||
try:
|
||||
timezone.activate(zoneinfo.ZoneInfo(tzname))
|
||||
except:
|
||||
timezone.deactivate()
|
||||
else:
|
||||
timezone.deactivate()
|
||||
|
||||
return self.get_response(request)
|
||||
78
core/migrations/0001_initial.py
Normal file
78
core/migrations/0001_initial.py
Normal file
@ -0,0 +1,78 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-24 05:12
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Tenant',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('slug', models.SlugField(blank=True, unique=True)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Voter',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('voter_id', models.CharField(blank=True, max_length=50)),
|
||||
('first_name', models.CharField(max_length=100)),
|
||||
('last_name', models.CharField(max_length=100)),
|
||||
('address', models.TextField(blank=True)),
|
||||
('phone', models.CharField(blank=True, max_length=20)),
|
||||
('email', models.EmailField(blank=True, max_length=254)),
|
||||
('geocode', models.CharField(blank=True, max_length=100)),
|
||||
('district', models.CharField(blank=True, max_length=100)),
|
||||
('precinct', models.CharField(blank=True, max_length=100)),
|
||||
('registration_date', models.DateField(blank=True, null=True)),
|
||||
('candidate_support', models.CharField(choices=[('unknown', 'Unknown'), ('supporting', 'Supporting'), ('not_supporting', 'Not Supporting')], default='unknown', max_length=20)),
|
||||
('yard_sign', models.CharField(choices=[('none', 'None'), ('wants', 'Wants a yard sign'), ('has', 'Has a yard sign')], default='none', max_length=20)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='voters', to='core.tenant')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='InteractionType',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interaction_types', to='core.tenant')),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('tenant', 'name')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ElectionType',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='election_types', to='core.tenant')),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('tenant', 'name')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='DonationMethod',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='donation_methods', to='core.tenant')),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('tenant', 'name')},
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,75 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-24 05:18
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Donation',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('date', models.DateField()),
|
||||
('amount', models.DecimalField(decimal_places=2, max_digits=10)),
|
||||
('method', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.donationmethod')),
|
||||
('voter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='donations', to='core.voter')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Event',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('date', models.DateField()),
|
||||
('event_type', models.CharField(max_length=100)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='events', to='core.tenant')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='EventParticipation',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='participations', to='core.event')),
|
||||
('voter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='event_participations', to='core.voter')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Interaction',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('date', models.DateField()),
|
||||
('description', models.CharField(max_length=255)),
|
||||
('notes', models.TextField(blank=True)),
|
||||
('type', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.interactiontype')),
|
||||
('voter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interactions', to='core.voter')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='VotingRecord',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('election_date', models.DateField()),
|
||||
('election_description', models.CharField(max_length=255)),
|
||||
('primary_party', models.CharField(blank=True, max_length=100)),
|
||||
('voter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='voting_records', to='core.voter')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='VoterLikelihood',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('likelihood', models.CharField(choices=[('not_likely', 'Not Likely'), ('somewhat_likely', 'Somewhat Likely'), ('very_likely', 'Very Likely')], max_length=20)),
|
||||
('election_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.electiontype')),
|
||||
('voter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='likelihoods', to='core.voter')),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('voter', 'election_type')},
|
||||
},
|
||||
),
|
||||
]
|
||||
28
core/migrations/0003_tenantuserrole.py
Normal file
28
core/migrations/0003_tenantuserrole.py
Normal file
@ -0,0 +1,28 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-24 05:27
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0002_donation_event_eventparticipation_interaction_and_more'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='TenantUserRole',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('role', models.CharField(choices=[('system_admin', 'System Administrator'), ('campaign_admin', 'Campaign Administrator'), ('campaign_staff', 'Campaign Staff')], max_length=20)),
|
||||
('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_roles', to='core.tenant')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tenant_roles', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('user', 'tenant', 'role')},
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,50 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-24 14:50
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0003_tenantuserrole'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='voter',
|
||||
name='geocode',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='donationmethod',
|
||||
name='is_active',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='electiontype',
|
||||
name='is_active',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interactiontype',
|
||||
name='is_active',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='EventType',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='event_types', to='core.tenant')),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('tenant', 'name')},
|
||||
},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='event_type',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='core.eventtype'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-24 16:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0004_remove_voter_geocode_donationmethod_is_active_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='eventparticipation',
|
||||
name='participation_type',
|
||||
field=models.CharField(choices=[('invited', 'Invited'), ('invited_not_attended', "Invited but didn't attend"), ('attended', 'Attended')], default='invited', max_length=50),
|
||||
),
|
||||
]
|
||||
18
core/migrations/0006_voter_is_targeted.py
Normal file
18
core/migrations/0006_voter_is_targeted.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-24 16:33
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0005_eventparticipation_participation_type'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='voter',
|
||||
name='is_targeted',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,48 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-24 16:59
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0006_voter_is_targeted'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='voter',
|
||||
name='address_street',
|
||||
field=models.CharField(blank=True, max_length=255),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='voter',
|
||||
name='city',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='voter',
|
||||
name='county',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='voter',
|
||||
name='latitude',
|
||||
field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='voter',
|
||||
name='longitude',
|
||||
field=models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='voter',
|
||||
name='state',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='voter',
|
||||
name='zip_code',
|
||||
field=models.CharField(blank=True, max_length=20),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-24 21:57
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0007_voter_address_street_voter_city_voter_county_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='voter',
|
||||
name='latitude',
|
||||
field=models.DecimalField(blank=True, decimal_places=9, max_digits=12, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='voter',
|
||||
name='longitude',
|
||||
field=models.DecimalField(blank=True, decimal_places=9, max_digits=12, null=True),
|
||||
),
|
||||
]
|
||||
18
core/migrations/0009_voter_window_sticker.py
Normal file
18
core/migrations/0009_voter_window_sticker.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-24 23:46
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0008_alter_voter_latitude_alter_voter_longitude'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='voter',
|
||||
name='window_sticker',
|
||||
field=models.CharField(choices=[('none', 'None'), ('wants', 'Wants Sticker'), ('has', 'Has Sticker')], default='none', max_length=20),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,31 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-24 23:58
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0009_voter_window_sticker'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='voter',
|
||||
name='window_sticker',
|
||||
field=models.CharField(choices=[('none', 'None'), ('wants', 'Wants Sticker'), ('has', 'Has Sticker')], default='none', max_length=20, verbose_name='Window Sticker Status'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CampaignSettings',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('donation_goal', models.DecimalField(decimal_places=2, default=170000.0, max_digits=12)),
|
||||
('tenant', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='settings', to='core.tenant')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Campaign Settings',
|
||||
'verbose_name_plural': 'Campaign Settings',
|
||||
},
|
||||
),
|
||||
]
|
||||
23
core/migrations/0011_voter_birthdate_voter_nickname.py
Normal file
23
core/migrations/0011_voter_birthdate_voter_nickname.py
Normal file
@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-25 00:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0010_alter_voter_window_sticker_campaignsettings'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='voter',
|
||||
name='birthdate',
|
||||
field=models.DateField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='voter',
|
||||
name='nickname',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
]
|
||||
23
core/migrations/0012_voter_prior_state_alter_voter_state.py
Normal file
23
core/migrations/0012_voter_prior_state_alter_voter_state.py
Normal file
@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-25 01:02
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0011_voter_birthdate_voter_nickname'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='voter',
|
||||
name='prior_state',
|
||||
field=models.CharField(blank=True, max_length=2),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='voter',
|
||||
name='state',
|
||||
field=models.CharField(blank=True, max_length=2),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,71 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-25 16:33
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0012_voter_prior_state_alter_voter_state'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='tenant',
|
||||
name='description',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='tenant',
|
||||
name='slug',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='tenant',
|
||||
name='name',
|
||||
field=models.CharField(max_length=100),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='tenantuserrole',
|
||||
name='role',
|
||||
field=models.CharField(choices=[('admin', 'Admin'), ('campaign_manager', 'Campaign Manager'), ('campaign_staff', 'Campaign Staff')], max_length=20),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Interest',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interests', to='core.tenant')),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('tenant', 'name')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Volunteer',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('email', models.EmailField(max_length=254)),
|
||||
('phone', models.CharField(blank=True, max_length=20)),
|
||||
('interests', models.ManyToManyField(blank=True, related_name='volunteers', to='core.interest')),
|
||||
('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='volunteers', to='core.tenant')),
|
||||
('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='volunteer_profile', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interaction',
|
||||
name='volunteer',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='interactions', to='core.volunteer'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='VolunteerEvent',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('role', models.CharField(max_length=100)),
|
||||
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='volunteers', to='core.event')),
|
||||
('volunteer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='event_assignments', to='core.volunteer')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,24 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-25 16:34
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0013_remove_tenant_description_remove_tenant_slug_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='volunteer',
|
||||
name='assigned_events',
|
||||
field=models.ManyToManyField(related_name='assigned_volunteers', through='core.VolunteerEvent', to='core.event'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='volunteerevent',
|
||||
name='event',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='volunteer_assignments', to='core.event'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-25 18:30
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0014_volunteer_assigned_events_alter_volunteerevent_event'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='eventparticipation',
|
||||
old_name='participation_type',
|
||||
new_name='participation_status',
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,37 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-25 18:51
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0015_remove_eventparticipation_participation_type_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='eventparticipation',
|
||||
name='participation_status',
|
||||
field=models.CharField(blank=True, max_length=50),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ParticipationStatus',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='participation_statuses', to='core.tenant')),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Participation Statuses',
|
||||
'unique_together': {('tenant', 'name')},
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='eventparticipation',
|
||||
name='participation_status_link',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='core.participationstatus'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-25 18:52
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0016_alter_eventparticipation_participation_status_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='eventparticipation',
|
||||
name='participation_status',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='eventparticipation',
|
||||
old_name='participation_status_link',
|
||||
new_name='participation_status',
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,28 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-25 19:33
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0017_remove_eventparticipation_participation_status_link_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='end_time',
|
||||
field=models.TimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='name',
|
||||
field=models.CharField(blank=True, max_length=255),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='start_time',
|
||||
field=models.TimeField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-26 05:27
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0018_event_end_time_event_name_event_start_time'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='volunteer',
|
||||
name='first_name',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='volunteer',
|
||||
name='last_name',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
]
|
||||
17
core/migrations/0020_remove_volunteer_name.py
Normal file
17
core/migrations/0020_remove_volunteer_name.py
Normal file
@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-26 13:59
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0019_volunteer_first_name_volunteer_last_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='volunteer',
|
||||
name='name',
|
||||
),
|
||||
]
|
||||
18
core/migrations/0021_voter_phone_type.py
Normal file
18
core/migrations/0021_voter_phone_type.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-26 16:13
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0020_remove_volunteer_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='voter',
|
||||
name='phone_type',
|
||||
field=models.CharField(choices=[('home', 'Home Phone'), ('cell', 'Cell Phone'), ('work', 'Work Phone')], default='cell', max_length=10),
|
||||
),
|
||||
]
|
||||
18
core/migrations/0022_voter_notes.py
Normal file
18
core/migrations/0022_voter_notes.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-26 17:51
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0021_voter_phone_type'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='voter',
|
||||
name='notes',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,83 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-28 04:34
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0022_voter_notes'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='voter',
|
||||
name='address_street',
|
||||
field=models.CharField(blank=True, db_index=True, max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='voter',
|
||||
name='birthdate',
|
||||
field=models.DateField(blank=True, db_index=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='voter',
|
||||
name='candidate_support',
|
||||
field=models.CharField(choices=[('unknown', 'Unknown'), ('supporting', 'Supporting'), ('not_supporting', 'Not Supporting')], db_index=True, default='unknown', max_length=20),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='voter',
|
||||
name='city',
|
||||
field=models.CharField(blank=True, db_index=True, max_length=100),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='voter',
|
||||
name='district',
|
||||
field=models.CharField(blank=True, db_index=True, max_length=100),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='voter',
|
||||
name='first_name',
|
||||
field=models.CharField(db_index=True, max_length=100),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='voter',
|
||||
name='is_targeted',
|
||||
field=models.BooleanField(db_index=True, default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='voter',
|
||||
name='last_name',
|
||||
field=models.CharField(db_index=True, max_length=100),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='voter',
|
||||
name='precinct',
|
||||
field=models.CharField(blank=True, db_index=True, max_length=100),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='voter',
|
||||
name='state',
|
||||
field=models.CharField(blank=True, db_index=True, max_length=2),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='voter',
|
||||
name='voter_id',
|
||||
field=models.CharField(blank=True, db_index=True, max_length=50),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='voter',
|
||||
name='window_sticker',
|
||||
field=models.CharField(choices=[('none', 'None'), ('wants', 'Wants Sticker'), ('has', 'Has Sticker')], db_index=True, default='none', max_length=20, verbose_name='Window Sticker Status'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='voter',
|
||||
name='yard_sign',
|
||||
field=models.CharField(choices=[('none', 'None'), ('wants', 'Wants a yard sign'), ('has', 'Has a yard sign')], db_index=True, default='none', max_length=20),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='voter',
|
||||
name='zip_code',
|
||||
field=models.CharField(blank=True, db_index=True, max_length=20),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,22 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-28 21:21
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0023_alter_voter_address_street_alter_voter_birthdate_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='name',
|
||||
field=models.CharField(db_index=True, max_length=255),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='event',
|
||||
unique_together={('tenant', 'name')},
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,28 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-29 01:05
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0024_alter_event_name_alter_event_unique_together'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='campaignsettings',
|
||||
name='twilio_account_sid',
|
||||
field=models.CharField(blank=True, default='ACcd11acb5095cec6477245d385a2bf127', max_length=100),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='campaignsettings',
|
||||
name='twilio_auth_token',
|
||||
field=models.CharField(blank=True, default='89ec830d0fa02ab0afa6c76084865713', max_length=100),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='campaignsettings',
|
||||
name='twilio_from_number',
|
||||
field=models.CharField(blank=True, default='+18556945903', max_length=20),
|
||||
),
|
||||
]
|
||||
18
core/migrations/0026_alter_interaction_date.py
Normal file
18
core/migrations/0026_alter_interaction_date.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-29 03:41
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0025_campaignsettings_twilio_account_sid_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='interaction',
|
||||
name='date',
|
||||
field=models.DateTimeField(),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-29 18:48
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0026_alter_interaction_date'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='voter',
|
||||
name='secondary_phone',
|
||||
field=models.CharField(blank=True, max_length=20),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='voter',
|
||||
name='secondary_phone_type',
|
||||
field=models.CharField(choices=[('home', 'Home Phone'), ('cell', 'Cell Phone'), ('work', 'Work Phone')], default='cell', max_length=10),
|
||||
),
|
||||
]
|
||||
18
core/migrations/0028_volunteer_notes.py
Normal file
18
core/migrations/0028_volunteer_notes.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-29 21:45
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0027_voter_secondary_phone_voter_secondary_phone_type'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='volunteer',
|
||||
name='notes',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,43 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-29 22:15
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0028_volunteer_notes'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='address',
|
||||
field=models.CharField(blank=True, max_length=255),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='city',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='latitude',
|
||||
field=models.DecimalField(blank=True, decimal_places=9, max_digits=12, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='longitude',
|
||||
field=models.DecimalField(blank=True, decimal_places=9, max_digits=12, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='state',
|
||||
field=models.CharField(blank=True, max_length=2),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='zip_code',
|
||||
field=models.CharField(blank=True, max_length=20),
|
||||
),
|
||||
]
|
||||
18
core/migrations/0030_event_location_name.py
Normal file
18
core/migrations/0030_event_location_name.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-29 22:24
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0029_event_address_event_city_event_latitude_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='location_name',
|
||||
field=models.CharField(blank=True, max_length=255),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,41 @@
|
||||
# Generated by Django 5.2.7 on 2026-01-31 13:00
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0030_event_location_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='VolunteerRole',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='volunteer_roles', to='core.tenant')),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('tenant', 'name')},
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='default_volunteer_role',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_for_events', to='core.volunteerrole'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='eventtype',
|
||||
name='available_roles',
|
||||
field=models.ManyToManyField(blank=True, related_name='event_types', to='core.volunteerrole'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='volunteerevent',
|
||||
name='role_type',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='volunteer_assignments', to='core.volunteerrole'),
|
||||
),
|
||||
]
|
||||
18
core/migrations/0032_alter_volunteerevent_role.py
Normal file
18
core/migrations/0032_alter_volunteerevent_role.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-01 00:20
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0031_volunteerrole_event_default_volunteer_role_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='volunteerevent',
|
||||
name='role',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
]
|
||||
17
core/migrations/0033_remove_volunteerevent_role.py
Normal file
17
core/migrations/0033_remove_volunteerevent_role.py
Normal file
@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-01 00:54
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0032_alter_volunteerevent_role'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='volunteerevent',
|
||||
name='role',
|
||||
),
|
||||
]
|
||||
19
core/migrations/0034_eventtype_default_volunteer_role.py
Normal file
19
core/migrations/0034_eventtype_default_volunteer_role.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-01 01:02
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0033_remove_volunteerevent_role'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='eventtype',
|
||||
name='default_volunteer_role',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_for_event_types', to='core.volunteerrole'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,43 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-01 01:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0034_eventtype_default_volunteer_role'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='interaction',
|
||||
name='door_visit',
|
||||
field=models.BooleanField(db_index=True, default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interaction',
|
||||
name='neighborhood',
|
||||
field=models.CharField(blank=True, db_index=True, max_length=100),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='volunteer',
|
||||
name='door_visit',
|
||||
field=models.BooleanField(db_index=True, default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='volunteer',
|
||||
name='neighborhood',
|
||||
field=models.CharField(blank=True, db_index=True, max_length=100),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='voter',
|
||||
name='door_visit',
|
||||
field=models.BooleanField(db_index=True, default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='voter',
|
||||
name='neighborhood',
|
||||
field=models.CharField(blank=True, db_index=True, max_length=100),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,29 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-01 01:55
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0035_interaction_door_visit_interaction_neighborhood_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='interaction',
|
||||
name='door_visit',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='interaction',
|
||||
name='neighborhood',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='volunteer',
|
||||
name='door_visit',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='volunteer',
|
||||
name='neighborhood',
|
||||
),
|
||||
]
|
||||
18
core/migrations/0037_campaignsettings_timezone.py
Normal file
18
core/migrations/0037_campaignsettings_timezone.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-01 03:07
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0036_remove_interaction_door_visit_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='campaignsettings',
|
||||
name='timezone',
|
||||
field=models.CharField(default='America/Chicago', max_length=50),
|
||||
),
|
||||
]
|
||||
18
core/migrations/0038_alter_campaignsettings_timezone.py
Normal file
18
core/migrations/0038_alter_campaignsettings_timezone.py
Normal file
File diff suppressed because one or more lines are too long
18
core/migrations/0039_alter_tenantuserrole_role.py
Normal file
18
core/migrations/0039_alter_tenantuserrole_role.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-01 15:57
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0038_alter_campaignsettings_timezone'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='tenantuserrole',
|
||||
name='role',
|
||||
field=models.CharField(choices=[('system_admin', 'System Administrator'), ('campaign_admin', 'Campaign Administrator'), ('campaign_staff', 'Campaign Staff')], max_length=20),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,32 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-03 01:13
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0039_alter_tenantuserrole_role'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='volunteer',
|
||||
name='is_default_caller',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ScheduledCall',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('comments', models.TextField(blank=True)),
|
||||
('status', models.CharField(choices=[('pending', 'Pending'), ('completed', 'Completed'), ('cancelled', 'Cancelled')], default='pending', max_length=20)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('tenant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_calls', to='core.tenant')),
|
||||
('volunteer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned_calls', to='core.volunteer')),
|
||||
('voter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_calls', to='core.voter')),
|
||||
],
|
||||
),
|
||||
]
|
||||
17
core/migrations/0041_alter_volunteer_options.py
Normal file
17
core/migrations/0041_alter_volunteer_options.py
Normal file
@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-03 03:43
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0040_volunteer_is_default_caller_scheduledcall'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='volunteer',
|
||||
options={'ordering': ('last_name', 'first_name')},
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,48 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-11 15:23
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0041_alter_volunteer_options'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='campaignsettings',
|
||||
name='email_from_address',
|
||||
field=models.EmailField(blank=True, max_length=254),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='campaignsettings',
|
||||
name='smtp_host',
|
||||
field=models.CharField(blank=True, max_length=255),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='campaignsettings',
|
||||
name='smtp_password',
|
||||
field=models.CharField(blank=True, max_length=255),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='campaignsettings',
|
||||
name='smtp_port',
|
||||
field=models.IntegerField(default=587),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='campaignsettings',
|
||||
name='smtp_use_ssl',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='campaignsettings',
|
||||
name='smtp_use_tls',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='campaignsettings',
|
||||
name='smtp_username',
|
||||
field=models.CharField(blank=True, max_length=255),
|
||||
),
|
||||
]
|
||||
BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
core/migrations/__pycache__/0003_tenantuserrole.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0003_tenantuserrole.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
core/migrations/__pycache__/0022_voter_notes.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0022_voter_notes.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
core/migrations/__pycache__/0028_volunteer_notes.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0028_volunteer_notes.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user