diff --git a/config/__pycache__/__init__.cpython-311.pyc b/config/__pycache__/__init__.cpython-311.pyc index 423a636..b2a6eee 100644 Binary files a/config/__pycache__/__init__.cpython-311.pyc and b/config/__pycache__/__init__.cpython-311.pyc differ diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index 96bce55..c8bcb91 100644 Binary files a/config/__pycache__/settings.cpython-311.pyc and b/config/__pycache__/settings.cpython-311.pyc differ diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc index 0b85e94..c5a37af 100644 Binary files a/config/__pycache__/urls.cpython-311.pyc and b/config/__pycache__/urls.cpython-311.pyc differ diff --git a/config/__pycache__/wsgi.cpython-311.pyc b/config/__pycache__/wsgi.cpython-311.pyc index 9c49e09..945c6e2 100644 Binary files a/config/__pycache__/wsgi.cpython-311.pyc and b/config/__pycache__/wsgi.cpython-311.pyc differ diff --git a/core/__pycache__/__init__.cpython-311.pyc b/core/__pycache__/__init__.cpython-311.pyc index 74b1112..c3e6f00 100644 Binary files a/core/__pycache__/__init__.cpython-311.pyc and b/core/__pycache__/__init__.cpython-311.pyc differ diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index a5ed392..9eeed8d 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/apps.cpython-311.pyc b/core/__pycache__/apps.cpython-311.pyc index 6f131d4..c93069c 100644 Binary files a/core/__pycache__/apps.cpython-311.pyc and b/core/__pycache__/apps.cpython-311.pyc differ diff --git a/core/__pycache__/context_processors.cpython-311.pyc b/core/__pycache__/context_processors.cpython-311.pyc index 75bf223..3df58b8 100644 Binary files a/core/__pycache__/context_processors.cpython-311.pyc and b/core/__pycache__/context_processors.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..fb593c8 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..9c700fe 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..d1c2c24 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..2b1d115 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..5395bfb 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,3 +1,69 @@ from django.contrib import admin +from .models import ( + Tenant, TenantUserRole, InteractionType, DonationMethod, ElectionType, Voter, + VotingRecord, Event, EventParticipation, Donation, Interaction, VoterLikelihood +) -# Register your models here. +class TenantUserRoleInline(admin.TabularInline): + model = TenantUserRole + extra = 1 + +@admin.register(Tenant) +class TenantAdmin(admin.ModelAdmin): + list_display = ('name', 'slug', 'created_at') + search_fields = ('name',) + inlines = [TenantUserRoleInline] + +@admin.register(TenantUserRole) +class TenantUserRoleAdmin(admin.ModelAdmin): + list_display = ('user', 'tenant', 'role') + list_filter = ('tenant', 'role') + search_fields = ('user__username', 'tenant__name') + +@admin.register(InteractionType) +class InteractionTypeAdmin(admin.ModelAdmin): + list_display = ('name', 'tenant') + list_filter = ('tenant',) + +@admin.register(DonationMethod) +class DonationMethodAdmin(admin.ModelAdmin): + list_display = ('name', 'tenant') + list_filter = ('tenant',) + +@admin.register(ElectionType) +class ElectionTypeAdmin(admin.ModelAdmin): + list_display = ('name', 'tenant') + list_filter = ('tenant',) + +class VotingRecordInline(admin.TabularInline): + model = VotingRecord + extra = 1 + +class DonationInline(admin.TabularInline): + model = Donation + extra = 1 + +class InteractionInline(admin.TabularInline): + model = Interaction + extra = 1 + +class VoterLikelihoodInline(admin.TabularInline): + model = VoterLikelihood + extra = 1 + +@admin.register(Voter) +class VoterAdmin(admin.ModelAdmin): + list_display = ('first_name', 'last_name', 'voter_id', 'tenant', 'district', 'candidate_support') + list_filter = ('tenant', 'candidate_support', 'yard_sign', 'district') + search_fields = ('first_name', 'last_name', 'voter_id') + inlines = [VotingRecordInline, DonationInline, InteractionInline, VoterLikelihoodInline] + +@admin.register(Event) +class EventAdmin(admin.ModelAdmin): + list_display = ('event_type', 'date', 'tenant') + list_filter = ('tenant', 'date') + +@admin.register(EventParticipation) +class EventParticipationAdmin(admin.ModelAdmin): + list_display = ('voter', 'event') + list_filter = ('event__tenant', 'event') \ No newline at end of file diff --git a/core/forms.py b/core/forms.py new file mode 100644 index 0000000..3e108a6 --- /dev/null +++ b/core/forms.py @@ -0,0 +1,82 @@ +from django import forms +from .models import Voter, Interaction, Donation, VoterLikelihood, InteractionType, DonationMethod, ElectionType, Event, EventParticipation + +class VoterForm(forms.ModelForm): + class Meta: + model = Voter + fields = [ + 'first_name', 'last_name', 'street_address', 'city', 'state', 'zip_code', + 'latitude', 'longitude', 'phone', 'email', + 'voter_id', 'district', 'precinct', 'registration_date', + 'candidate_support', 'yard_sign' + ] + widgets = { + 'registration_date': forms.DateInput(attrs={'type': 'date'}), + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for field in self.fields.values(): + 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'}) + +class InteractionForm(forms.ModelForm): + class Meta: + model = Interaction + fields = ['type', 'date', 'description', 'notes'] + widgets = { + 'date': forms.DateInput(attrs={'type': 'date'}), + '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) + for field in self.fields.values(): + field.widget.attrs.update({'class': 'form-control'}) + self.fields['type'].widget.attrs.update({'class': 'form-select'}) + +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) + 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) + 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'] + + 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'}) \ 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..371e2a0 --- /dev/null +++ b/core/migrations/0001_initial.py @@ -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')}, + }, + ), + ] diff --git a/core/migrations/0002_donation_event_eventparticipation_interaction_and_more.py b/core/migrations/0002_donation_event_eventparticipation_interaction_and_more.py new file mode 100644 index 0000000..e395f53 --- /dev/null +++ b/core/migrations/0002_donation_event_eventparticipation_interaction_and_more.py @@ -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')}, + }, + ), + ] diff --git a/core/migrations/0003_tenantuserrole.py b/core/migrations/0003_tenantuserrole.py new file mode 100644 index 0000000..574946f --- /dev/null +++ b/core/migrations/0003_tenantuserrole.py @@ -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')}, + }, + ), + ] 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..df898f7 Binary files /dev/null and b/core/migrations/__pycache__/0001_initial.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0002_donation_event_eventparticipation_interaction_and_more.cpython-311.pyc b/core/migrations/__pycache__/0002_donation_event_eventparticipation_interaction_and_more.cpython-311.pyc new file mode 100644 index 0000000..6c15850 Binary files /dev/null and b/core/migrations/__pycache__/0002_donation_event_eventparticipation_interaction_and_more.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0003_tenantuserrole.cpython-311.pyc b/core/migrations/__pycache__/0003_tenantuserrole.cpython-311.pyc new file mode 100644 index 0000000..7827bf9 Binary files /dev/null and b/core/migrations/__pycache__/0003_tenantuserrole.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/__init__.cpython-311.pyc b/core/migrations/__pycache__/__init__.cpython-311.pyc index 9c833c8..154497b 100644 Binary files a/core/migrations/__pycache__/__init__.cpython-311.pyc and b/core/migrations/__pycache__/__init__.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 71a8362..7de862b 100644 --- a/core/models.py +++ b/core/models.py @@ -1,3 +1,161 @@ from django.db import models +from django.utils.text import slugify +from django.contrib.auth.models import User -# Create your models here. +class Tenant(models.Model): + name = models.CharField(max_length=255) + slug = models.SlugField(unique=True, blank=True) + description = models.TextField(blank=True) + created_at = models.DateTimeField(auto_now_add=True) + + def save(self, *args, **kwargs): + if not self.slug: + self.slug = slugify(self.name) + super().save(*args, **kwargs) + + def __str__(self): + return self.name + +class TenantUserRole(models.Model): + ROLE_CHOICES = [ + ('system_admin', 'System Administrator'), + ('campaign_admin', 'Campaign Administrator'), + ('campaign_staff', 'Campaign Staff'), + ] + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='tenant_roles') + tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name='user_roles') + role = models.CharField(max_length=20, choices=ROLE_CHOICES) + + class Meta: + unique_together = ('user', 'tenant', 'role') + + def __str__(self): + return f"{self.user.username} - {self.tenant.name} ({self.role})" + +class InteractionType(models.Model): + tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name='interaction_types') + name = models.CharField(max_length=100) + + class Meta: + unique_together = ('tenant', 'name') + + def __str__(self): + return f"{self.name} ({self.tenant.name})" + +class DonationMethod(models.Model): + tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name='donation_methods') + name = models.CharField(max_length=100) + + class Meta: + unique_together = ('tenant', 'name') + + def __str__(self): + return f"{self.name} ({self.tenant.name})" + +class ElectionType(models.Model): + tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name='election_types') + name = models.CharField(max_length=100) + + class Meta: + unique_together = ('tenant', 'name') + + def __str__(self): + return f"{self.name} ({self.tenant.name})" + +class Voter(models.Model): + 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'), + ] + + tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name='voters') + voter_id = models.CharField(max_length=50, blank=True) + first_name = models.CharField(max_length=100) + last_name = models.CharField(max_length=100) + + # Separated address fields + street_address = models.CharField(max_length=255, blank=True) + city = models.CharField(max_length=100, blank=True) + state = models.CharField(max_length=50, blank=True) + zip_code = models.CharField(max_length=20, blank=True) + latitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True) + longitude = models.DecimalField(max_digits=9, decimal_places=6, null=True, blank=True) + + phone = models.CharField(max_length=20, blank=True) + email = models.EmailField(blank=True) + district = models.CharField(max_length=100, blank=True) + precinct = models.CharField(max_length=100, blank=True) + registration_date = models.DateField(null=True, blank=True) + candidate_support = models.CharField(max_length=20, choices=SUPPORT_CHOICES, default='unknown') + yard_sign = models.CharField(max_length=20, choices=YARD_SIGN_CHOICES, default='none') + + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"{self.first_name} {self.last_name}" + +class VotingRecord(models.Model): + voter = models.ForeignKey(Voter, on_delete=models.CASCADE, related_name='voting_records') + election_date = models.DateField() + election_description = models.CharField(max_length=255) + primary_party = models.CharField(max_length=100, blank=True) + + def __str__(self): + return f"{self.voter} - {self.election_description}" + +class Event(models.Model): + tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name='events') + date = models.DateField() + event_type = models.CharField(max_length=100) + description = models.TextField(blank=True) + + def __str__(self): + return f"{self.event_type} on {self.date}" + +class EventParticipation(models.Model): + event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name='participations') + voter = models.ForeignKey(Voter, on_delete=models.CASCADE, related_name='event_participations') + + def __str__(self): + return f"{self.voter} at {self.event}" + +class Donation(models.Model): + voter = models.ForeignKey(Voter, on_delete=models.CASCADE, related_name='donations') + date = models.DateField() + method = models.ForeignKey(DonationMethod, on_delete=models.SET_NULL, null=True) + amount = models.DecimalField(max_digits=10, decimal_places=2) + + def __str__(self): + return f"{self.voter} - {self.amount} on {self.date}" + +class Interaction(models.Model): + voter = models.ForeignKey(Voter, on_delete=models.CASCADE, related_name='interactions') + type = models.ForeignKey(InteractionType, on_delete=models.SET_NULL, null=True) + date = models.DateField() + description = models.CharField(max_length=255) + notes = models.TextField(blank=True) + + def __str__(self): + return f"{self.voter} - {self.type} on {self.date}" + +class VoterLikelihood(models.Model): + LIKELIHOOD_CHOICES = [ + ('not_likely', 'Not Likely'), + ('somewhat_likely', 'Somewhat Likely'), + ('very_likely', 'Very Likely'), + ] + voter = models.ForeignKey(Voter, on_delete=models.CASCADE, related_name='likelihoods') + election_type = models.ForeignKey(ElectionType, on_delete=models.CASCADE) + likelihood = models.CharField(max_length=20, choices=LIKELIHOOD_CHOICES) + + class Meta: + unique_together = ('voter', 'election_type') + + def __str__(self): + return f"{self.voter} - {self.election_type}: {self.get_likelihood_display()}" \ No newline at end of file diff --git a/core/templates/base.html b/core/templates/base.html index 1e7e5fb..d40c799 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -1,25 +1,67 @@ -
- -