diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index a5ed392..5e21bfb 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 new file mode 100644 index 0000000..d4709a8 Binary files /dev/null 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 e061640..7f48f71 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 5a69659..0fba60a 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 2a36fd6..fca47c7 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 8c38f3f..ebf1823 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,3 +1,41 @@ from django.contrib import admin +from .models import Voter, VotingRecord, Donation, VoterContact, EventParticipation -# Register your models here. +class VotingRecordInline(admin.TabularInline): + model = VotingRecord + extra = 1 + +class DonationInline(admin.TabularInline): + model = Donation + extra = 1 + +class VoterContactInline(admin.TabularInline): + model = VoterContact + extra = 1 + +class EventParticipationInline(admin.TabularInline): + model = EventParticipation + extra = 1 + +@admin.register(Voter) +class VoterAdmin(admin.ModelAdmin): + list_display = ('voter_id', 'last_name', 'first_name', 'district', 'precinct', 'candidate_support') + list_filter = ('candidate_support', 'yard_sign_status', 'district') + search_fields = ('voter_id', 'last_name', 'first_name', 'email') + inlines = [VotingRecordInline, DonationInline, VoterContactInline, EventParticipationInline] + +@admin.register(VotingRecord) +class VotingRecordAdmin(admin.ModelAdmin): + list_display = ('voter', 'election_date', 'description') + +@admin.register(Donation) +class DonationAdmin(admin.ModelAdmin): + list_display = ('voter', 'donation_date', 'amount') + +@admin.register(VoterContact) +class VoterContactAdmin(admin.ModelAdmin): + list_display = ('voter', 'contact_type', 'contact_date') + +@admin.register(EventParticipation) +class EventParticipationAdmin(admin.ModelAdmin): + list_display = ('voter', 'event_type', 'event_date') \ No newline at end of file diff --git a/core/forms.py b/core/forms.py new file mode 100644 index 0000000..ef9e5a8 --- /dev/null +++ b/core/forms.py @@ -0,0 +1,49 @@ +from django import forms +from .models import Voter, VotingRecord, Donation, VoterContact, EventParticipation + +class VoterForm(forms.ModelForm): + class Meta: + model = Voter + fields = [ + 'first_name', 'last_name', 'voter_id', 'address', 'phone', 'email', + 'district', 'precinct', 'registration_date', 'likelihood_to_vote', + 'candidate_support', 'yard_sign_status' + ] + widgets = { + 'registration_date': forms.DateInput(attrs={'type': 'date'}), + 'address': forms.Textarea(attrs={'rows': 3}), + } + +class VotingRecordForm(forms.ModelForm): + class Meta: + model = VotingRecord + fields = ['election_date', 'description', 'primary_party'] + widgets = { + 'election_date': forms.DateInput(attrs={'type': 'date'}), + } + +class DonationForm(forms.ModelForm): + class Meta: + model = Donation + fields = ['donation_date', 'amount', 'method'] + widgets = { + 'donation_date': forms.DateInput(attrs={'type': 'date'}), + } + +class VoterContactForm(forms.ModelForm): + class Meta: + model = VoterContact + fields = ['contact_type', 'contact_date', 'description', 'notes'] + widgets = { + 'contact_date': forms.DateTimeInput(attrs={'type': 'datetime-local'}), + 'notes': forms.Textarea(attrs={'rows': 3}), + } + +class EventParticipationForm(forms.ModelForm): + class Meta: + model = EventParticipation + fields = ['event_date', 'event_type', 'description'] + widgets = { + 'event_date': forms.DateInput(attrs={'type': 'date'}), + 'description': forms.Textarea(attrs={'rows': 3}), + } \ No newline at end of file diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..2aac963 --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,70 @@ +# Generated by Django 5.2.7 on 2026-01-24 03:04 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Voter', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('voter_id', models.CharField(max_length=50, unique=True)), + ('first_name', models.CharField(max_length=100)), + ('last_name', models.CharField(max_length=100)), + ('address', models.TextField()), + ('phone', models.CharField(blank=True, max_length=20, null=True)), + ('email', models.EmailField(blank=True, max_length=254, null=True)), + ('district', models.CharField(blank=True, max_length=100, null=True)), + ('precinct', models.CharField(blank=True, max_length=100, null=True)), + ('registration_date', models.DateField(blank=True, null=True)), + ('likelihood_to_vote', models.IntegerField(choices=[(1, '1'), (2, '2'), (3, '3'), (4, '4'), (5, '5')], default=3)), + ('candidate_support', models.CharField(choices=[('unknown', 'Unknown'), ('supporting', 'Supporting'), ('not_supporting', 'Not Supporting')], default='unknown', max_length=20)), + ('yard_sign_status', models.CharField(choices=[('none', 'None'), ('wants', 'Wants a Yard Sign'), ('has', 'Has a Yard Sign')], default='none', max_length=20)), + ('latitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)), + ('longitude', models.DecimalField(blank=True, decimal_places=6, max_digits=9, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + options={ + 'ordering': ['last_name', 'first_name'], + }, + ), + migrations.CreateModel( + name='Donation', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('donation_date', models.DateField()), + ('amount', models.DecimalField(decimal_places=2, max_digits=10)), + ('voter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='donations', to='core.voter')), + ], + ), + migrations.CreateModel( + name='VoterContact', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('contact_type', models.CharField(choices=[('Phone', 'Phone'), ('Door Visit', 'Door Visit'), ('Mail', 'Mail')], max_length=20)), + ('contact_date', models.DateTimeField()), + ('description', models.CharField(max_length=255)), + ('notes', models.TextField(blank=True, null=True)), + ('voter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contacts', 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()), + ('description', models.CharField(max_length=255)), + ('primary_party', models.CharField(blank=True, max_length=50, null=True)), + ('voter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='voting_records', to='core.voter')), + ], + ), + ] diff --git a/core/migrations/0002_eventparticipation.py b/core/migrations/0002_eventparticipation.py new file mode 100644 index 0000000..cfdc506 --- /dev/null +++ b/core/migrations/0002_eventparticipation.py @@ -0,0 +1,24 @@ +# Generated by Django 5.2.7 on 2026-01-24 03:23 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='EventParticipation', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('event_date', models.DateField()), + ('event_type', models.CharField(max_length=100)), + ('description', models.TextField(blank=True, null=True)), + ('voter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='event_participations', to='core.voter')), + ], + ), + ] diff --git a/core/migrations/0003_donation_method.py b/core/migrations/0003_donation_method.py new file mode 100644 index 0000000..95f1ffb --- /dev/null +++ b/core/migrations/0003_donation_method.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2026-01-24 03:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0002_eventparticipation'), + ] + + operations = [ + migrations.AddField( + model_name='donation', + name='method', + field=models.CharField(choices=[('Cash', 'Cash'), ('Check', 'Check'), ('Credit/Debit', 'Credit/Debit')], default='Check', max_length=20), + ), + ] diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc new file mode 100644 index 0000000..030c168 Binary files /dev/null and b/core/migrations/__pycache__/0001_initial.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0002_eventparticipation.cpython-311.pyc b/core/migrations/__pycache__/0002_eventparticipation.cpython-311.pyc new file mode 100644 index 0000000..9c87672 Binary files /dev/null and b/core/migrations/__pycache__/0002_eventparticipation.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0003_donation_method.cpython-311.pyc b/core/migrations/__pycache__/0003_donation_method.cpython-311.pyc new file mode 100644 index 0000000..e4b6ad0 Binary files /dev/null and b/core/migrations/__pycache__/0003_donation_method.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 71a8362..e57acdb 100644 --- a/core/models.py +++ b/core/models.py @@ -1,3 +1,95 @@ from django.db import models +from django.urls import reverse -# Create your models here. +class Voter(models.Model): + CANDIDATE_SUPPORT_CHOICES = [ + ('unknown', 'Unknown'), + ('supporting', 'Supporting'), + ('not_supporting', 'Not Supporting'), + ] + YARD_SIGN_CHOICES = [ + ('none', 'None'), + ('wants', 'Wants a Yard Sign'), + ('has', 'Has a Yard Sign'), + ] + LIKELIHOOD_CHOICES = [(i, str(i)) for i in range(1, 6)] + + voter_id = models.CharField(max_length=50, unique=True) + first_name = models.CharField(max_length=100) + last_name = models.CharField(max_length=100) + address = models.TextField() + phone = models.CharField(max_length=20, blank=True, null=True) + email = models.EmailField(blank=True, null=True) + + # Demographics + district = models.CharField(max_length=100, blank=True, null=True) + precinct = models.CharField(max_length=100, blank=True, null=True) + registration_date = models.DateField(blank=True, null=True) + + # Engagement + likelihood_to_vote = models.IntegerField(choices=LIKELIHOOD_CHOICES, default=3) + candidate_support = models.CharField(max_length=20, choices=CANDIDATE_SUPPORT_CHOICES, default='unknown') + yard_sign_status = models.CharField(max_length=20, choices=YARD_SIGN_CHOICES, default='none') + + # Geocode (Optional for map integration later) + latitude = models.DecimalField(max_digits=9, decimal_places=6, blank=True, null=True) + longitude = models.DecimalField(max_digits=9, decimal_places=6, blank=True, null=True) + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + ordering = ['last_name', 'first_name'] + + def __str__(self): + return f"{self.first_name} {self.last_name} ({self.voter_id})" + + def get_absolute_url(self): + return reverse('voter_detail', kwargs={'pk': self.pk}) + +class VotingRecord(models.Model): + voter = models.ForeignKey(Voter, on_delete=models.CASCADE, related_name='voting_records') + election_date = models.DateField() + description = models.CharField(max_length=255) + primary_party = models.CharField(max_length=50, blank=True, null=True) + + def __str__(self): + return f"{self.voter.last_name} - {self.election_date}" + +class Donation(models.Model): + METHOD_CHOICES = [ + ('Cash', 'Cash'), + ('Check', 'Check'), + ('Credit/Debit', 'Credit/Debit'), + ] + voter = models.ForeignKey(Voter, on_delete=models.CASCADE, related_name='donations') + donation_date = models.DateField() + amount = models.DecimalField(max_digits=10, decimal_places=2) + method = models.CharField(max_length=20, choices=METHOD_CHOICES, default='Check') + + def __str__(self): + return f"{self.voter.last_name} - ${self.amount} ({self.method})" + +class VoterContact(models.Model): + CONTACT_TYPE_CHOICES = [ + ('Phone', 'Phone'), + ('Door Visit', 'Door Visit'), + ('Mail', 'Mail'), + ] + voter = models.ForeignKey(Voter, on_delete=models.CASCADE, related_name='contacts') + contact_type = models.CharField(max_length=20, choices=CONTACT_TYPE_CHOICES) + contact_date = models.DateTimeField() + description = models.CharField(max_length=255) + notes = models.TextField(blank=True, null=True) + + def __str__(self): + return f"{self.voter.last_name} - {self.contact_type} on {self.contact_date.date()}" + +class EventParticipation(models.Model): + voter = models.ForeignKey(Voter, on_delete=models.CASCADE, related_name='event_participations') + event_date = models.DateField() + event_type = models.CharField(max_length=100) + description = models.TextField(blank=True, null=True) + + def __str__(self): + return f"{self.voter.last_name} - {self.event_type} on {self.event_date}" diff --git a/core/templates/base.html b/core/templates/base.html index 1e7e5fb..2998087 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -1,25 +1,75 @@ - - - {% block title %}Knowledge Base{% endblock %} - {% if project_description %} - - - - {% endif %} - {% if project_image_url %} - - - {% endif %} - {% load static %} - - {% block head %}{% endblock %} + + + {% block title %}Voter Campaign CRM{% endblock %} + + {% if project_description %} + + + + {% endif %} + {% if project_image_url %} + + + {% endif %} + + {% load static %} + + + + + + + + {% block head %}{% endblock %} - - {% block content %}{% endblock %} - + - + {% if messages %} +
+ {% for message in messages %} + + {% endfor %} +
+ {% endif %} + +
+ {% block content %}{% endblock %} +
+ + + + + + {% block scripts %}{% endblock %} + + \ No newline at end of file diff --git a/core/templates/core/index.html b/core/templates/core/index.html index faec813..03d1bf4 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -1,145 +1,88 @@ -{% extends "base.html" %} - -{% block title %}{{ project_name }}{% endblock %} - -{% block head %} - - - - -{% endblock %} +{% extends 'base.html' %} +{% load static %} {% block content %} -
-
-

Analyzing your requirements and generating your app…

-
- Loading… +
+
+

Voter Campaign CRM

+

Identify, Engaged, and Activate your base with a 360-degree voter view.

+ +
+
+
+
+ + +
+
+
+
-

AppWizzy AI is collecting your requirements and applying the first changes.

-

This page will refresh automatically as the plan is implemented.

-

- Runtime: Django {{ django_version }} · Python {{ python_version }} - — UTC {{ current_time|date:"Y-m-d H:i:s" }} -

-
-
- -{% endblock %} \ No newline at end of file + + +
+ +
+
+
+
Total Voters
+

{{ stats.total_voters }}

+
+
+
+
+
Active Support
+

{{ stats.supporters }}

+
+
+
+
+
Yard Signs
+

{{ stats.yard_signs }}

+
+
+
+ + +
+
+
+

{% if query %}Search Results{% else %}Recent Voters{% endif %}

+ + Add New Voter +
+ + {% if voters %} +
+ {% for voter in voters %} +
+
+
+
+
{{ voter.first_name }} {{ voter.last_name }}
+

ID: {{ voter.voter_id }}

+
+
+ + {{ voter.get_candidate_support_display }} + +
+
+
+ {{ voter.address|truncatechars:40 }} +
+ +
+
+ {% endfor %} +
+ {% else %} +
+

No voters found. Try a different search or add a new one.

+
+ {% endif %} +
+
+
+{% endblock %} diff --git a/core/templates/core/voter_detail.html b/core/templates/core/voter_detail.html new file mode 100644 index 0000000..3c2d385 --- /dev/null +++ b/core/templates/core/voter_detail.html @@ -0,0 +1,311 @@ +{% extends 'base.html' %} +{% load static %} + +{% block content %} +
+
+ ← Back to Dashboard +

{{ voter.first_name }} {{ voter.last_name }}

+

Voter ID: {{ voter.voter_id }} | {{ voter.district }} / {{ voter.precinct }}

+ +
+
+ +
+
+
+
+

Demographics & Contact

+
+
+ +

{{ voter.address }}

+
+
+ +

{{ voter.phone|default:"N/A" }}
{{ voter.email|default:"N/A" }}

+
+
+ +

{{ voter.registration_date|date:"M d, Y"|default:"Unknown" }}

+
+
+ +

{{ voter.district }} / {{ voter.precinct }}

+
+
+
+
+

Engagement

+
+ +
+
+
+ {{ voter.likelihood_to_vote }}/5 +
+
+ + + {{ voter.get_candidate_support_display }} + +
+
+ +

{{ voter.get_yard_sign_status_display }}

+
+
+
+
+ +
+ +
+
+
+
Voting
+ +
+ {% for record in voter.voting_records.all %} +
+ {{ record.description }} + {{ record.election_date|date:"Y" }} +
+ {% empty %} +

No voting records on file.

+ {% endfor %} +
+
+ + +
+
+
+
Donations
+ +
+ {% for donation in voter.donations.all %} +
+
+ ${{ donation.amount }} + {{ donation.donation_date|date:"M d, Y" }} +
+
{{ donation.method }}
+
+ {% empty %} +

No donation history.

+ {% endfor %} +
+
+ + +
+
+
+
Events
+ +
+ {% for participation in voter.event_participations.all %} +
+
+ {{ participation.event_type }} + {{ participation.event_date|date:"M d, Y" }} +
+ {% if participation.description %} +

{{ participation.description|truncatechars:50 }}

+ {% endif %} +
+ {% empty %} +

No events recorded.

+ {% endfor %} +
+
+ + +
+
+
+
Contacts
+ +
+ {% for contact in voter.contacts.all %} +
+
+ {{ contact.contact_type }} + {{ contact.contact_date|date:"M d, Y" }} +
+

{{ contact.description|truncatechars:50 }}

+
+ {% empty %} +

No contact history.

+ {% endfor %} +
+
+
+
+ + + + + + + + + + + + + + + + + + + +{% endblock %} \ No newline at end of file diff --git a/core/urls.py b/core/urls.py index 6299e3d..988d649 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,7 +1,12 @@ from django.urls import path - -from .views import home +from . import views urlpatterns = [ - path("", home, name="home"), -] + path('', views.index, name='index'), + path('voter//', views.voter_detail, name='voter_detail'), + path('voter//edit/', views.voter_edit, name='voter_edit'), + path('voter//add-voting-record/', views.add_voting_record, name='add_voting_record'), + path('voter//add-donation/', views.add_donation, name='add_donation'), + path('voter//add-contact/', views.add_contact, name='add_contact'), + path('voter//add-event/', views.add_event, name='add_event'), +] \ No newline at end of file diff --git a/core/views.py b/core/views.py index c9aed12..01fff2e 100644 --- a/core/views.py +++ b/core/views.py @@ -1,25 +1,98 @@ -import os -import platform +from django.shortcuts import render, get_object_or_404, redirect +from django.db.models import Q +from django.contrib import messages +from .models import Voter, VotingRecord, Donation, VoterContact, EventParticipation +from .forms import VoterForm, VotingRecordForm, DonationForm, VoterContactForm, EventParticipationForm -from django import get_version as django_version -from django.shortcuts import render -from django.utils import timezone - - -def home(request): - """Render the landing screen with loader and environment details.""" - host_name = request.get_host().lower() - agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic" - now = timezone.now() - - context = { - "project_name": "New Style", - "agent_brand": agent_brand, - "django_version": django_version(), - "python_version": platform.python_version(), - "current_time": now, - "host_name": host_name, - "project_description": os.getenv("PROJECT_DESCRIPTION", ""), - "project_image_url": os.getenv("PROJECT_IMAGE_URL", ""), +def index(request): + query = request.GET.get('q') + if query: + voters = Voter.objects.filter( + Q(first_name__icontains=query) | + Q(last_name__icontains=query) | + Q(voter_id__icontains=query) + )[:10] + else: + voters = Voter.objects.all().order_by('-created_at')[:5] + + stats = { + 'total_voters': Voter.objects.count(), + 'supporters': Voter.objects.filter(candidate_support='supporting').count(), + 'yard_signs': Voter.objects.filter(yard_sign_status='has').count(), } - return render(request, "core/index.html", context) + + return render(request, 'core/index.html', { + 'voters': voters, + 'query': query, + 'stats': stats + }) + +def voter_detail(request, pk): + voter = get_object_or_404(Voter, pk=pk) + + # Forms for adding related records (initialized as empty) + context = { + 'voter': voter, + 'voter_form': VoterForm(instance=voter), + 'voting_form': VotingRecordForm(), + 'donation_form': DonationForm(), + 'contact_form': VoterContactForm(), + 'event_form': EventParticipationForm(), + } + return render(request, 'core/voter_detail.html', context) + +def voter_edit(request, pk): + voter = get_object_or_404(Voter, pk=pk) + if request.method == 'POST': + form = VoterForm(request.POST, instance=voter) + if form.is_valid(): + form.save() + messages.success(request, 'Voter updated successfully.') + return redirect('voter_detail', pk=voter.pk) + else: + form = VoterForm(instance=voter) + return render(request, 'core/voter_detail.html', {'voter': voter, 'voter_form': form}) + +def add_voting_record(request, pk): + voter = get_object_or_404(Voter, pk=pk) + if request.method == 'POST': + form = VotingRecordForm(request.POST) + if form.is_valid(): + record = form.save(commit=False) + record.voter = voter + record.save() + messages.success(request, 'Voting record added.') + return redirect('voter_detail', pk=voter.pk) + +def add_donation(request, pk): + voter = get_object_or_404(Voter, pk=pk) + if request.method == 'POST': + form = DonationForm(request.POST) + if form.is_valid(): + donation = form.save(commit=False) + donation.voter = voter + donation.save() + messages.success(request, 'Donation added.') + return redirect('voter_detail', pk=voter.pk) + +def add_contact(request, pk): + voter = get_object_or_404(Voter, pk=pk) + if request.method == 'POST': + form = VoterContactForm(request.POST) + if form.is_valid(): + contact = form.save(commit=False) + contact.voter = voter + contact.save() + messages.success(request, 'Contact logged.') + return redirect('voter_detail', pk=voter.pk) + +def add_event(request, pk): + voter = get_object_or_404(Voter, pk=pk) + if request.method == 'POST': + form = EventParticipationForm(request.POST) + if form.is_valid(): + participation = form.save(commit=False) + participation.voter = voter + participation.save() + messages.success(request, 'Event participation added.') + return redirect('voter_detail', pk=voter.pk) diff --git a/populate_engagement.py b/populate_engagement.py new file mode 100644 index 0000000..5866cff --- /dev/null +++ b/populate_engagement.py @@ -0,0 +1,48 @@ +import os +import django +import random +from datetime import date, timedelta + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') +django.setup() + +from core.models import Voter, Donation, EventParticipation + +def populate_engagement_data(): + voters = Voter.objects.all() + + event_types = ['Town Hall', 'Neighborhood Walk', 'Phone Bank', 'Fundraiser Dinner', 'Rally'] + event_descriptions = [ + 'Discussed local infrastructure issues.', + 'Canvassed the precinct with volunteers.', + 'Made 50 calls to likely voters.', + 'Annual gala for the candidate.', + 'Major campaign kickoff event.' + ] + + for voter in voters: + # 30% chance of having donations + if random.random() < 0.3: + num_donations = random.randint(1, 4) + for _ in range(num_donations): + Donation.objects.create( + voter=voter, + donation_date=date.today() - timedelta(days=random.randint(1, 365)), + amount=random.choice([10, 25, 50, 100, 250, 500]) + ) + + # 40% chance of having events + if random.random() < 0.4: + num_events = random.randint(1, 3) + for _ in range(num_events): + EventParticipation.objects.create( + voter=voter, + event_date=date.today() - timedelta(days=random.randint(1, 180)), + event_type=random.choice(event_types), + description=random.choice(event_descriptions) + ) + + print(f"Populated donations and events for {voters.count()} voters.") + +if __name__ == '__main__': + populate_engagement_data() diff --git a/static/css/custom.css b/static/css/custom.css index 925f6ed..30cc01d 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -1,4 +1,75 @@ -/* Custom styles for the application */ -body { - font-family: system-ui, -apple-system, sans-serif; +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Outfit:wght@600;700&display=swap'); + +:root { + --primary-navy: #0F172A; + --electric-blue: #3B82F6; + --emerald-accent: #10B981; + --soft-bg: #F8FAFC; + --card-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); } + +body { + font-family: 'Inter', sans-serif; + background-color: var(--soft-bg); + color: var(--primary-navy); +} + +h1, h2, h3, h4, .navbar-brand { + font-family: 'Outfit', sans-serif; + font-weight: 700; +} + +.hero-section { + background: linear-gradient(135deg, var(--primary-navy) 0%, #1E293B 100%); + color: white; + padding: 80px 0; + border-bottom: 4px solid var(--electric-blue); +} + +.search-glass { + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 12px; + padding: 20px; +} + +.card { + border: none; + border-radius: 16px; + box-shadow: var(--card-shadow); + transition: transform 0.2s ease; +} + +.card:hover { + transform: translateY(-4px); +} + +.btn-primary { + background-color: var(--electric-blue); + border: none; + padding: 10px 24px; + border-radius: 8px; + font-weight: 500; +} + +.badge-support { + background-color: var(--emerald-accent); + color: white; +} + +.nav-link { + font-weight: 500; +} + +.stat-card { + border-left: 4px solid var(--electric-blue); +} + +.voter-profile-header { + background-color: white; + padding: 40px; + border-radius: 20px; + margin-top: -40px; + box-shadow: var(--card-shadow); +} \ No newline at end of file diff --git a/staticfiles/css/custom.css b/staticfiles/css/custom.css index 108056f..30cc01d 100644 --- a/staticfiles/css/custom.css +++ b/staticfiles/css/custom.css @@ -1,21 +1,75 @@ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Outfit:wght@600;700&display=swap'); :root { - --bg-color-start: #6a11cb; - --bg-color-end: #2575fc; - --text-color: #ffffff; - --card-bg-color: rgba(255, 255, 255, 0.01); - --card-border-color: rgba(255, 255, 255, 0.1); + --primary-navy: #0F172A; + --electric-blue: #3B82F6; + --emerald-accent: #10B981; + --soft-bg: #F8FAFC; + --card-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); } + body { - margin: 0; font-family: 'Inter', sans-serif; - background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end)); - color: var(--text-color); - display: flex; - justify-content: center; - align-items: center; - min-height: 100vh; - text-align: center; - overflow: hidden; - position: relative; + background-color: var(--soft-bg); + color: var(--primary-navy); } + +h1, h2, h3, h4, .navbar-brand { + font-family: 'Outfit', sans-serif; + font-weight: 700; +} + +.hero-section { + background: linear-gradient(135deg, var(--primary-navy) 0%, #1E293B 100%); + color: white; + padding: 80px 0; + border-bottom: 4px solid var(--electric-blue); +} + +.search-glass { + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 12px; + padding: 20px; +} + +.card { + border: none; + border-radius: 16px; + box-shadow: var(--card-shadow); + transition: transform 0.2s ease; +} + +.card:hover { + transform: translateY(-4px); +} + +.btn-primary { + background-color: var(--electric-blue); + border: none; + padding: 10px 24px; + border-radius: 8px; + font-weight: 500; +} + +.badge-support { + background-color: var(--emerald-accent); + color: white; +} + +.nav-link { + font-weight: 500; +} + +.stat-card { + border-left: 4px solid var(--electric-blue); +} + +.voter-profile-header { + background-color: white; + padding: 40px; + border-radius: 20px; + margin-top: -40px; + box-shadow: var(--card-shadow); +} \ No newline at end of file