diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index bcabe2c..ac91214 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc index d062e20..92d31a3 100644 Binary files a/core/__pycache__/forms.cpython-311.pyc and b/core/__pycache__/forms.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index fcae47b..7fdfe9b 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 57229ea..a732775 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 51419e3..d9e576b 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index 40b43cc..54572ac 100644 --- a/core/admin.py +++ b/core/admin.py @@ -35,6 +35,7 @@ VOTER_MAPPABLE_FIELDS = [ ('zip_code', 'Zip Code'), ('county', 'County'), ('phone', 'Phone'), + ('phone_type', 'Phone Type'), ('email', 'Email'), ('district', 'District'), ('precinct', 'Precinct'), @@ -203,7 +204,7 @@ class VolunteerEventInline(admin.TabularInline): @admin.register(Voter) class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin): list_display = ('first_name', 'last_name', 'nickname', 'voter_id', 'tenant', 'district', 'candidate_support', 'is_targeted', 'city', 'state', 'prior_state') - list_filter = ('tenant', 'candidate_support', 'is_targeted', 'yard_sign', 'district', 'city', 'state', 'prior_state') + list_filter = ('tenant', 'candidate_support', 'is_targeted', 'phone_type', 'yard_sign', 'district', 'city', 'state', 'prior_state') search_fields = ('first_name', 'last_name', 'nickname', 'voter_id', 'address', 'city', 'state', 'prior_state', 'zip_code', 'county') inlines = [VotingRecordInline, DonationInline, InteractionInline, VoterLikelihoodInline] readonly_fields = ('address',) @@ -317,12 +318,34 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin): if 'window_sticker' in voter_data: if voter_data['window_sticker'] not in dict(Voter.WINDOW_STICKER_CHOICES): voter_data['window_sticker'] = 'none' + if 'phone_type' in voter_data: + pt_val = str(voter_data['phone_type']).lower() + pt_choices = dict(Voter.PHONE_TYPE_CHOICES) + if pt_val not in pt_choices: + # Try to match by display name + found = False + for k, v in pt_choices.items(): + if v.lower() == pt_val: + voter_data['phone_type'] = k + found = True + break + if not found: + voter_data['phone_type'] = 'cell' + else: + voter_data['phone_type'] = pt_val - Voter.objects.update_or_create( + voter, created = Voter.objects.get_or_create( tenant=tenant, voter_id=voter_id, - defaults=voter_data ) + for key, value in voter_data.items(): + setattr(voter, key, value) + + # Flag that coordinates were provided in the import to avoid geocoding + if "latitude" in voter_data and "longitude" in voter_data: + voter._coords_provided_in_import = True + + voter.save() count += 1 except Exception as e: logger.error(f"Error importing: {e}") diff --git a/core/forms.py b/core/forms.py index d9ca81b..a281af4 100644 --- a/core/forms.py +++ b/core/forms.py @@ -7,7 +7,7 @@ class VoterForm(forms.ModelForm): fields = [ 'first_name', 'last_name', 'nickname', 'birthdate', 'address_street', 'city', 'state', 'prior_state', 'zip_code', 'county', 'latitude', 'longitude', - 'phone', 'email', 'voter_id', 'district', 'precinct', + 'phone', 'phone_type', 'email', 'voter_id', 'district', 'precinct', 'registration_date', 'is_targeted', 'candidate_support', 'yard_sign', 'window_sticker' ] widgets = { @@ -30,6 +30,55 @@ class VoterForm(forms.ModelForm): 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 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) + 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) + district = forms.CharField(required=False) + precinct = forms.CharField(required=False) + phone_type = forms.ChoiceField( + choices=[('', 'Any')] + Voter.PHONE_TYPE_CHOICES, + required=False + ) + is_targeted = forms.BooleanField(required=False, label="Targeted Only") + candidate_support = forms.ChoiceField( + choices=[('', 'Any')] + Voter.SUPPORT_CHOICES, + required=False + ) + yard_sign = forms.ChoiceField( + choices=[('', 'Any')] + Voter.YARD_SIGN_CHOICES, + required=False + ) + window_sticker = forms.ChoiceField( + choices=[('', 'Any')] + Voter.WINDOW_STICKER_CHOICES, + required=False + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for field in self.fields.values(): + if isinstance(field.widget, forms.CheckboxInput): + field.widget.attrs.update({'class': 'form-check-input'}) + else: + field.widget.attrs.update({'class': 'form-control'}) + + self.fields['birth_month'].widget.attrs.update({'class': 'form-select'}) + self.fields['candidate_support'].widget.attrs.update({'class': 'form-select'}) + self.fields['yard_sign'].widget.attrs.update({'class': 'form-select'}) + self.fields['window_sticker'].widget.attrs.update({'class': 'form-select'}) + self.fields['phone_type'].widget.attrs.update({'class': 'form-select'}) class InteractionForm(forms.ModelForm): class Meta: @@ -173,4 +222,4 @@ class VolunteerImportForm(forms.Form): 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'}) + self.fields['file'].widget.attrs.update({'class': 'form-control'}) \ No newline at end of file diff --git a/core/migrations/0021_voter_phone_type.py b/core/migrations/0021_voter_phone_type.py new file mode 100644 index 0000000..81f3f7a --- /dev/null +++ b/core/migrations/0021_voter_phone_type.py @@ -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), + ), + ] diff --git a/core/migrations/__pycache__/0021_voter_phone_type.cpython-311.pyc b/core/migrations/__pycache__/0021_voter_phone_type.cpython-311.pyc new file mode 100644 index 0000000..af9f7b9 Binary files /dev/null and b/core/migrations/__pycache__/0021_voter_phone_type.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 9198711..e847349 100644 --- a/core/models.py +++ b/core/models.py @@ -126,6 +126,11 @@ class Voter(models.Model): ('wants', 'Wants Sticker'), ('has', 'Has Sticker'), ] + PHONE_TYPE_CHOICES = [ + ('home', 'Home Phone'), + ('cell', 'Cell Phone'), + ('work', 'Work Phone'), + ] tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name='voters') voter_id = models.CharField(max_length=50, blank=True) @@ -143,6 +148,7 @@ class Voter(models.Model): latitude = models.DecimalField(max_digits=12, decimal_places=9, null=True, blank=True) longitude = models.DecimalField(max_digits=12, decimal_places=9, null=True, blank=True) phone = models.CharField(max_length=20, blank=True) + phone_type = models.CharField(max_length=10, choices=PHONE_TYPE_CHOICES, default='cell') email = models.EmailField(blank=True) district = models.CharField(max_length=100, blank=True) precinct = models.CharField(max_length=100, blank=True) @@ -239,9 +245,12 @@ class Voter(models.Model): self.state != orig.state or self.zip_code != orig.zip_code) - # Detect if coordinates were changed in this transaction (e.g., from a form) coords_provided = (self.latitude != orig.latitude or self.longitude != orig.longitude) + # If specifically provided in import, treat as provided even if same as DB + if getattr(self, "_coords_provided_in_import", False): + coords_provided = True + # Auto-geocode if address changed AND coordinates were NOT manually updated if address_changed and not coords_provided: should_geocode = True diff --git a/core/templates/core/index.html b/core/templates/core/index.html index cde8d7b..8000f67 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -30,7 +30,7 @@ -