feat: group people by role priority
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
|
||||
ROLE_PRIORITY = {"lead": 0, "leader": 0, "organizer": 1, "collaborator": 2}
|
||||
ROLE_TITLES = {0: "Lead / Leader", 1: "Organizers", 2: "Collaborators"}
|
||||
|
||||
|
||||
def person_sort_key(person):
|
||||
role = (person.role or "").strip().casefold()
|
||||
return (ROLE_PRIORITY.get(role, 3), role if role not in ROLE_PRIORITY else "", person.order, person.pk or 0)
|
||||
|
||||
|
||||
def group_people(people):
|
||||
groups = OrderedDict()
|
||||
for person in sorted(people, key=person_sort_key):
|
||||
role = (person.role or "").strip()
|
||||
priority = ROLE_PRIORITY.get(role.casefold(), 3)
|
||||
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",
|
||||
"people": [],
|
||||
}
|
||||
groups[key]["people"].append(person)
|
||||
return list(groups.values())
|
||||
@@ -0,0 +1,10 @@
|
||||
from django import template
|
||||
|
||||
from apps.core.people import group_people
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter
|
||||
def people_rows(people):
|
||||
return group_people(people)
|
||||
@@ -0,0 +1,32 @@
|
||||
# Generated by Django 5.2.16 on 2026-10-28 19:50
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pages', '0027_aboutsectionitem_role_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='blogpostpartner',
|
||||
options={'ordering': [models.Case(models.When(role__iexact='lead', then=models.Value(0)), models.When(role__iexact='leader', then=models.Value(0)), models.When(role__iexact='organizer', then=models.Value(1)), models.When(role__iexact='collaborator', then=models.Value(2)), default=models.Value(3), output_field=models.IntegerField()), 'order', 'pk'], 'verbose_name': 'Article person', 'verbose_name_plural': 'Article people'},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='aboutsectionitem',
|
||||
name='role',
|
||||
field=models.CharField(blank=True, help_text='For People sections: Lead, Leader, Organizer, Collaborator, or any custom role name.', max_length=100, verbose_name='Role / group'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='blogpostpartner',
|
||||
name='order',
|
||||
field=models.PositiveIntegerField(default=0, help_text='Smaller numbers appear first within each role. Leads/Leaders, Organizers, Collaborators, then custom roles.', verbose_name='Display order'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='blogpostpartner',
|
||||
name='role',
|
||||
field=models.CharField(default='Collaborator', help_text='Type Lead, Leader, Organizer, Collaborator, or any other role name.', max_length=100, verbose_name='Role / group'),
|
||||
),
|
||||
]
|
||||
+12
-14
@@ -4,6 +4,7 @@ from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.text import slugify
|
||||
|
||||
from apps.core.people import person_sort_key
|
||||
from apps.core.utils import CONTENT_FORMAT_CHOICES, FORMAT_MARKDOWN, FORMAT_PLAIN, render_content
|
||||
from apps.core.video import VideoBlockMixin
|
||||
from apps.pages.contact_uploads import (
|
||||
@@ -157,12 +158,7 @@ class AboutSection(VideoBlockMixin, models.Model):
|
||||
|
||||
@property
|
||||
def ordered_people_items(self):
|
||||
def sort_key(item):
|
||||
role = item.role.strip().casefold()
|
||||
priority = 0 if role == "organizer" else 1 if role == "collaborator" else 2
|
||||
return priority, item.order, item.pk or 0
|
||||
|
||||
return sorted(self.items.all(), key=sort_key)
|
||||
return sorted(self.items.all(), key=person_sort_key)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
@@ -183,7 +179,7 @@ class AboutSectionItem(models.Model):
|
||||
"Role / group",
|
||||
max_length=100,
|
||||
blank=True,
|
||||
help_text="For People sections: Organizer, Collaborator, or any custom role name.",
|
||||
help_text="For People sections: Lead, Leader, Organizer, Collaborator, or any custom role name.",
|
||||
)
|
||||
content = models.TextField(blank=True, help_text="Description text or link label for History links.")
|
||||
read_more_content = models.BooleanField(default=False, help_text="Collapse this text with a Read more button.")
|
||||
@@ -212,7 +208,7 @@ class AboutSectionItem(models.Model):
|
||||
|
||||
@property
|
||||
def role_label(self):
|
||||
if self.role.casefold() in {"organizer", "collaborator"}:
|
||||
if self.role.casefold() in {"lead", "leader", "organizer", "collaborator"}:
|
||||
return self.role.title()
|
||||
return self.role
|
||||
|
||||
@@ -415,7 +411,7 @@ class BlogPostPartner(models.Model):
|
||||
"Role / group",
|
||||
max_length=100,
|
||||
default="Collaborator",
|
||||
help_text="Type Organizer, Collaborator, or any other role name you want to display.",
|
||||
help_text="Type Lead, Leader, Organizer, Collaborator, or any other role name.",
|
||||
)
|
||||
name = models.CharField(max_length=200)
|
||||
description = models.TextField(blank=True)
|
||||
@@ -426,15 +422,17 @@ class BlogPostPartner(models.Model):
|
||||
order = models.PositiveIntegerField(
|
||||
"Display order",
|
||||
default=0,
|
||||
help_text="Smaller numbers appear first. Organizers appear before collaborators, then custom roles.",
|
||||
help_text="Smaller numbers appear first within each role. Leads/Leaders, Organizers, Collaborators, then custom roles.",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = [
|
||||
models.Case(
|
||||
models.When(role__iexact="organizer", then=models.Value(0)),
|
||||
models.When(role__iexact="collaborator", then=models.Value(1)),
|
||||
default=models.Value(2),
|
||||
models.When(role__iexact="lead", then=models.Value(0)),
|
||||
models.When(role__iexact="leader", then=models.Value(0)),
|
||||
models.When(role__iexact="organizer", then=models.Value(1)),
|
||||
models.When(role__iexact="collaborator", then=models.Value(2)),
|
||||
default=models.Value(3),
|
||||
output_field=models.IntegerField(),
|
||||
),
|
||||
"order",
|
||||
@@ -448,7 +446,7 @@ class BlogPostPartner(models.Model):
|
||||
|
||||
@property
|
||||
def role_label(self):
|
||||
if self.role.casefold() in {self.ORGANIZER, self.COLLABORATOR}:
|
||||
if self.role.casefold() in {"lead", "leader", self.ORGANIZER, self.COLLABORATOR}:
|
||||
return self.role.title()
|
||||
return self.role
|
||||
|
||||
|
||||
@@ -44,11 +44,14 @@ class AboutViewTest(TestCase):
|
||||
AboutSectionItem.objects.create(section=section, title="Advisor", role="Scientific advisor", content="Advisor profile", order=1)
|
||||
AboutSectionItem.objects.create(section=section, title="Collaborator", role="Collaborator", content="Collaboration profile", read_more_content=True, order=3)
|
||||
AboutSectionItem.objects.create(section=section, title="Organizer", role="Organizer", content="Organizer profile", order=9)
|
||||
AboutSectionItem.objects.create(section=section, title="Leader", role="Leader", content="Leader profile", order=20)
|
||||
html = self.client.get(reverse("pages:about")).content.decode()
|
||||
self.assertIn("People behind the work", html)
|
||||
self.assertIn("Scientific advisor", html)
|
||||
self.assertLess(html.index(">Organizer</h3>"), html.index(">Collaborator</h3>"))
|
||||
self.assertLess(html.index(">Collaborator</h3>"), html.index(">Advisor</h3>"))
|
||||
self.assertLess(html.index(">Leader</h4>"), html.index(">Organizer</h4>"))
|
||||
self.assertLess(html.index(">Organizer</h4>"), html.index(">Collaborator</h4>"))
|
||||
self.assertLess(html.index(">Collaborator</h4>"), html.index(">Advisor</h4>"))
|
||||
self.assertIn("Lead / Leader", html)
|
||||
self.assertIn('data-readmore="200"', html)
|
||||
self.assertEqual(html.count('data-readmore="88"'), 1)
|
||||
|
||||
@@ -113,10 +116,15 @@ class BlogViewTest(TestCase):
|
||||
BlogPostPartner.objects.create(post=self.post, role=BlogPostPartner.COLLABORATOR, name="Second collaborator", order=2)
|
||||
BlogPostPartner.objects.create(post=self.post, role=BlogPostPartner.COLLABORATOR, name="First collaborator", order=1)
|
||||
BlogPostPartner.objects.create(post=self.post, role=BlogPostPartner.ORGANIZER, name="Organizer", order=9)
|
||||
BlogPostPartner.objects.create(post=self.post, role="Lead", name="Lead", order=5)
|
||||
BlogPostPartner.objects.create(post=self.post, role="Leader", name="Leader", order=1)
|
||||
html = self.client.get(self.post.get_absolute_url()).content.decode()
|
||||
self.assertLess(html.index('class="blog-article rich-content'), html.index('class="blog-partners"'))
|
||||
self.assertLess(html.index('>Organizer</h3>'), html.index('>First collaborator</h3>'))
|
||||
self.assertLess(html.index('>First collaborator</h3>'), html.index('>Second collaborator</h3>'))
|
||||
self.assertLess(html.index('>Leader</h4>'), html.index('>Lead</h4>'))
|
||||
self.assertLess(html.index('>Lead</h4>'), html.index('>Organizer</h4>'))
|
||||
self.assertLess(html.index('>Organizer</h4>'), html.index('>First collaborator</h4>'))
|
||||
self.assertLess(html.index('>First collaborator</h4>'), html.index('>Second collaborator</h4>'))
|
||||
self.assertLess(html.index('Lead / Leader'), html.index('>Organizers</h3>'))
|
||||
|
||||
def test_partner_headings_are_shared_and_description_read_more_is_independent(self):
|
||||
branding = SiteBranding.load()
|
||||
@@ -152,8 +160,8 @@ class BlogViewTest(TestCase):
|
||||
BlogPostPartner.objects.create(post=self.post, role=BlogPostPartner.ORGANIZER, name="Lead", order=9)
|
||||
html = self.client.get(self.post.get_absolute_url()).content.decode()
|
||||
self.assertIn("Scientific advisor", html)
|
||||
self.assertLess(html.index(">Lead</h3>"), html.index(">Partner</h3>"))
|
||||
self.assertLess(html.index(">Partner</h3>"), html.index(">Advisor</h3>"))
|
||||
self.assertLess(html.index(">Lead</h4>"), html.index(">Partner</h4>"))
|
||||
self.assertLess(html.index(">Partner</h4>"), html.index(">Advisor</h4>"))
|
||||
|
||||
|
||||
class ContactViewTest(TestCase):
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# Generated by Django 5.2.16 on 2026-10-28 19:50
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0025_articleperson_mainproductperson_subproductperson'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='articleperson',
|
||||
options={'ordering': [models.Case(models.When(role__iexact='lead', then=models.Value(0)), models.When(role__iexact='leader', then=models.Value(0)), models.When(role__iexact='organizer', then=models.Value(1)), models.When(role__iexact='collaborator', then=models.Value(2)), default=models.Value(3), output_field=models.IntegerField()), 'order', 'pk'], 'verbose_name': 'Project article person', 'verbose_name_plural': 'Project article people'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='mainproductperson',
|
||||
options={'ordering': [models.Case(models.When(role__iexact='lead', then=models.Value(0)), models.When(role__iexact='leader', then=models.Value(0)), models.When(role__iexact='organizer', then=models.Value(1)), models.When(role__iexact='collaborator', then=models.Value(2)), default=models.Value(3), output_field=models.IntegerField()), 'order', 'pk'], 'verbose_name': 'Main project person', 'verbose_name_plural': 'Main project people'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='subproductperson',
|
||||
options={'ordering': [models.Case(models.When(role__iexact='lead', then=models.Value(0)), models.When(role__iexact='leader', then=models.Value(0)), models.When(role__iexact='organizer', then=models.Value(1)), models.When(role__iexact='collaborator', then=models.Value(2)), default=models.Value(3), output_field=models.IntegerField()), 'order', 'pk'], 'verbose_name': 'Sub-project person', 'verbose_name_plural': 'Sub-project people'},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='articleperson',
|
||||
name='order',
|
||||
field=models.PositiveIntegerField(default=0, help_text='Smaller numbers appear first within each role. Leads/Leaders, Organizers, Collaborators, then custom roles.', verbose_name='Display order'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='articleperson',
|
||||
name='role',
|
||||
field=models.CharField(default='Collaborator', help_text='Type Lead, Leader, Organizer, Collaborator, or any other role name.', max_length=100, verbose_name='Role / group'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='mainproductperson',
|
||||
name='order',
|
||||
field=models.PositiveIntegerField(default=0, help_text='Smaller numbers appear first within each role. Leads/Leaders, Organizers, Collaborators, then custom roles.', verbose_name='Display order'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='mainproductperson',
|
||||
name='role',
|
||||
field=models.CharField(default='Collaborator', help_text='Type Lead, Leader, Organizer, Collaborator, or any other role name.', max_length=100, verbose_name='Role / group'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='subproductperson',
|
||||
name='order',
|
||||
field=models.PositiveIntegerField(default=0, help_text='Smaller numbers appear first within each role. Leads/Leaders, Organizers, Collaborators, then custom roles.', verbose_name='Display order'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='subproductperson',
|
||||
name='role',
|
||||
field=models.CharField(default='Collaborator', help_text='Type Lead, Leader, Organizer, Collaborator, or any other role name.', max_length=100, verbose_name='Role / group'),
|
||||
),
|
||||
]
|
||||
@@ -227,7 +227,7 @@ class Article(models.Model):
|
||||
class ProjectPerson(models.Model):
|
||||
role = models.CharField(
|
||||
"Role / group", max_length=100, default="Collaborator",
|
||||
help_text="Type Organizer, Collaborator, or any other role name.",
|
||||
help_text="Type Lead, Leader, Organizer, Collaborator, or any other role name.",
|
||||
)
|
||||
name = models.CharField(max_length=200)
|
||||
description = models.TextField(blank=True)
|
||||
@@ -239,23 +239,25 @@ class ProjectPerson(models.Model):
|
||||
url = models.URLField(blank=True)
|
||||
order = models.PositiveIntegerField(
|
||||
"Display order", default=0,
|
||||
help_text="Smaller numbers appear first. Organizers appear before collaborators, then custom roles.",
|
||||
help_text="Smaller numbers appear first within each role. Leads/Leaders, Organizers, Collaborators, then custom roles.",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
ordering = [
|
||||
models.Case(
|
||||
models.When(role__iexact="organizer", then=models.Value(0)),
|
||||
models.When(role__iexact="collaborator", then=models.Value(1)),
|
||||
default=models.Value(2), output_field=models.IntegerField(),
|
||||
models.When(role__iexact="lead", then=models.Value(0)),
|
||||
models.When(role__iexact="leader", then=models.Value(0)),
|
||||
models.When(role__iexact="organizer", then=models.Value(1)),
|
||||
models.When(role__iexact="collaborator", then=models.Value(2)),
|
||||
default=models.Value(3), output_field=models.IntegerField(),
|
||||
),
|
||||
"order", "pk",
|
||||
]
|
||||
|
||||
@property
|
||||
def role_label(self):
|
||||
return self.role.title() if self.role.casefold() in {"organizer", "collaborator"} else self.role
|
||||
return self.role.title() if self.role.casefold() in {"lead", "leader", "organizer", "collaborator"} else self.role
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.role_label})"
|
||||
|
||||
@@ -62,11 +62,17 @@ class MainProductDetailViewTest(ProductViewsSetup):
|
||||
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)
|
||||
MainProductPerson.objects.create(main_product=self.main_product, name="Main leader", role="Leader", order=2)
|
||||
MainProductPerson.objects.create(main_product=self.main_product, name="Main lead", role="Lead", order=1)
|
||||
main_article = Article.objects.create(main_product=self.main_product, title="Main article", description="Text")
|
||||
ArticlePerson.objects.create(article=main_article, name="Article advisor", role="Scientific advisor", order=0)
|
||||
response = self.client.get(self.main_product.get_absolute_url())
|
||||
html = response.content.decode()
|
||||
self.assertLess(html.index("Main lead"), html.index("Main leader"))
|
||||
self.assertLess(html.index("Main leader"), html.index("Main organizer"))
|
||||
self.assertLess(html.index("Main organizer"), html.index("Main collaborator"))
|
||||
self.assertLess(html.index("Lead / Leader"), html.index(">Organizers</h3>"))
|
||||
self.assertLess(html.index(">Organizers</h3>"), html.index(">Collaborators</h3>"))
|
||||
self.assertContains(response, "Article advisor")
|
||||
self.assertNotContains(response, "Sub collaborator")
|
||||
self.assertContains(response, f'id="article-{main_article.pk}-people"')
|
||||
|
||||
Reference in New Issue
Block a user