Autosave: 20260125-214643

This commit is contained in:
Flatlogic Bot 2026-01-25 21:46:44 +00:00
parent dc2bd62142
commit ac90cc59f4
21 changed files with 355 additions and 39 deletions

13
ERD.md
View File

@ -7,6 +7,7 @@ erDiagram
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
@ -20,6 +21,7 @@ erDiagram
Event ||--o{ EventParticipation : includes
EventType ||--o{ Event : categorizes
ParticipationStatus ||--o{ EventParticipation : defines_status
InteractionType ||--o{ Interaction : categorizes
DonationMethod ||--o{ Donation : categorizes
@ -76,6 +78,13 @@ erDiagram
boolean is_active
}
ParticipationStatus {
int id PK
int tenant_id FK
string name
boolean is_active
}
Voter {
int id PK
int tenant_id FK
@ -121,7 +130,7 @@ erDiagram
int id PK
int event_id FK
int voter_id FK
string participation_type
int participation_status_id FK
}
Donation {
@ -147,4 +156,4 @@ erDiagram
int election_type_id FK
string likelihood
}
```
```

View File

@ -12,7 +12,7 @@ from django.template.response import TemplateResponse
from .models import (
Tenant, TenantUserRole, InteractionType, DonationMethod, ElectionType, EventType, Voter,
VotingRecord, Event, EventParticipation, Donation, Interaction, VoterLikelihood, CampaignSettings,
Interest, Volunteer, VolunteerEvent
Interest, Volunteer, VolunteerEvent, ParticipationStatus
)
from .forms import (
VoterImportForm, EventImportForm, EventParticipationImportForm,
@ -47,7 +47,10 @@ VOTER_MAPPABLE_FIELDS = [
]
EVENT_MAPPABLE_FIELDS = [
('name', 'Name'),
('date', 'Date'),
('start_time', 'Start Time'),
('end_time', 'End Time'),
('event_type', 'Event Type (Name)'),
('description', 'Description'),
]
@ -57,7 +60,7 @@ EVENT_PARTICIPATION_MAPPABLE_FIELDS = [
('event_id', 'Event ID'),
('event_date', 'Event Date'),
('event_type', 'Event Type (Name)'),
('participation_type', 'Participation Type'),
('participation_status', 'Participation Type'),
]
DONATION_MAPPABLE_FIELDS = [
@ -148,6 +151,20 @@ class EventTypeAdmin(admin.ModelAdmin):
list_filter = ('tenant', 'is_active')
search_fields = ('name',)
@admin.register(ParticipationStatus)
class ParticipationStatusAdmin(admin.ModelAdmin):
list_display = ('name', 'tenant', 'is_active')
list_filter = ('tenant', 'is_active')
search_fields = ('name',)
change_list_template = 'admin/participationstatus_change_list.html'
def changelist_view(self, request, extra_context=None):
extra_context = extra_context or {}
from core.models import Tenant
extra_context['tenants'] = Tenant.objects.all()
return super().changelist_view(request, extra_context=extra_context)
@admin.register(Interest)
class InterestAdmin(admin.ModelAdmin):
list_display = ('name', 'tenant')
@ -182,6 +199,12 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin):
inlines = [VotingRecordInline, DonationInline, InteractionInline, VoterLikelihoodInline]
readonly_fields = ('address',)
change_list_template = "admin/voter_change_list.html"
def changelist_view(self, request, extra_context=None):
extra_context = extra_context or {}
from core.models import Tenant
extra_context["tenants"] = Tenant.objects.all()
return super().changelist_view(request, extra_context=extra_context)
def get_urls(self):
urls = super().get_urls()
@ -352,9 +375,16 @@ class VoterAdmin(BaseImportAdminMixin, admin.ModelAdmin):
@admin.register(Event)
class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin):
list_display = ('id', 'event_type', 'date', 'tenant')
list_display = ('id', 'name', 'event_type', 'date', 'start_time', 'end_time', 'tenant')
list_filter = ('tenant', 'date', 'event_type')
search_fields = ('name', 'description')
change_list_template = "admin/event_change_list.html"
def changelist_view(self, request, extra_context=None):
extra_context = extra_context or {}
from core.models import Tenant
extra_context["tenants"] = Tenant.objects.all()
return super().changelist_view(request, extra_context=extra_context)
def get_urls(self):
urls = super().get_urls()
@ -384,9 +414,13 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin):
total_count += 1
date = row.get(mapping.get('date'))
event_type_name = row.get(mapping.get('event_type'))
event_name = row.get(mapping.get('name'))
exists = False
if date and event_type_name:
exists = Event.objects.filter(tenant=tenant, date=date, event_type__name=event_type_name).exists()
q = Event.objects.filter(tenant=tenant, date=date, event_type__name=event_type_name)
if event_name:
q = q.filter(name=event_name)
exists = q.exists()
if exists:
update_count += 1
@ -398,7 +432,7 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin):
if len(preview_data) < 10:
preview_data.append({
'action': action,
'identifier': f"{date} - {event_type_name}",
'identifier': f"{event_name or 'No Name'} ({date} - {event_type_name})",
'details': row.get(mapping.get('description', '')) or ''
})
context = self.admin_site.each_context(request)
@ -439,6 +473,9 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin):
date = row.get(mapping.get('date')) if mapping.get('date') else None
event_type_name = row.get(mapping.get('event_type')) if mapping.get('event_type') else None
description = row.get(mapping.get('description')) if mapping.get('description') else None
name = row.get(mapping.get('name')) if mapping.get('name') else None
start_time = row.get(mapping.get('start_time')) if mapping.get('start_time') else None
end_time = row.get(mapping.get('end_time')) if mapping.get('end_time') else None
if not date or not event_type_name:
row["Import Error"] = "Missing date or event type"
@ -454,11 +491,18 @@ class EventAdmin(BaseImportAdminMixin, admin.ModelAdmin):
defaults = {}
if description and description.strip():
defaults['description'] = description
if name and name.strip():
defaults['name'] = name
if start_time and start_time.strip():
defaults['start_time'] = start_time
if end_time and end_time.strip():
defaults['end_time'] = end_time
Event.objects.update_or_create(
tenant=tenant,
date=date,
event_type=event_type,
name=name or '',
defaults=defaults
)
count += 1
@ -535,8 +579,8 @@ class VolunteerEventAdmin(admin.ModelAdmin):
@admin.register(EventParticipation)
class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
list_display = ('voter', 'event', 'participation_type')
list_filter = ('event__tenant', 'event', 'participation_type')
list_display = ('voter', 'event', 'participation_status')
list_filter = ('event__tenant', 'event', 'participation_status')
change_list_template = "admin/eventparticipation_change_list.html"
def get_urls(self):
@ -592,7 +636,7 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
preview_data.append({
'action': action,
'identifier': f"Voter: {voter_id}",
'details': f"Participation: {row.get(mapping.get('participation_type', '')) or ''}"
'details': f"Participation: {row.get(mapping.get('participation_status', '')) or ''}"
})
context = self.admin_site.each_context(request)
context.update({
@ -630,7 +674,7 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
for row in reader:
try:
voter_id = row.get(mapping.get('voter_id')) if mapping.get('voter_id') else None
participation_type_val = row.get(mapping.get('participation_type')) if mapping.get('participation_type') else None
participation_status_val = row.get(mapping.get('participation_status')) if mapping.get('participation_status') else None
if not voter_id:
row["Import Error"] = "Missing voter ID"
@ -675,11 +719,11 @@ class EventParticipationAdmin(BaseImportAdminMixin, admin.ModelAdmin):
continue
defaults = {}
if participation_type_val and participation_type_val.strip():
if participation_type_val in dict(EventParticipation.PARTICIPATION_TYPE_CHOICES):
defaults['participation_type'] = participation_type_val
if participation_status_val and participation_status_val.strip():
if participation_status_val in dict(EventParticipation.PARTICIPATION_TYPE_CHOICES):
defaults['participation_status'] = participation_status_val
else:
defaults['participation_type'] = 'invited'
defaults['participation_status'] = 'invited'
EventParticipation.objects.update_or_create(
event=event,
@ -1332,4 +1376,4 @@ class VoterLikelihoodAdmin(BaseImportAdminMixin, admin.ModelAdmin):
@admin.register(CampaignSettings)
class CampaignSettingsAdmin(admin.ModelAdmin):
list_display = ('tenant', 'donation_goal')
list_filter = ('tenant',)
list_filter = ('tenant',)

View File

@ -1,5 +1,5 @@
from django import forms
from .models import Voter, Interaction, Donation, VoterLikelihood, InteractionType, DonationMethod, ElectionType, Event, EventParticipation, EventType, Tenant
from .models import Voter, Interaction, Donation, VoterLikelihood, InteractionType, DonationMethod, ElectionType, Event, EventParticipation, EventType, Tenant, ParticipationStatus
class VoterForm(forms.ModelForm):
class Meta:
@ -81,23 +81,26 @@ class VoterLikelihoodForm(forms.ModelForm):
class EventParticipationForm(forms.ModelForm):
class Meta:
model = EventParticipation
fields = ['event', 'participation_type']
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_type'].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 = ['date', 'event_type', 'description']
fields = ['name', 'date', 'start_time', 'end_time', 'event_type', 'description']
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}),
}
@ -161,4 +164,4 @@ class VoterLikelihoodImportForm(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'})

View File

@ -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',
),
]

View File

@ -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'),
),
]

View File

@ -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',
),
]

View File

@ -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),
),
]

View File

@ -6,9 +6,21 @@ import urllib.request
import logging
from decimal import Decimal
from django.conf import settings
import re
logger = logging.getLogger(__name__)
def format_phone_number(phone):
"""Formats a phone number to (xxx) xxx-xxxx if it has 10 digits or 11 starting with 1."""
if not phone:
return phone
digits = re.sub(r'\D', '', str(phone))
if len(digits) == 10:
return f"({digits[:3]}) {digits[3:6]}-{digits[6:]}"
elif len(digits) == 11 and digits.startswith('1'):
return f"({digits[1:4]}) {digits[4:7]}-{digits[7:]}"
return phone
class Tenant(models.Model):
name = models.CharField(max_length=100)
created_at = models.DateTimeField(auto_now_add=True)
@ -76,6 +88,18 @@ class EventType(models.Model):
def __str__(self):
return self.name
class ParticipationStatus(models.Model):
tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name='participation_statuses')
name = models.CharField(max_length=100)
is_active = models.BooleanField(default=True)
class Meta:
unique_together = ('tenant', 'name')
verbose_name_plural = 'Participation Statuses'
def __str__(self):
return self.name
class Interest(models.Model):
tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name='interests')
name = models.CharField(max_length=100)
@ -189,6 +213,9 @@ class Voter(models.Model):
return False, err
def save(self, *args, **kwargs):
# Auto-format phone number
self.phone = format_phone_number(self.phone)
# Ensure longitude is truncated to 12 characters before saving
if self.longitude:
self.longitude = Decimal(str(self.longitude)[:12])
@ -243,11 +270,16 @@ class VotingRecord(models.Model):
class Event(models.Model):
tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name='events')
name = models.CharField(max_length=255, blank=True)
date = models.DateField()
start_time = models.TimeField(null=True, blank=True)
end_time = models.TimeField(null=True, blank=True)
event_type = models.ForeignKey(EventType, on_delete=models.PROTECT, null=True)
description = models.TextField(blank=True)
def __str__(self):
if self.name:
return f"{self.name} ({self.date})"
return f"{self.event_type} on {self.date}"
class Volunteer(models.Model):
@ -259,6 +291,11 @@ class Volunteer(models.Model):
interests = models.ManyToManyField(Interest, blank=True, related_name='volunteers')
assigned_events = models.ManyToManyField(Event, through='VolunteerEvent', related_name='assigned_volunteers')
def save(self, *args, **kwargs):
# Auto-format phone number
self.phone = format_phone_number(self.phone)
super().save(*args, **kwargs)
def __str__(self):
return self.name
@ -271,18 +308,12 @@ class VolunteerEvent(models.Model):
return f"{self.volunteer} at {self.event} as {self.role}"
class EventParticipation(models.Model):
PARTICIPATION_TYPE_CHOICES = [
("invited", "Invited"),
("invited_not_attended", "Invited but didn't attend"),
("attended", "Attended"),
]
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name='participations')
voter = models.ForeignKey(Voter, on_delete=models.CASCADE, related_name='event_participations')
participation_type = models.CharField(max_length=50, choices=PARTICIPATION_TYPE_CHOICES, default='invited')
participation_status = models.ForeignKey(ParticipationStatus, on_delete=models.PROTECT, null=True)
def __str__(self):
return f"{self.voter} at {self.event} ({self.get_participation_type_display()})"
return f"{self.voter} at {self.event} ({self.participation_status})"
class Donation(models.Model):
voter = models.ForeignKey(Voter, on_delete=models.CASCADE, related_name='donations')
@ -328,4 +359,4 @@ class CampaignSettings(models.Model):
verbose_name_plural = 'Campaign Settings'
def __str__(self):
return f'Settings for {self.tenant.name}'
return f'Settings for {self.tenant.name}'

View File

@ -1,7 +1,38 @@
{% extends "admin/change_list.html" %}
{% load i18n admin_urls static admin_list %}
{% block object-tools-items %}
<li>
<a href="import-events/" class="addlink">Import Events</a>
</li>
{{ block.super }}
{% endblock %}
{% block search %}
{{ block.super }}
<div class="tenant-filter-container" style="margin: 10px 0; padding: 15px; background: var(--darkened-bg, #f8f9fa); border: 1px solid var(--border-color, #dee2e6); border-radius: 4px; display: flex; align-items: center; color: var(--body-fg, #333);">
<label for="tenant-filter-select" style="font-weight: 600; margin-right: 15px; color: var(--body-fg, #333);">Filter by Tenant:</label>
<select id="tenant-filter-select" onchange="filterTenant(this.value)" style="padding: 6px 12px; border-radius: 4px; border: 1px solid var(--border-color, #ced4da); background-color: var(--body-bg, #fff); color: var(--body-fg, #333); min-width: 200px;">
<option value="" style="background-color: var(--body-bg); color: var(--body-fg);">-- All Tenants --</option>
{% for tenant in tenants %}
<option value="{{ tenant.id }}" {% if request.GET.tenant__id__exact == tenant.id|stringformat:"s" %}selected{% endif %} style="background-color: var(--body-bg); color: var(--body-fg);">
{{ tenant.name }}
</option>
{% endfor %}
</select>
</div>
<script>
function filterTenant(tenantId) {
const url = new URL(window.location.href);
if (tenantId) {
url.searchParams.set('tenant__id__exact', tenantId);
} else {
url.searchParams.delete('tenant__id__exact');
}
// Reset to page 1 if filtering
url.searchParams.delete('p');
window.location.href = url.pathname + url.search;
}
</script>
{% endblock %}

View File

@ -0,0 +1,31 @@
{% extends "admin/change_list.html" %}
{% load i18n admin_urls static admin_list %}
{% block search %}
{{ block.super }}
<div class="tenant-filter-container" style="margin: 10px 0; padding: 15px; background: var(--darkened-bg, #f8f9fa); border: 1px solid var(--border-color, #dee2e6); border-radius: 4px; display: flex; align-items: center; color: var(--body-fg, #333);">
<label for="tenant-filter-select" style="font-weight: 600; margin-right: 15px; color: var(--body-fg, #333);">Filter by Tenant:</label>
<select id="tenant-filter-select" onchange="filterTenant(this.value)" style="padding: 6px 12px; border-radius: 4px; border: 1px solid var(--border-color, #ced4da); background-color: var(--body-bg, #fff); color: var(--body-fg, #333); min-width: 200px;">
<option value="" style="background-color: var(--body-bg); color: var(--body-fg);">-- All Tenants --</option>
{% for tenant in tenants %}
<option value="{{ tenant.id }}" {% if request.GET.tenant__id__exact == tenant.id|stringformat:"s" %}selected{% endif %} style="background-color: var(--body-bg); color: var(--body-fg);">
{{ tenant.name }}
</option>
{% endfor %}
</select>
</div>
<script>
function filterTenant(tenantId) {
const url = new URL(window.location.href);
if (tenantId) {
url.searchParams.set('tenant__id__exact', tenantId);
} else {
url.searchParams.delete('tenant__id__exact');
}
// Reset to page 1 if filtering
url.searchParams.delete('p');
window.location.href = url.pathname + url.search;
}
</script>
{% endblock %}

View File

@ -1,7 +1,38 @@
{% extends "admin/change_list.html" %}
{% load i18n admin_urls static admin_list %}
{% block object-tools-items %}
<li>
<a href="import-voters/" class="addlink">Import Voters</a>
</li>
{{ block.super }}
{% endblock %}
{% block search %}
{{ block.super }}
<div class="tenant-filter-container" style="margin: 10px 0; padding: 15px; background: var(--darkened-bg, #f8f9fa); border: 1px solid var(--border-color, #dee2e6); border-radius: 4px; display: flex; align-items: center; color: var(--body-fg, #333);">
<label for="tenant-filter-select" style="font-weight: 600; margin-right: 15px; color: var(--body-fg, #333);">Filter by Tenant:</label>
<select id="tenant-filter-select" onchange="filterTenant(this.value)" style="padding: 6px 12px; border-radius: 4px; border: 1px solid var(--border-color, #ced4da); background-color: var(--body-bg, #fff); color: var(--body-fg, #333); min-width: 200px;">
<option value="" style="background-color: var(--body-bg); color: var(--body-fg);">-- All Tenants --</option>
{% for tenant in tenants %}
<option value="{{ tenant.id }}" {% if request.GET.tenant__id__exact == tenant.id|stringformat:"s" %}selected{% endif %} style="background-color: var(--body-bg); color: var(--body-fg);">
{{ tenant.name }}
</option>
{% endfor %}
</select>
</div>
<script>
function filterTenant(tenantId) {
const url = new URL(window.location.href);
if (tenantId) {
url.searchParams.set('tenant__id__exact', tenantId);
} else {
url.searchParams.delete('tenant__id__exact');
}
// Reset to page 1 if filtering
url.searchParams.delete('p');
window.location.href = url.pathname + url.search;
}
</script>
{% endblock %}

View File

@ -74,5 +74,35 @@
<!-- Bootstrap 5 JS Bundle -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
function formatPhoneNumber(value) {
if (!value) return value;
const phoneNumber = value.replace(/[^\d]/g, '');
const phoneNumberLength = phoneNumber.length;
if (phoneNumberLength < 4) return phoneNumber;
if (phoneNumberLength < 7) {
return `(${phoneNumber.slice(0, 3)}) ${phoneNumber.slice(3)}`;
}
return `(${phoneNumber.slice(0, 3)}) ${phoneNumber.slice(3, 6)}-${phoneNumber.slice(6, 10)}`;
}
function phoneNumberFormatter() {
const inputField = this;
const formattedFieldValue = formatPhoneNumber(inputField.value);
inputField.value = formattedFieldValue;
}
const phoneInputs = document.querySelectorAll('input[name="phone"], input[type="tel"]');
phoneInputs.forEach(input => {
input.addEventListener('input', phoneNumberFormatter);
// Also format on load if it has a value
if (input.value) {
input.value = formatPhoneNumber(input.value);
}
});
});
</script>
</body>
</html>

View File

@ -327,7 +327,7 @@
<tr>
<th class="ps-4">Date</th>
<th>Event Type</th>
<th>Type</th>
<th>Status</th>
<th>Description</th>
<th class="pe-4 text-end">Actions</th>
</tr>
@ -338,12 +338,12 @@
<td class="ps-4 text-nowrap">{{ participation.event.date|date:"M d, Y" }}</td>
<td><span class="badge bg-light text-dark border">{{ participation.event.event_type.name }}</span></td>
<td>
{% if participation.participation_type == 'attended' %}
{% if participation.participation_status.name|lower == 'attended' %}
<span class="badge bg-success-subtle text-success border border-success-subtle">Attended</span>
{% elif participation.participation_type == 'invited_not_attended' %}
{% elif participation.participation_status.name|lower == "invited but didn't attend" or participation.participation_status.name|lower == "invited but didn't attend" %}
<span class="badge bg-danger-subtle text-danger border border-danger-subtle">Did Not Attend</span>
{% else %}
<span class="badge bg-info-subtle text-info border border-info-subtle">Invited</span>
<span class="badge bg-info-subtle text-info border border-info-subtle">{{ participation.participation_status.name }}</span>
{% endif %}
</td>
<td class="small text-muted">{{ participation.event.description|truncatechars:60 }}</td>
@ -799,7 +799,7 @@
</div>
<div class="mb-0">
<label class="form-label fw-medium">Participation Status</label>
{{ event_participation_form.participation_type }}
{{ event_participation_form.participation_status }}
</div>
</div>
<div class="modal-footer border-0 p-4 pt-0">
@ -833,9 +833,9 @@
</div>
<div class="mb-0">
<label class="form-label fw-medium">Participation Status</label>
<select name="participation_type" class="form-select">
{% for val, label in event_participation_form.fields.participation_type.choices %}
<option value="{{ val }}" {% if val == participation.participation_type %}selected{% endif %}>{{ label }}</option>
<select name="participation_status" class="form-select">
{% for status in event_participation_form.fields.participation_status.queryset %}
<option value="{{ status.id }}" {% if status.id == participation.participation_status.id %}selected{% endif %}>{{ status.name }}</option>
{% endfor %}
</select>
</div>
@ -998,4 +998,4 @@
}
});
</script>
{% endblock %}
{% endblock %}