diff --git a/apps/core/admin.py b/apps/core/admin.py index c038098..4a47bc8 100644 --- a/apps/core/admin.py +++ b/apps/core/admin.py @@ -50,6 +50,13 @@ class SiteBrandingAdmin(admin.ModelAdmin): "description": "These headings are shared by main projects and sub-projects that have people cards.", }, ), + ( + "People role headings", + { + "fields": ("lead_group_label", "organizer_group_label", "collaborator_group_label"), + "description": "Shared across About, blog articles, project articles, main projects, and sub-projects. Empty headings are hidden; people cards remain visible.", + }, + ), ( "Brand assets", { diff --git a/apps/core/migrations/0008_sitebranding_collaborator_group_label_and_more.py b/apps/core/migrations/0008_sitebranding_collaborator_group_label_and_more.py new file mode 100644 index 0000000..1565d44 --- /dev/null +++ b/apps/core/migrations/0008_sitebranding_collaborator_group_label_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 5.2.16 on 2026-10-28 20:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0007_sitebranding_project_people_eyebrow_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='sitebranding', + name='collaborator_group_label', + field=models.CharField(blank=True, default='Collaborators', help_text='Heading for Collaborators. Leave empty to hide the heading while keeping their cards.', max_length=100), + ), + migrations.AddField( + model_name='sitebranding', + name='lead_group_label', + field=models.CharField(blank=True, default='Lead / Leader', help_text='Heading for Lead and Leader people. Leave empty to hide the heading while keeping their cards.', max_length=100), + ), + migrations.AddField( + model_name='sitebranding', + name='organizer_group_label', + field=models.CharField(blank=True, default='Organizers', help_text='Heading for Organizers. Leave empty to hide the heading while keeping their cards.', max_length=100), + ), + ] diff --git a/apps/core/models.py b/apps/core/models.py index 5cf42c6..ca30dea 100644 --- a/apps/core/models.py +++ b/apps/core/models.py @@ -49,6 +49,18 @@ class SiteBranding(models.Model): max_length=200, default="Organizers & collaborators", help_text="Heading above the people cards on main projects and sub-projects.", ) + lead_group_label = models.CharField( + max_length=100, blank=True, default="Lead / Leader", + help_text="Heading for Lead and Leader people. Leave empty to hide the heading while keeping their cards.", + ) + organizer_group_label = models.CharField( + max_length=100, blank=True, default="Organizers", + help_text="Heading for Organizers. Leave empty to hide the heading while keeping their cards.", + ) + collaborator_group_label = models.CharField( + max_length=100, blank=True, default="Collaborators", + help_text="Heading for Collaborators. Leave empty to hide the heading while keeping their cards.", + ) icon = models.ImageField( upload_to="branding/", diff --git a/apps/core/people.py b/apps/core/people.py index 08c7afd..b008356 100644 --- a/apps/core/people.py +++ b/apps/core/people.py @@ -2,7 +2,7 @@ from collections import OrderedDict ROLE_PRIORITY = {"lead": 0, "leader": 0, "organizer": 1, "collaborator": 2} -ROLE_TITLES = {0: "Lead / Leader", 1: "Organizers", 2: "Collaborators"} +ROLE_LABEL_FIELDS = {0: "lead_group_label", 1: "organizer_group_label", 2: "collaborator_group_label"} def person_sort_key(person): @@ -10,7 +10,7 @@ def person_sort_key(person): return (ROLE_PRIORITY.get(role, 3), role if role not in ROLE_PRIORITY else "", person.order, person.pk or 0) -def group_people(people): +def group_people(people, branding): groups = OrderedDict() for person in sorted(people, key=person_sort_key): role = (person.role or "").strip() @@ -18,7 +18,7 @@ def group_people(people): key = priority if priority < 3 else role.casefold() if key not in groups: groups[key] = { - "title": ROLE_TITLES[priority] if priority < 3 else role or "Other contributors", + "title": getattr(branding, ROLE_LABEL_FIELDS[priority], "") if priority < 3 else role, "people": [], } groups[key]["people"].append(person) diff --git a/apps/core/templatetags/people.py b/apps/core/templatetags/people.py index 9a74863..10d7d59 100644 --- a/apps/core/templatetags/people.py +++ b/apps/core/templatetags/people.py @@ -6,5 +6,5 @@ register = template.Library() @register.filter -def people_rows(people): - return group_people(people) +def people_rows(people, branding): + return group_people(people, branding) diff --git a/apps/pages/tests/test_views.py b/apps/pages/tests/test_views.py index 6353638..98094b8 100644 --- a/apps/pages/tests/test_views.py +++ b/apps/pages/tests/test_views.py @@ -60,6 +60,16 @@ class AboutViewTest(TestCase): response = self.client.get(reverse("pages:about")) self.assertNotContains(response, "Nobody yet") + def test_about_role_heading_uses_shared_admin_setting(self): + section = AboutSection.objects.create(section_type=AboutSection.TYPE_PEOPLE, title="Our people") + AboutSectionItem.objects.create(section=section, title="Team lead", role="Leader") + branding = SiteBranding.load() + branding.lead_group_label = "Leadership" + branding.save(update_fields=["lead_group_label"]) + response = self.client.get(reverse("pages:about")) + self.assertContains(response, "Leadership") + self.assertNotContains(response, "Lead / Leader") + class BlogViewTest(TestCase): def setUp(self): diff --git a/apps/products/tests/test_views.py b/apps/products/tests/test_views.py index dafa756..3a828ca 100644 --- a/apps/products/tests/test_views.py +++ b/apps/products/tests/test_views.py @@ -1,6 +1,7 @@ from django.test import TestCase from django.urls import reverse +from apps.core.models import SiteBranding from apps.products.models import ( Article, ArticlePerson, ArticleSection, MainProduct, MainProductPerson, SubProduct, SubProductPerson, @@ -59,6 +60,22 @@ class ProductOverviewViewTest(ProductViewsSetup): class MainProductDetailViewTest(ProductViewsSetup): + def test_role_headings_follow_site_settings_and_can_be_hidden(self): + MainProductPerson.objects.create(main_product=self.main_product, name="Project lead", role="Lead") + MainProductPerson.objects.create(main_product=self.main_product, name="Project organizer", role="Organizer") + branding = SiteBranding.load() + branding.lead_group_label = "Leadership team" + branding.organizer_group_label = "" + branding.save(update_fields=["lead_group_label", "organizer_group_label"]) + + response = self.client.get(self.main_product.get_absolute_url()) + self.assertContains(response, "Leadership team") + self.assertNotContains(response, "Lead / Leader") + self.assertNotContains(response, ">Organizers") + self.assertContains(response, "Project lead") + self.assertContains(response, "Project organizer") + self.assertNotContains(response, ">Collaborators") + def test_project_and_article_people_are_independent_and_ordered(self): MainProductPerson.objects.create(main_product=self.main_product, name="Main collaborator", role="Collaborator", order=1) MainProductPerson.objects.create(main_product=self.main_product, name="Main organizer", role="Organizer", order=9) diff --git a/templates/partials/_people_rows.html b/templates/partials/_people_rows.html index 14bb372..225c2f7 100644 --- a/templates/partials/_people_rows.html +++ b/templates/partials/_people_rows.html @@ -1,7 +1,7 @@ {% load people %} -{% for row in people|people_rows %} +{% for row in people|people_rows:site_branding %}
- {% if embedded %}
{{ row.title }}
{% else %}

{{ row.title }}

{% endif %} + {% if row.title %}{% if embedded %}
{{ row.title }}
{% else %}

{{ row.title }}

{% endif %}{% endif %}
{% for person in row.people %} {% if about %}