diff --git a/src/comms/migrations/0011_fix_news_item_ordering.py b/src/comms/migrations/0011_fix_news_item_ordering.py new file mode 100644 index 0000000000..b439ada290 --- /dev/null +++ b/src/comms/migrations/0011_fix_news_item_ordering.py @@ -0,0 +1,16 @@ +# Generated by Django 4.2.20 on 2026-03-31 19:54 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("comms", "0010_historicalnewsitem_body_cy_and_more"), + ] + + operations = [ + migrations.AlterModelOptions( + name="newsitem", + options={"ordering": ("-pinned", "sequence", "-start_display", "title")}, + ), + ] diff --git a/src/comms/models.py b/src/comms/models.py index f01bbde03d..cc9701c82a 100755 --- a/src/comms/models.py +++ b/src/comms/models.py @@ -87,7 +87,7 @@ class NewsItem(models.Model): active_objects = ActiveNewsItemManager() class Meta: - ordering = ("pinned", "-posted", "title") + ordering = ("-pinned", "sequence", "-start_display", "title") @property def url(self): diff --git a/src/comms/tests.py b/src/comms/tests.py index 9ac6d46fed..570ab973c8 100644 --- a/src/comms/tests.py +++ b/src/comms/tests.py @@ -1,4 +1,6 @@ -from django.test import TestCase +import datetime + +from django.test import TestCase, override_settings from django.urls import reverse from django.contrib.contenttypes.models import ContentType from django.utils import timezone @@ -116,3 +118,106 @@ def test_manage_news_delete_image(self): core_models.File.objects.filter(pk=image_file.pk).exists(), msg="File was not deleted as expected.", ) + + +class NewsItemOrderingTest(TestCase): + @classmethod + def setUpTestData(cls): + cls.press = helpers.create_press() + cls.journal_one, cls.journal_two = helpers.create_journals() + cls.content_type = ContentType.objects.get_for_model(cls.journal_one) + + today = datetime.date.today() + yesterday = today - datetime.timedelta(days=1) + last_week = today - datetime.timedelta(days=7) + + cls.item_pinned = models.NewsItem.objects.create( + content_type=cls.content_type, + object_id=cls.journal_one.pk, + title="Pinned Item", + body="body", + start_display=yesterday, + pinned=True, + ) + cls.item_seq_1 = models.NewsItem.objects.create( + content_type=cls.content_type, + object_id=cls.journal_one.pk, + title="Sequence 1", + body="body", + start_display=today, + sequence=1, + ) + cls.item_seq_2 = models.NewsItem.objects.create( + content_type=cls.content_type, + object_id=cls.journal_one.pk, + title="Sequence 2", + body="body", + start_display=last_week, + sequence=2, + ) + # Two items sharing the same sequence and start_display — title breaks the tie. + cls.item_title_apple = models.NewsItem.objects.create( + content_type=cls.content_type, + object_id=cls.journal_one.pk, + title="Apple", + body="body", + start_display=last_week, + sequence=3, + ) + cls.item_title_zebra = models.NewsItem.objects.create( + content_type=cls.content_type, + object_id=cls.journal_one.pk, + title="Zebra", + body="body", + start_display=last_week, + sequence=3, + ) + cls.url = reverse("core_news_list") + + @override_settings(URL_CONFIG="domain") + def test_pinned_item_appears_first(self): + response = self.client.get(self.url, SERVER_NAME=self.journal_one.domain) + items = list(response.context["news_items"]) + self.assertEqual(items[0], self.item_pinned) + + @override_settings(URL_CONFIG="domain") + def test_sequence_respected_after_pinned(self): + response = self.client.get(self.url, SERVER_NAME=self.journal_one.domain) + items = list(response.context["news_items"]) + self.assertEqual(items[1], self.item_seq_1) + self.assertEqual(items[2], self.item_seq_2) + + @override_settings(URL_CONFIG="domain") + def test_ordering_uses_start_display_not_posted(self): + # item_seq_2 has an older start_display but was created after item_seq_1. + # Sequence drives order so seq_1 (sequence=1) precedes seq_2 (sequence=2), + # confirming posted timestamp is not used. + response = self.client.get(self.url, SERVER_NAME=self.journal_one.domain) + items = [i for i in response.context["news_items"] if not i.pinned] + self.assertEqual(items[0], self.item_seq_1) + self.assertEqual(items[1], self.item_seq_2) + + @override_settings(URL_CONFIG="domain") + def test_same_sequence_and_start_display_orders_by_title(self): + # item_title_apple and item_title_zebra share sequence=3 and start_display, + # so title alphabetical order is the tiebreaker. + response = self.client.get(self.url, SERVER_NAME=self.journal_one.domain) + items = [i for i in response.context["news_items"] if i.sequence == 3] + self.assertEqual(items[0], self.item_title_apple) + self.assertEqual(items[1], self.item_title_zebra) + + @override_settings(URL_CONFIG="domain") + def test_higher_sequence_with_newer_start_display_sorts_after_lower_sequence(self): + # item_seq_1 (sequence=1, start_display=today) must appear before + # item_seq_2 (sequence=2, start_display=last_week) even though a naive + # date-first sort would put last_week after today. + response = self.client.get(self.url, SERVER_NAME=self.journal_one.domain) + items = [ + i + for i in response.context["news_items"] + if not i.pinned and i.sequence in (1, 2) + ] + self.assertLess( + items.index(self.item_seq_1), + items.index(self.item_seq_2), + )