diff --git a/apps/nominations/admin.py b/apps/nominations/admin.py index 4b19aff51..b9d157dff 100644 --- a/apps/nominations/admin.py +++ b/apps/nominations/admin.py @@ -3,13 +3,23 @@ from django.contrib import admin from django.db.models.functions import Lower -from apps.nominations.models import Election, Nomination, Nominee +from apps.nominations.models import Election, ElectionKind, Nomination, Nominee + + +@admin.register(ElectionKind) +class ElectionKindAdmin(admin.ModelAdmin): + """Admin interface for managing election kinds and their accent colors.""" + + list_display = ("name", "accent_color", "slug") + readonly_fields = ("slug",) @admin.register(Election) class ElectionAdmin(admin.ModelAdmin): """Admin interface for managing elections.""" + list_display = ("name", "kind", "date", "slug") + list_filter = ("kind",) readonly_fields = ("slug",) diff --git a/apps/nominations/migrations/0003_election_kind.py b/apps/nominations/migrations/0003_election_kind.py new file mode 100644 index 000000000..8e32e6ffe --- /dev/null +++ b/apps/nominations/migrations/0003_election_kind.py @@ -0,0 +1,46 @@ +# Generated by Django 5.2.13 on 2026-06-24 15:33 + +import colorfield.fields +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("nominations", "0002_auto_20190514_1435"), + ] + + operations = [ + migrations.CreateModel( + name="ElectionKind", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("name", models.CharField(max_length=100, unique=True)), + ("slug", models.SlugField(blank=True, max_length=120, null=True, unique=True)), + ( + "accent_color", + colorfield.fields.ColorField( + default="#0073b7", + help_text="Accent color used to theme this kind's pages.", + image_field=None, + max_length=25, + samples=None, + ), + ), + ], + options={ + "ordering": ["name"], + }, + ), + migrations.AddField( + model_name="election", + name="kind", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="elections", + to="nominations.electionkind", + ), + ), + ] diff --git a/apps/nominations/models.py b/apps/nominations/models.py index c440999a4..888a2853d 100644 --- a/apps/nominations/models.py +++ b/apps/nominations/models.py @@ -2,6 +2,7 @@ import datetime +from colorfield.fields import ColorField from django.db import models from django.db.models.signals import post_save from django.dispatch import receiver @@ -13,12 +14,48 @@ from apps.users.models import User from fastly.utils import purge_url +DEFAULT_ACCENT_COLOR = "#0073b7" + + +class ElectionKind(models.Model): + """An admin-managed category of election (Board, Packaging Council, ...). + + Each kind carries an accent color used to visually distinguish its + election pages. New kinds can be added in the Django admin without + code changes. + """ + + name = models.CharField(max_length=100, unique=True) + slug = models.SlugField(max_length=120, unique=True, blank=True, null=True) + accent_color = ColorField(default=DEFAULT_ACCENT_COLOR, help_text="Accent color used to theme this kind's pages.") + + class Meta: + """Meta configuration for ElectionKind.""" + + ordering = ["name"] + + def __str__(self): + """Return the kind name.""" + return self.name + + def save(self, *args, **kwargs): + """Generate slug from name before saving.""" + self.slug = slugify(self.name) + super().save(*args, **kwargs) + class Election(models.Model): """A PSF board election with nomination open/close dates.""" name = models.CharField(max_length=100) date = models.DateField() + kind = models.ForeignKey( + ElectionKind, + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name="elections", + ) nominations_open_at = models.DateTimeField(blank=True, null=True) nominations_close_at = models.DateTimeField(blank=True, null=True) description = MarkupField(escape_html=False, markup_type="markdown", blank=False, null=True) @@ -39,6 +76,11 @@ def save(self, *args, **kwargs): self.slug = slugify(self.name) super().save(*args, **kwargs) + @property + def accent_color(self): + """Return the CSS accent color for this election's kind.""" + return self.kind.accent_color if self.kind else DEFAULT_ACCENT_COLOR + @property def nominations_open(self): """Return True if the current time is within the nomination window.""" diff --git a/apps/nominations/templates/nominations/_theme.html b/apps/nominations/templates/nominations/_theme.html new file mode 100644 index 000000000..74c0f41fc --- /dev/null +++ b/apps/nominations/templates/nominations/_theme.html @@ -0,0 +1,20 @@ +{% comment %} +Per-election theming. Expects `election` in context (pass via +`{% include "nominations/_theme.html" with election=object.election %}` +where only the nomination object is available). Colors key headings +with the election's accent so Board vs. Packaging Council pages are +visually distinct. See Election.accent_color / the ElectionKind model. +{% endcomment %} +{% if election %} + +{% endif %} diff --git a/apps/nominations/templates/nominations/election_detail.html b/apps/nominations/templates/nominations/election_detail.html index cf08b865d..a06f7afa7 100644 --- a/apps/nominations/templates/nominations/election_detail.html +++ b/apps/nominations/templates/nominations/election_detail.html @@ -4,6 +4,8 @@ {% block page_title %}Elections | {{ SITE_INFO.site_name }}{% endblock %} +{% block head %}{% include "nominations/_theme.html" %}{% endblock %} + {% block content_attributes %}with-left-sidebar{% endblock %} diff --git a/apps/nominations/templates/nominations/nomination_accept_form.html b/apps/nominations/templates/nominations/nomination_accept_form.html index 37a07da60..4e0de693a 100644 --- a/apps/nominations/templates/nominations/nomination_accept_form.html +++ b/apps/nominations/templates/nominations/nomination_accept_form.html @@ -6,6 +6,7 @@ {% endblock %} {% block body_attributes %}class="nominations nominations_view"{% endblock %} +{% block head %}{% include "nominations/_theme.html" %}{% endblock %} {% block left_sidebar %}{% endblock %} {% block content_attributes %}{% endblock %} diff --git a/apps/nominations/templates/nominations/nomination_detail.html b/apps/nominations/templates/nominations/nomination_detail.html index 36f6fe773..8c57f6fd3 100644 --- a/apps/nominations/templates/nominations/nomination_detail.html +++ b/apps/nominations/templates/nominations/nomination_detail.html @@ -6,6 +6,7 @@ {% endblock %} {% block body_attributes %}class="nominations nominations_view"{% endblock %} +{% block head %}{% include "nominations/_theme.html" with election=nomination.election %}{% endblock %} {% block left_sidebar %}{% endblock %} {% block content_attributes %}{% endblock %} diff --git a/apps/nominations/templates/nominations/nomination_form.html b/apps/nominations/templates/nominations/nomination_form.html index baed48410..f258aa3ad 100644 --- a/apps/nominations/templates/nominations/nomination_form.html +++ b/apps/nominations/templates/nominations/nomination_form.html @@ -10,6 +10,7 @@ {% endblock %} {% block body_attributes %}class="nominations nominations_form"{% endblock %} +{% block head %}{% include "nominations/_theme.html" %}{% endblock %} {% block left_sidebar %}{% endblock %} {% block content_attributes %}{% endblock %} diff --git a/apps/nominations/templates/nominations/nominee_detail.html b/apps/nominations/templates/nominations/nominee_detail.html index a0dcd7926..911e9b161 100644 --- a/apps/nominations/templates/nominations/nominee_detail.html +++ b/apps/nominations/templates/nominations/nominee_detail.html @@ -7,6 +7,7 @@ {% endblock %} {% block body_attributes %}class="nominations nominations_view"{% endblock %} +{% block head %}{% include "nominations/_theme.html" %}{% endblock %} {% block left_sidebar %}{% endblock %} {% block content_attributes %}{% endblock %} diff --git a/apps/nominations/templates/nominations/nominee_list.html b/apps/nominations/templates/nominations/nominee_list.html index 370de5d37..def5fdca5 100644 --- a/apps/nominations/templates/nominations/nominee_list.html +++ b/apps/nominations/templates/nominations/nominee_list.html @@ -11,6 +11,7 @@ {% block content_attributes %}{% endblock %} {% block head %} +{% include "nominations/_theme.html" %}