From a455eab3306c9f8a6ba8f9314998f02b00c8ada4 Mon Sep 17 00:00:00 2001 From: Andy Vandenberghe Date: Sun, 4 Oct 2026 22:32:46 +0200 Subject: [PATCH] Add an ACP option to show announcements first New setting "Show announcements first" (rt_announcements_first, off by default). When on, fill_template() moves the announcements and global announcements on the current page to the top of that page, keeping their time order; the rest of the page, including stickies, keeps its order. Only rows already on the page move, so old announcements never come back into the list, and paging is unchanged. - release_3_0_12 migration adds the config value - ACP checkbox next to "Minimum topic type level" - Language strings in all 14 packs Closes #201 Co-Authored-By: Claude Opus 5.5 --- acp/recenttopics_module.php | 4 + adm/style/acp_recenttopics.html | 5 + core/recenttopics.php | 33 ++++ language/ar/info_acp_recenttopics.php | 2 + language/cs/info_acp_recenttopics.php | 2 + language/de/info_acp_recenttopics.php | 2 + language/de_x_sie/info_acp_recenttopics.php | 2 + language/en/info_acp_recenttopics.php | 2 + language/es/info_acp_recenttopics.php | 2 + language/es_x_tu/info_acp_recenttopics.php | 2 + language/fr/info_acp_recenttopics.php | 2 + language/nl/info_acp_recenttopics.php | 2 + language/pt/info_acp_recenttopics.php | 2 + language/ru/info_acp_recenttopics.php | 2 + language/sk/info_acp_recenttopics.php | 2 + language/sv/info_acp_recenttopics.php | 2 + language/uk/info_acp_recenttopics.php | 2 + migrations/v300/release_3_0_12.php | 38 ++++ tests/core/announcements_first_test.php | 182 ++++++++++++++++++++ tests/functional/recenttopics_test.php | 52 ++++++ tests/tests.md | 20 ++- 21 files changed, 361 insertions(+), 1 deletion(-) create mode 100644 migrations/v300/release_3_0_12.php create mode 100644 tests/core/announcements_first_test.php diff --git a/acp/recenttopics_module.php b/acp/recenttopics_module.php index 11d9655..9838519 100644 --- a/acp/recenttopics_module.php +++ b/acp/recenttopics_module.php @@ -92,6 +92,9 @@ public function main($id, $mode) $rt_min_topic_level = $request->variable('rt_min_topic_level', 0); $config->set('rt_min_topic_level', $rt_min_topic_level); + // Show announcements first + $config->set('rt_announcements_first', $request->variable('rt_announcements_first', 0) ? 1 : 0); + // variable should be '' as it is a string ("1, 2, 3928") here, not an integer. $rt_anti_topics = $request->variable('rt_anti_topics', ''); $ants = explode(',', $rt_anti_topics); @@ -297,6 +300,7 @@ public function main($id, $mode) 'RT_ADS_CODE' => $config_text->get('rt_ads_code'), 'RT_SHOW_LIKES' => (int) $config['rt_show_likes'], 'RT_SIDE_SHOW_DATE' => (int) $config['rt_side_show_date'], + 'RT_ANNOUNCEMENTS_FIRST' => (int) $config['rt_announcements_first'], 'S_POSTLOVE' => $phpbb_container->has('avathar.postlove.topic_likes'), // Bundled copy, so the ACP page makes no request to Patreon's CDN (issue #200) 'U_PATREON_BUTTON' => $phpbb_root_path . 'ext/avathar/recenttopics/adm/style/images/become_a_patron_button.png', diff --git a/adm/style/acp_recenttopics.html b/adm/style/acp_recenttopics.html index 99d54d3..f2451ed 100644 --- a/adm/style/acp_recenttopics.html +++ b/adm/style/acp_recenttopics.html @@ -46,6 +46,11 @@

{{ lang('RECENT_TOPICS') }}

+
+

{{ lang('RT_ANNOUNCEMENTS_FIRST_EXP') }}
+
+
+

{{ lang('RT_ANTI_TOPICS_EXP') }}
diff --git a/core/recenttopics.php b/core/recenttopics.php index 0d94036..1e5261d 100644 --- a/core/recenttopics.php +++ b/core/recenttopics.php @@ -468,6 +468,33 @@ private function show_likes(): bool return $this->topic_likes_service !== null && !empty($this->config['rt_show_likes']); } + /** + * Put a page's announcements and global announcements ahead of its other topics. + * + * Only the rows already on this page move, so old announcements never come back into the + * list. Both groups keep their existing (time) order; stickies stay with the normal topics. + * + * @param array $rowset Topic rows for the current page, as returned by get_topics_sql() + * @return array The same rows, announcements first + */ + private function announcements_first(array $rowset): array + { + $announcements = $others = []; + foreach ($rowset as $row) + { + if (in_array((int) $row['topic_type'], [POST_ANNOUNCE, POST_GLOBAL], true)) + { + $announcements[] = $row; + } + else + { + $others[] = $row; + } + } + + return array_merge($announcements, $others); + } + /** * Narrow the board's forums down to the ones this list may draw topics from. * @@ -786,6 +813,12 @@ private function fill_template($tpl_loopname, $topic_tracking_info, int $topics_ $rowset = $this->get_topics_sql(); $topic_icons = array(); + // Move this page's announcements and global announcements to its top (issue #201) + if (!empty($this->config['rt_announcements_first'])) + { + $rowset = $this->announcements_first($rowset); + } + // Get postlove like counts if installed and switched on in the ACP $topic_likes = []; if ($this->show_likes() && !empty($this->topic_list)) diff --git a/language/ar/info_acp_recenttopics.php b/language/ar/info_acp_recenttopics.php index 35f005a..f95e2d9 100644 --- a/language/ar/info_acp_recenttopics.php +++ b/language/ar/info_acp_recenttopics.php @@ -43,6 +43,8 @@ 'RT_PAGE_NUMBERMAX_EXP' => 'تحديد الحد الأقصى لعدد الصفحات التي تريد عرضها . هذا الخيار لا يعمل في حالة تحديد الخيار السابق ( عرض جميع الصفحات ).', 'RT_MIN_TOPIC_LEVEL' => 'نوع المواضيع ', 'RT_MIN_TOPIC_LEVEL_EXP' => 'تحديد نوع المواضيع التي تريد عرضها. سيتم العرض من الأقل إلى الأكثر.', + 'RT_ANNOUNCEMENTS_FIRST' => 'عرض الإعلانات أولاً', + 'RT_ANNOUNCEMENTS_FIRST_EXP' => 'ينقل الإعلانات والإعلانات العامة في كل صفحة من القائمة إلى أعلى تلك الصفحة. لا تتم إضافة الإعلانات الأقدم غير الموجودة في الصفحة.', 'RT_ANTI_TOPICS' => 'المواضيع المُستبعدة ', 'RT_ANTI_TOPICS_EXP' => 'اكتب علامة الفاصلة ", " بين أرقام المواضيع التي تريد عدم ظهورها في "أحدث المواضيع" ( مثال : 7,9 )', 'RT_PARENTS' => 'إظهار المنتدى الأب ', diff --git a/language/cs/info_acp_recenttopics.php b/language/cs/info_acp_recenttopics.php index 4b0464b..0cf8869 100644 --- a/language/cs/info_acp_recenttopics.php +++ b/language/cs/info_acp_recenttopics.php @@ -40,6 +40,8 @@ 'RT_PAGE_NUMBERMAX_EXP' => 'Nastavte maximum stránek pro zobrazení ve stránkování nedávných témat (není-li přepsáno jiným nastavením).', 'RT_MIN_TOPIC_LEVEL' => 'Minimální úroveň tématu', 'RT_MIN_TOPIC_LEVEL_EXP' => 'Určuje minimální úroveň typu tématu pro zobrazení. Zobrazena budou pouze témata zvolené úrovně a vyšší.', + 'RT_ANNOUNCEMENTS_FIRST' => 'Zobrazit oznámení jako první', + 'RT_ANNOUNCEMENTS_FIRST_EXP' => 'Přesune oznámení a globální oznámení na každé stránce seznamu na začátek dané stránky. Starší oznámení, která na stránce nejsou, se nepřidávají.', 'RT_ANTI_TOPICS' => 'Vyloučená témata', 'RT_ANTI_TOPICS_EXP' => 'Identifikátory témat k vyloučení, oddělené čárkou „,“ (příklad: 7,9)
', 'RT_PARENTS' => 'Zobrazit nadřazená fóra', diff --git a/language/de/info_acp_recenttopics.php b/language/de/info_acp_recenttopics.php index cbee5a2..c8406ac 100644 --- a/language/de/info_acp_recenttopics.php +++ b/language/de/info_acp_recenttopics.php @@ -59,6 +59,8 @@ 'RT_PAGE_NUMBERMAX_EXP' => 'Lege die maximale Anzahl der Seiten fest.', 'RT_MIN_TOPIC_LEVEL' => 'Minimaler Thementyp', 'RT_MIN_TOPIC_LEVEL_EXP' => 'Definiert das Minimum des anzuzeigenden Thementyps. Wenn du einen Thementyp angibst, werden nur Themen dieses oder eines höheren Typs angezeigt.', + 'RT_ANNOUNCEMENTS_FIRST' => 'Bekanntmachungen zuerst anzeigen', + 'RT_ANNOUNCEMENTS_FIRST_EXP' => 'Verschiebt die Bekanntmachungen und globalen Bekanntmachungen auf jeder Seite der Liste an den Anfang dieser Seite. Ältere Bekanntmachungen, die nicht auf der Seite stehen, werden nicht hinzugefügt.', 'RT_ANTI_TOPICS' => 'Ausgeschlossene Themen', 'RT_ANTI_TOPICS_EXP' => 'Gib die Themen-IDs ein, kommagetrennt (z. B. 7,9), andernfalls 0, um alle Themen anzuzeigen (wie in der URL viewtopic.php?t=12345).', 'RT_PARENTS' => 'Übergeordnete Foren anzeigen', diff --git a/language/de_x_sie/info_acp_recenttopics.php b/language/de_x_sie/info_acp_recenttopics.php index 2ff5769..f042f9a 100644 --- a/language/de_x_sie/info_acp_recenttopics.php +++ b/language/de_x_sie/info_acp_recenttopics.php @@ -59,6 +59,8 @@ 'RT_PAGE_NUMBERMAX_EXP' => 'Legt die maximale Anzahl der Seiten fest.', 'RT_MIN_TOPIC_LEVEL' => 'Minimaler Thementyp', 'RT_MIN_TOPIC_LEVEL_EXP' => 'Definiert das Minimum eines Thementyps, der angezeigt wird. Wenn Sie einen Thementyp angeben, werden nur Themen dieses oder eines höheren Typs angezeigt.', + 'RT_ANNOUNCEMENTS_FIRST' => 'Bekanntmachungen zuerst anzeigen', + 'RT_ANNOUNCEMENTS_FIRST_EXP' => 'Verschiebt die Bekanntmachungen und globalen Bekanntmachungen auf jeder Seite der Liste an den Anfang dieser Seite. Ältere Bekanntmachungen, die nicht auf der Seite stehen, werden nicht hinzugefügt.', 'RT_ANTI_TOPICS' => 'Ausgeschlossene Themen', 'RT_ANTI_TOPICS_EXP' => 'Geben Sie die Themen-IDs ein, kommagetrennt (z. B. 7,9), andernfalls 0, um alle Themen anzuzeigen (wie in der URL viewtopic.php?t=12345).', 'RT_PARENTS' => 'Übergeordnete Foren anzeigen', diff --git a/language/en/info_acp_recenttopics.php b/language/en/info_acp_recenttopics.php index f2e0cd0..20f5aa4 100644 --- a/language/en/info_acp_recenttopics.php +++ b/language/en/info_acp_recenttopics.php @@ -43,6 +43,8 @@ 'RT_PAGE_NUMBERMAX_EXP' => 'Set the page maximum to display in the recent topics pagination unless overridden.', 'RT_MIN_TOPIC_LEVEL' => 'Minimum topic type level', 'RT_MIN_TOPIC_LEVEL_EXP' => 'Determines the minimum level of the topic-type to display. It will only display topics of the set level and higher.', + 'RT_ANNOUNCEMENTS_FIRST' => 'Show announcements first', + 'RT_ANNOUNCEMENTS_FIRST_EXP' => 'Moves the announcements and global announcements on each page of the list to the top of that page. Older announcements that are not on the page are not added.', 'RT_ANTI_TOPICS' => 'Excluded topic IDs', 'RT_ANTI_TOPICS_EXP' => 'The IDs of topics to exclude, separated by “,” (Example: 7,9)
The value 0 disables this behaviour.', 'RT_PARENTS' => 'Display parent forums', diff --git a/language/es/info_acp_recenttopics.php b/language/es/info_acp_recenttopics.php index 5271d32..7852ac2 100644 --- a/language/es/info_acp_recenttopics.php +++ b/language/es/info_acp_recenttopics.php @@ -43,6 +43,8 @@ 'RT_PAGE_NUMBERMAX_EXP' => 'Definir el número máximo de páginas', 'RT_MIN_TOPIC_LEVEL' => 'Nivel de tema mínimo', 'RT_MIN_TOPIC_LEVEL_EXP' => 'Determina el nivel de tema mínimo para poder ser mostrado. Solo mostrará temas del nivel especificado y superior.', + 'RT_ANNOUNCEMENTS_FIRST' => 'Mostrar primero los anuncios', + 'RT_ANNOUNCEMENTS_FIRST_EXP' => 'Coloca los anuncios y anuncios globales de cada página de la lista al principio de esa página. Los anuncios más antiguos que no estén en la página no se añaden.', 'RT_ANTI_TOPICS' => 'Temas excluidos', 'RT_ANTI_TOPICS_EXP' => 'Las IDs de los temas a excluir, separados por "," (Por ejemplo: 7,9)
Si no quiere excluir un tema, simplemente introduzca 0.', 'RT_PARENTS' => 'Mostrar foros padre', diff --git a/language/es_x_tu/info_acp_recenttopics.php b/language/es_x_tu/info_acp_recenttopics.php index 8b8ec56..e92a76a 100644 --- a/language/es_x_tu/info_acp_recenttopics.php +++ b/language/es_x_tu/info_acp_recenttopics.php @@ -43,6 +43,8 @@ 'RT_PAGE_NUMBERMAX_EXP' => 'Definir el número máximo de páginas', 'RT_MIN_TOPIC_LEVEL' => 'Nivel de tema mínimo', 'RT_MIN_TOPIC_LEVEL_EXP' => 'Determina el nivel de tema mínimo para poder ser mostrado. Solo mostrará temas del nivel especificado y superior.', + 'RT_ANNOUNCEMENTS_FIRST' => 'Mostrar primero los anuncios', + 'RT_ANNOUNCEMENTS_FIRST_EXP' => 'Coloca los anuncios y anuncios globales de cada página de la lista al principio de esa página. Los anuncios más antiguos que no estén en la página no se añaden.', 'RT_ANTI_TOPICS' => 'Temas excluidos', 'RT_ANTI_TOPICS_EXP' => 'Las IDs de los temas a excluír, separados por "," (Por ejemplo: 7,9)
Si no quieres excluir un tema, simplemente introduce 0.', 'RT_PARENTS' => 'Mostrar foros padre', diff --git a/language/fr/info_acp_recenttopics.php b/language/fr/info_acp_recenttopics.php index 7a59591..1a631ad 100644 --- a/language/fr/info_acp_recenttopics.php +++ b/language/fr/info_acp_recenttopics.php @@ -59,6 +59,8 @@ 'RT_PAGE_NUMBERMAX_EXP' => 'Permet de saisir le nombre maximal de pages à afficher dans la pagination des sujets récents lorsque l’option « Afficher toutes les pages des sujets récents » est désactivée (décochée).', 'RT_MIN_TOPIC_LEVEL' => 'Niveau minimum du type de sujets affichés', 'RT_MIN_TOPIC_LEVEL_EXP' => 'Permet de sélectionner le niveau minimum du type de sujets à afficher dans la liste des sujets récents. Les sujets correspondants au niveau paramétré et aux niveaux supérieurs seront affichés.', + 'RT_ANNOUNCEMENTS_FIRST' => 'Afficher les annonces en premier', + 'RT_ANNOUNCEMENTS_FIRST_EXP' => 'Place les annonces et annonces globales de chaque page de la liste en haut de cette page. Les annonces plus anciennes qui ne figurent pas sur la page ne sont pas ajoutées.', 'RT_ANTI_TOPICS' => 'Exclure des sujets de l’affichage', 'RT_ANTI_TOPICS_EXP' => 'Permet de saisir les ID, séparés par une virgule (exemple : 7,9), des sujets à exclure de l’affichage des sujets récents.
Pour afficher tous les sujets saisir la valeur 0.', 'RT_PARENTS' => 'Afficher les forums parents', diff --git a/language/nl/info_acp_recenttopics.php b/language/nl/info_acp_recenttopics.php index 8ddc927..c1f2d17 100644 --- a/language/nl/info_acp_recenttopics.php +++ b/language/nl/info_acp_recenttopics.php @@ -59,6 +59,8 @@ 'RT_PAGE_NUMBERMAX_EXP' => 'Stel het maximum aantal pagina’s in.', 'RT_MIN_TOPIC_LEVEL' => 'Onderwerptypes', 'RT_MIN_TOPIC_LEVEL_EXP' => 'Stel het minimum weer te geven onderwerptype in.', + 'RT_ANNOUNCEMENTS_FIRST' => 'Mededelingen eerst tonen', + 'RT_ANNOUNCEMENTS_FIRST_EXP' => 'Plaatst de mededelingen en algemene mededelingen op elke pagina van de lijst bovenaan die pagina. Oudere mededelingen die niet op de pagina staan, worden niet toegevoegd.', 'RT_ANTI_TOPICS' => 'Uitgesloten onderwerpen', 'RT_ANTI_TOPICS_EXP' => 'Vul de onderwerp id’s in (bijvoorbeeld 7,9), anders 0. (deze nummers vind je in de url viewtopic.php?t=12345)', 'RT_PARENTS' => 'Weergeven van hoofdforums', diff --git a/language/pt/info_acp_recenttopics.php b/language/pt/info_acp_recenttopics.php index 2b188ac..674dc3b 100644 --- a/language/pt/info_acp_recenttopics.php +++ b/language/pt/info_acp_recenttopics.php @@ -43,6 +43,8 @@ 'RT_PAGE_NUMBERMAX_EXP' => 'Defina o máximo da página para exibir na paginação dos tópicos recentes, a menos que seja substituído.', 'RT_MIN_TOPIC_LEVEL' => 'Nível mínimo do tipo de tópico', 'RT_MIN_TOPIC_LEVEL_EXP' => 'Determina o nível mínimo do tipo de tópico a ser exibido. Ele só exibirá tópicos do nível definido e mais alto.', + 'RT_ANNOUNCEMENTS_FIRST' => 'Mostrar anúncios primeiro', + 'RT_ANNOUNCEMENTS_FIRST_EXP' => 'Coloca os anúncios e anúncios globais de cada página da lista no topo dessa página. Anúncios mais antigos que não estejam na página não são adicionados.', 'RT_ANTI_TOPICS' => 'ID de tópico excluído', 'RT_ANTI_TOPICS_EXP' => 'Os IDs de tópicos a excluir, separados por "," (Exemplo: 7,9)
O valor 0 desabilita esse comportamento.', 'RT_PARENTS' => 'Mostrar Fórum Pai', diff --git a/language/ru/info_acp_recenttopics.php b/language/ru/info_acp_recenttopics.php index 9aeb36c..0146860 100644 --- a/language/ru/info_acp_recenttopics.php +++ b/language/ru/info_acp_recenttopics.php @@ -42,6 +42,8 @@ 'RT_PAGE_NUMBERMAX_EXP' => 'Максимальное число страниц для отображения в блоке последних тем (если значение не переопределено).', 'RT_MIN_TOPIC_LEVEL' => 'Минимальный статус темы', 'RT_MIN_TOPIC_LEVEL_EXP' => 'Минимальный статус темы для отображения. В списке последних тем будут отображены темы, имеющие указанный статус, и выше.
(0 = обычная, 1 = прилепленная, 2 = объявление, 3 = важная)', + 'RT_ANNOUNCEMENTS_FIRST' => 'Показывать объявления первыми', + 'RT_ANNOUNCEMENTS_FIRST_EXP' => 'Перемещает объявления и глобальные объявления на каждой странице списка в начало этой страницы. Более старые объявления, которых нет на странице, не добавляются.', 'RT_ANTI_TOPICS' => 'Исключённые темы', 'RT_ANTI_TOPICS_EXP' => 'Разделённый запятыми список идентификаторов тем, которые не должны отображаться в списке последних тем (например: 7, 9)
Установите 0 для отключения данной функции.', 'RT_PARENTS' => 'Показать родительские форумы', diff --git a/language/sk/info_acp_recenttopics.php b/language/sk/info_acp_recenttopics.php index 69d8354..183578d 100644 --- a/language/sk/info_acp_recenttopics.php +++ b/language/sk/info_acp_recenttopics.php @@ -43,6 +43,8 @@ 'RT_PAGE_NUMBERMAX_EXP' => 'Nastavte maximum stránok pre zobrazenie v stránkovaní najnovších tém (ak nie je prepísané iným nastavením).', 'RT_MIN_TOPIC_LEVEL' => 'Minimálna úroveň témy', 'RT_MIN_TOPIC_LEVEL_EXP' => 'Určuje minimálnu úroveň typu témy pre zobrazenie. Zobrazené budú iba témy zvolenej úrovne a vyššie.', + 'RT_ANNOUNCEMENTS_FIRST' => 'Zobraziť oznámenia ako prvé', + 'RT_ANNOUNCEMENTS_FIRST_EXP' => 'Presunie oznámenia a globálne oznámenia na každej stránke zoznamu na začiatok danej stránky. Staršie oznámenia, ktoré na stránke nie sú, sa nepridávajú.', 'RT_ANTI_TOPICS' => 'Vylúčené témy', 'RT_ANTI_TOPICS_EXP' => 'ID tém na vylúčenie, oddelené čiarkou „," (príklad: 7,9)
Hodnota 0 túto funkciu vypne.', 'RT_PARENTS' => 'Zobraziť nadradené fóra', diff --git a/language/sv/info_acp_recenttopics.php b/language/sv/info_acp_recenttopics.php index 23f5226..130e7f5 100644 --- a/language/sv/info_acp_recenttopics.php +++ b/language/sv/info_acp_recenttopics.php @@ -44,6 +44,8 @@ 'RT_PAGE_NUMBERMAX_EXP' => 'Ange det maximala antalet sidor att visa i pagineringen av senaste trådar om det inte åsidosätts.', 'RT_MIN_TOPIC_LEVEL' => 'Lägsta trådtypsnivå', 'RT_MIN_TOPIC_LEVEL_EXP' => 'Bestämmer den lägsta nivån av trådtyp som ska visas. Endast trådar av den angivna nivån och högre visas.', + 'RT_ANNOUNCEMENTS_FIRST' => 'Visa meddelanden först', + 'RT_ANNOUNCEMENTS_FIRST_EXP' => 'Flyttar meddelanden och globala meddelanden på varje sida i listan överst på den sidan. Äldre meddelanden som inte finns på sidan läggs inte till.', 'RT_ANTI_TOPICS' => 'Exkluderade tråd-ID:n', 'RT_ANTI_TOPICS_EXP' => 'ID:n för trådar att exkludera, separerade med "," (Exempel: 7,9)
Värdet 0 inaktiverar denna funktion.', 'RT_PARENTS' => 'Visa överordnade forum', diff --git a/language/uk/info_acp_recenttopics.php b/language/uk/info_acp_recenttopics.php index 90560d9..22b1be2 100644 --- a/language/uk/info_acp_recenttopics.php +++ b/language/uk/info_acp_recenttopics.php @@ -43,6 +43,8 @@ 'RT_PAGE_NUMBERMAX_EXP' => 'Вкажіть максимальну кількість сторінок для відображення в списку тем.', 'RT_MIN_TOPIC_LEVEL' => 'Мінімальний тип теми', 'RT_MIN_TOPIC_LEVEL_EXP' => 'Цей параметр визначає мінімальний рівень типу тем. Буде відображено теми тільки вказаного рівня і вище.', + 'RT_ANNOUNCEMENTS_FIRST' => 'Показувати оголошення першими', + 'RT_ANNOUNCEMENTS_FIRST_EXP' => 'Переміщує оголошення та глобальні оголошення на кожній сторінці списку на початок цієї сторінки. Старіші оголошення, яких немає на сторінці, не додаються.', 'RT_ANTI_TOPICS' => 'Не відображати теми з вказаними ID', 'RT_ANTI_TOPICS_EXP' => 'Теми з вказаними ID будуть виключені зі списку відображення. Кожен ID теми має бути розділений комою “,” (Наприклад: 7,9)
Значення "0" деактивує цей список виключення.', 'RT_PARENTS' => 'Відображати батьківські форуми', diff --git a/migrations/v300/release_3_0_12.php b/migrations/v300/release_3_0_12.php new file mode 100644 index 0000000..2189171 --- /dev/null +++ b/migrations/v300/release_3_0_12.php @@ -0,0 +1,38 @@ +config['rt_announcements_first']); + } + + public static function depends_on() + { + return ['\avathar\recenttopics\migrations\v300\release_3_0_0']; + } + + public function update_data() + { + return [ + ['config.add', ['rt_announcements_first', 0]], + ]; + } + + public function revert_data() + { + return [ + ['config.remove', ['rt_announcements_first']], + ]; + } +} diff --git a/tests/core/announcements_first_test.php b/tests/core/announcements_first_test.php new file mode 100644 index 0000000..96cd61d --- /dev/null +++ b/tests/core/announcements_first_test.php @@ -0,0 +1,182 @@ + POST_NORMAL, 2 => POST_ANNOUNCE, 3 => POST_STICKY, 4 => POST_GLOBAL] as $topic_id => $type) + { + $rows[] = [ + 'topic_id' => $topic_id, 'forum_id' => 1, 'topic_type' => $type, 'topic_status' => ITEM_UNLOCKED, + 'topic_moved_id' => 0, 'topic_visibility' => ITEM_APPROVED, 'topic_posts_unapproved' => 0, + 'topic_reported' => 0, 'topic_attachment' => 0, 'topic_last_post_time' => 1700000000 - $topic_id, + 'topic_last_view_time' => 1700000000, 'topic_time' => 1699990000 - $topic_id, 'topic_last_post_id' => 99, + 'topic_last_post_subject' => 'Reply', 'topic_title' => "Topic $topic_id", 'topic_views' => 10, + 'icon_id' => 0, 'poll_start' => 0, 'topic_posted' => 0, 'topic_poster' => 2, + 'topic_first_poster_name' => 'testuser', 'topic_first_poster_colour' => '', + 'topic_last_poster_id' => 2, 'topic_last_poster_name' => 'testuser', + 'topic_last_poster_colour' => '', 'forum_name' => 'General', + ]; + } + return $rows; + } + + /** + * @param int $announcements_first Value of the ACP setting + * @return \avathar\recenttopics\core\recenttopics + */ + private function make_rt(int $announcements_first) + { + $rows = $this->page_rows(); + $db = $this->createMock(\phpbb\db\driver\driver_interface::class); + $db->method('sql_in_set')->willReturn('1=1'); + $db->method('sql_build_query')->willReturn('SELECT 1'); + $db->method('sql_query')->willReturn('result'); + $db->method('sql_query_limit')->willReturn('result'); + $db->method('sql_fetchrow')->willReturnCallback(function () use (&$rows) { + return array_shift($rows) ?? false; + }); + + $this->rendered = []; + $template = $this->createMock(\phpbb\template\template::class); + $template->method('assign_block_vars')->willReturnCallback(function ($loop, $vars) { + if ($loop === 'recent_topics') + { + $this->rendered[] = $vars['TOPIC_ID']; + } + }); + + $content_visibility = $this->createMock(\phpbb\content_visibility::class); + $content_visibility->method('get_count')->willReturn(1); + + $user = $this->createMock(\phpbb\user::class); + $user->data = ['user_id' => 2, 'is_registered' => true]; + $user->page = ['query_string' => '', 'page_name' => 'index.php']; + $user->session_id = 'testsession'; + $user->method('format_date')->willReturn('01 Jan 2026'); + $user->method('img')->willReturn(''); + + $dispatcher = new \phpbb\event\dispatcher(); + + $rt = new \avathar\recenttopics\core\recenttopics( + $this->createMock(\phpbb\auth\auth::class), + $this->createMock(\phpbb\cache\service::class), + new \phpbb\config\config(['posts_per_page' => 10, 'rt_topic_link_to' => 0, 'rt_announcements_first' => $announcements_first]), + $this->createMock(\phpbb\language\language::class), + $content_visibility, + $db, + $dispatcher, + $this->createMock(\phpbb\pagination::class), + $this->createMock(\phpbb\request\request_interface::class), + $template, + $user, + '/', + 'php', + $this->createMock(\phpbb\config\db_text::class) + ); + + foreach ([ + 'topic_list' => [1, 2, 3, 4], 'sort_topics' => 'topic_last_post_time', 'display_parent_forums' => false, + 'topics_per_page' => 10, 'unread_only' => false, 'icons' => [], 'rtstart' => 0, 'total_topics_limit' => 100, + ] as $name => $value) + { + $property = new \ReflectionProperty($rt, $name); + $property->setAccessible(true); + $property->setValue($rt, $value); + } + + // phpBB's real censor_text() (loaded by the CI bootstrap) reads global $config, $user and $auth. + $GLOBALS['phpbb_dispatcher'] = $dispatcher; + $GLOBALS['config'] = ['allow_smilies' => 0, 'allow_nocensors' => 1]; + $GLOBALS['user'] = $user; + $auth_global = $this->createMock(\phpbb\auth\auth::class); + $auth_global->method('acl_get')->willReturn(true); + $GLOBALS['auth'] = $auth_global; + + return $rt; + } + + private function render($rt): array + { + $method = new \ReflectionMethod($rt, 'fill_template'); + $method->setAccessible(true); + $method->invoke($rt, 'recent_topics', [], 4); + return $this->rendered; + } + + /** + * Setting on: the page's announcement and global announcement come first, still newest + * first among themselves; the rest keep their order and stickies are not moved up (#201). + */ + public function test_announcements_lead_the_page_when_setting_is_on() + { + $this->assertSame([2, 4, 1, 3], $this->render($this->make_rt(1))); + } + + /** + * Setting off (the default): the page keeps its time order. + */ + public function test_page_keeps_time_order_when_setting_is_off() + { + $this->assertSame([1, 2, 3, 4], $this->render($this->make_rt(0))); + } +} +} diff --git a/tests/functional/recenttopics_test.php b/tests/functional/recenttopics_test.php index 701124c..4755f94 100644 --- a/tests/functional/recenttopics_test.php +++ b/tests/functional/recenttopics_test.php @@ -211,6 +211,58 @@ public function test_acp_patreon_button_is_local() $this->assertSame(200, self::$client->getResponse()->getStatus()); } + /** + * With "Show announcements first" on, an announcement on the page is listed above a + * newer normal topic (#201). + */ + public function test_announcements_first_on_index() + { + $this->login(); + $this->set_config('rt_index', 1); + $this->set_config('rt_announcements_first', 1); + + // The announcement is older than the normal topic, so time order alone lists it second + $this->create_topic(2, 'RT Announcement 201', 'Announcement for issue 201.', array('topic_type' => POST_ANNOUNCE)); + $this->create_topic(2, 'RT Normal topic 201', 'Normal topic for issue 201.'); + + $crawler = self::request('GET', 'index.php?sid=' . $this->sid); + $titles = $crawler->filter('#recent-topics-box a.topictitle')->each(function ($node) { + return $node->text(); + }); + + $announcement = array_search('RT Announcement 201', $titles, true); + $normal = array_search('RT Normal topic 201', $titles, true); + $this->assertNotFalse($announcement, 'The announcement must be listed'); + $this->assertNotFalse($normal, 'The normal topic must be listed'); + $this->assertLessThan($normal, $announcement, 'The announcement must be listed above the newer normal topic'); + + $this->set_config('rt_announcements_first', 0); + } + + /** + * The ACP saves the "Show announcements first" checkbox (#201). + */ + public function test_acp_saves_announcements_first() + { + $this->login(); + $this->admin_login(); + $this->set_config('rt_announcements_first', 0); + + $crawler = self::request('GET', $this->acp_module_url()); + $form = $crawler->selectButton('submit')->form(); + $form['rt_announcements_first']->tick(); + self::submit($form); + + $db = $this->get_db(); + $result = $db->sql_query("SELECT config_value FROM phpbb_config WHERE config_name = 'rt_announcements_first'"); + $value = $db->sql_fetchfield('config_value'); + $db->sql_freeresult($result); + + $this->assertSame('1', (string) $value, 'Ticking the checkbox must store rt_announcements_first = 1'); + + $this->set_config('rt_announcements_first', 0); + } + // ----------------------------------------------------------------------- // Helper // ----------------------------------------------------------------------- diff --git a/tests/tests.md b/tests/tests.md index 3c71767..309cae9 100644 --- a/tests/tests.md +++ b/tests/tests.md @@ -44,6 +44,7 @@ tests/ ├── controller/ │ └── page_controller_test.php Dedicated /rt and /rt/simple page controller ├── core/ +│ ├── announcements_first_test.php ACP "Show announcements first" setting │ ├── forum_list_test.php Which forums the list may draw topics from │ ├── topic_count_test.php "Show all pages" count uses the forum list │ ├── topic_likes_test.php ACP "Show like counts" setting @@ -241,7 +242,22 @@ It reads the template and the image straight from disk. --- -## 10. Functional tests (`functional/recenttopics_test.php`) +## 10. Announcements first (`core/announcements_first_test.php`) + +**What this code does:** +With the ACP setting "Show announcements first" (`rt_announcements_first`) on, `fill_template()` moves the announcements and global announcements on the current page to the top of that page. Only rows already on the page move, so old announcements never come back into the list; stickies stay with the normal topics. + +**What this test file does:** +It runs `fill_template()` on one page of four topics in time order (normal, announcement, sticky, global announcement) and records the order in which they reach `assign_block_vars()`. + +| Test | Scenario | What it verifies | +|------|----------|-----------------| +| `test_announcements_lead_the_page_when_setting_is_on` | Setting on | Order is announcement, global announcement, normal, sticky: announcements first in their time order, the rest unchanged (#201) | +| `test_page_keeps_time_order_when_setting_is_off` | Setting off (default) | Order is unchanged | + +--- + +## 11. Functional tests (`functional/recenttopics_test.php`) **What this code does:** These tests start a real phpBB installation (using the test framework's built-in install), enable the extension, and make HTTP requests using a real browser-like crawler (Symfony DomCrawler). They check the actual rendered HTML for specific elements. @@ -258,6 +274,8 @@ Logs in as the admin account, optionally creates topics or changes config values | `test_rt_page_disabled` | `rt_page_enable` = 0; GET `/app.php/rt` | Page still loads (no 500 error); `` is present; `#recent-topics-box` must NOT appear | | `test_index_has_recent_topics` | `rt_index` = 1; create a topic; GET `index.php` | `` is present; `#recent-topics-box` contains the created topic title | | `test_acp_patreon_button_is_local` | Logged in as admin; GET the Recent Topics ACP page | The Patreon button's `src` is the bundled image, not `patreon.com`, and that image URL returns 200 (#200) | +| `test_announcements_first_on_index` | `rt_announcements_first` = 1; create an announcement, then a newer normal topic; GET `index.php` | The announcement is listed above the newer normal topic in `#recent-topics-box` (#201) | +| `test_acp_saves_announcements_first` | Admin ticks "Show announcements first" in the ACP and submits | `rt_announcements_first` is stored as 1 (#201) | | `test_ucp_preferences` | Logged in as admin; GET `/ucp.php?i=ucp_prefs&mode=view` | `input[name="rt_enable"]` and `input[name="rt_number"]` are present on the preferences page | ---