From a39b54206b2c44d04b9d1d39f0a75cefd02dacde Mon Sep 17 00:00:00 2001 From: carlobeltrame Date: Sat, 24 Jan 2026 14:42:34 +0100 Subject: [PATCH 1/7] Fix typo --- api/src/Entity/ContentNode/ChecklistNode.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/src/Entity/ContentNode/ChecklistNode.php b/api/src/Entity/ContentNode/ChecklistNode.php index f41a649a88..d3f9446798 100644 --- a/api/src/Entity/ContentNode/ChecklistNode.php +++ b/api/src/Entity/ContentNode/ChecklistNode.php @@ -105,9 +105,9 @@ public function copyFromPrototype($prototype, $entityMap): void { // copy all checklist-items foreach ($prototype->checklistItems as $itemPrototype) { /** @var ChecklistItem $itemPrototype */ - /** @var ChecklistItem $checklilstItem */ - $checklilstItem = $entityMap->get($itemPrototype); - $this->addChecklistItem($checklilstItem); + /** @var ChecklistItem $checklistItem */ + $checklistItem = $entityMap->get($itemPrototype); + $this->addChecklistItem($checklistItem); } } } From 8f579ecd731ae88e6402d64f6e4677ce95d0be0c Mon Sep 17 00:00:00 2001 From: carlobeltrame Date: Sat, 24 Jan 2026 14:45:23 +0100 Subject: [PATCH 2/7] Don't connect checklist nodes to checklist items from another camp --- api/src/Entity/ContentNode/ChecklistNode.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/api/src/Entity/ContentNode/ChecklistNode.php b/api/src/Entity/ContentNode/ChecklistNode.php index d3f9446798..bdebb1c625 100644 --- a/api/src/Entity/ContentNode/ChecklistNode.php +++ b/api/src/Entity/ContentNode/ChecklistNode.php @@ -104,10 +104,12 @@ public function copyFromPrototype($prototype, $entityMap): void { // copy all checklist-items foreach ($prototype->checklistItems as $itemPrototype) { - /** @var ChecklistItem $itemPrototype */ - /** @var ChecklistItem $checklistItem */ - $checklistItem = $entityMap->get($itemPrototype); - $this->addChecklistItem($checklistItem); + if ($this->getCamp()->getId() === $itemPrototype->getCamp()->getId()) { + /** @var ChecklistItem $itemPrototype */ + /** @var ChecklistItem $checklistItem */ + $checklistItem = $entityMap->get($itemPrototype); + $this->addChecklistItem($checklistItem); + } } } } From ff973f0f445de253115d654aa7cc00cfff1a793a Mon Sep 17 00:00:00 2001 From: carlobeltrame Date: Sat, 24 Jan 2026 15:48:51 +0100 Subject: [PATCH 3/7] Disable all ecamp-managed checklist prototypes Previously, we only manually disabled the Basis prototypes, but forgot about the Aufbau prototypes. We can completely remove the prototypes from the dev data. --- api/migrations/dev-data/data.sql | 135 ------------------ .../schema/Version20260124154000.php | 29 ++++ 2 files changed, 29 insertions(+), 135 deletions(-) create mode 100644 api/migrations/schema/Version20260124154000.php diff --git a/api/migrations/dev-data/data.sql b/api/migrations/dev-data/data.sql index 81ed738f9e..2a543e6735 100644 --- a/api/migrations/dev-data/data.sql +++ b/api/migrations/dev-data/data.sql @@ -1750,10 +1750,6 @@ INSERT INTO public.category_contenttype (category_id, contenttype_id) VALUES INSERT INTO public.checklist (id, createtime, updatetime, name, campid, isprototype, checklistprototypeid) VALUES - ('000100000000', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'PBS Basiskurs Wolfsstufe', NULL, true, NULL), - ('000200000000', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'PBS Basiskurs Pfadistufe', NULL, true, NULL), - ('000300000000', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'PBS Aufbaukurs Wolfsstufe', NULL, true, NULL), - ('000400000000', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'PBS Aufbaukurs Pfadistufe', NULL, true, NULL), ('ebbd0c61eb85', '2024-09-28 16:41:19', '2024-09-28 16:41:19', 'Ausbildungsziele', '5d28f99890bc', false, NULL), ('261ab1ae2947', '2025-05-05 14:37:17', '2025-07-14 12:36:47', 'MSdS Aumonier·ère', NULL, true, NULL), ('96d088458d49', '2025-06-30 12:18:25', '2025-07-01 08:27:10', 'MSdS Base Éclais', NULL, true, NULL), @@ -1840,137 +1836,6 @@ INSERT INTO public.checklist (id, createtime, updatetime, name, campid, isprotot INSERT INTO public.checklist_item (id, createtime, updatetime, text, "position", checklistid, parentid) VALUES ('18db4adbe9b1', '2024-09-28 16:41:19', '2024-09-28 16:41:19', 'Der Kurs vermittelt den TN die Pfadigrundlagen.', 0, 'ebbd0c61eb85', NULL), - ('000100010000', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Der Kurs vermittelt den TN die Pfadigrundlagen.', 0, '000100000000', NULL), - ('000100010001', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Entwicklungsstand und Bedürfnisse der Kinder der Wolfsstufe', 0, '000100000000', '000100010000'), - ('000100010002', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Wolfsstufensymbolik', 1, '000100000000', '000100010000'), - ('000100010003', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Persönliche Auseinandersetzung mit Gesetz und Versprechen der Roverstufe', 2, '000100000000', '000100010000'), - ('000100010004', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Bezug der Pfadigrundlagen zum Pfadialltag', 3, '000100000000', '000100010000'), - ('000100010005', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Stufenmodell und Abgrenzung zw. Biber-, Wolfs- und Pfadistufe', 4, '000100000000', '000100010000'), - ('000100010006', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Ausgestaltung der sieben Pfadimethoden und fünf Pfadibeziehungen auf der Wolfsstufe', 5, '000100000000', '000100010000'), - ('000100020000', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Der Kurs bildet die TN aus, ein Programm für die Wolfsstufe zu planen, durchzuführen und auszuwerten.', 1, '000100000000', NULL), - ('000100020001', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Einkleidung von Aktivitäten und Quartalsprogrammen', 0, '000100000000', '000100020000'), - ('000100020002', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Methoden zur Planung, Durchführung und Auswertung von Programmen', 1, '000100000000', '000100020000'), - ('000100020003', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Quartalsprogramm planen', 2, '000100000000', '000100020000'), - ('000100020004', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Abenteuer als Alternative zum Quartalsprogramm und als Form der Mitbestimmung auf der Wolfsstufe', 3, '000100000000', '000100020000'), - ('000100020005', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'wesentliche Punkte beim Organisieren von Weekends', 4, '000100000000', '000100020000'), - ('000100020006', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Planen, Durchführen und Auswerten von J+S Aktivitäten für die Wolfsstufe', 5, '000100000000', '000100020000'), - ('000100020007', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Planen, Durchführen und Auswerten von Wanderungen für die Wolfsstufe', 6, '000100000000', '000100020000'), - ('000100020008', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Inklusive Gestaltung des Programms, damit sich alle Wölfe wohlfühlen und ihre Persönlichkeiten individuell entwickeln können', 7, '000100000000', '000100020000'), - ('000100030000', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Der Kurs bildet die TN zu verantwortungsbewussten Mitgliedern eines Leitungsteams aus.', 2, '000100000000', NULL), - ('000100030001', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Funktion sowie Rechte und Pflichten als Mitglied eines Leitungsteams der Wolfsstufe', 0, '000100000000', '000100030000'), - ('000100030002', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Leitwölfe betreuen', 1, '000100000000', '000100030000'), - ('000100030003', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Umgang mit Wölfen mit herausforderndem Verhalten', 2, '000100000000', '000100030000'), - ('000100030004', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Sicherheitskonzepte für sicherheitsrelevante Aktivitäten planen und umsetzen', 3, '000100000000', '000100030000'), - ('000100030005', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Angebote und Anlaufstellen des Kantonalverbands / der Region sowie Krisenkonzept', 4, '000100000000', '000100030000'), - ('000100030006', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'eigene Leiterpersönlichkeit und Rolle im Team', 5, '000100000000', '000100030000'), - ('000100030007', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Regeln für konstruktive Gespräche im Leitungsteam', 6, '000100000000', '000100030000'), - ('000100030008', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Möglichkeiten der Aus- und Weiterbildung', 7, '000100000000', '000100030000'), - ('000100030009', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Sexuelle Ausbeutung und Grenzverletzungen, mögliche heikle Situationen in Aktivitäten und vorbeugende Massnahmen', 8, '000100000000', '000100030000'), - ('000100040000', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Der Kurs befähigt die TN, Aktivitäten wolfsstufengerecht zu gestalten.', 3, '000100000000', NULL), - ('000100040001', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Pfadimethode „Persönlicher Fortschritt fördern": Inhalte der Etappen und Spezialitäten in Aktivitäten der Wolfsstufe einbauen', 0, '000100000000', '000100040000'), - ('000100040002', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Arbeiten mit Gesetz und Versprechen auf der Wolfsstufe', 1, '000100000000', '000100040000'), - ('000100040003', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Gestaltung von Lagerfeuern auf der Wolfsstufe', 2, '000100000000', '000100040000'), - ('000100040004', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Vertiefen der Kenntnisse und stufengerechtes Vermitteln der Wolfsstufentechnik', 3, '000100000000', '000100040000'), - ('000200010000', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Der Kurs vermittelt den TN die Pfadigrundlagen.', 0, '000200000000', NULL), - ('000200010001', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Entwicklungsstand und Bedürfnisse der Kinder und Jugendlichen der Pfadistufe', 0, '000200000000', '000200010000'), - ('000200010002', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Persönliche Auseinandersetzung mit Gesetz und Versprechen der Roverstufe', 1, '000200000000', '000200010000'), - ('000200010003', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Bezug der Pfadigrundlagen zum Pfadialltag', 2, '000200000000', '000200010000'), - ('000200010004', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Stufenmodell und Abgrenzung zw. Wolfs-, Pfadi- und Piostufe', 3, '000200000000', '000200010000'), - ('000200010005', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Ausgestaltung der sieben Pfadimethoden und fünf Pfadibeziehungen auf der Pfadistufe', 4, '000200000000', '000200010000'), - ('000200020000', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Der Kurs bildet die TN aus, ein Programm für die Pfadistufe zu planen, durchzuführen und auszuwerten.', 1, '000200000000', NULL), - ('000200020001', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Einkleidung von Aktivitäten und Quartalsprogrammen', 0, '000200000000', '000200020000'), - ('000200020002', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Methoden zur Planung, Durchführung und Auswertung von Programmen', 1, '000200000000', '000200020000'), - ('000200020003', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Quartalsprogramm planen (inkl. Integration von Fähnliaktivitäten in geeigneter Weise)', 2, '000200000000', '000200020000'), - ('000200020004', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Projekt als Alternative zum Quartalsprogramm und als Form der Mitbestimmung auf der Pfadistufe', 3, '000200000000', '000200020000'), - ('000200020005', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'wesentliche Punkte beim Organisieren von Weekends', 4, '000200000000', '000200020000'), - ('000200020006', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Planen, Durchführen und Auswerten von J+S Aktivitäten für die Pfadistufe', 5, '000200000000', '000200020000'), - ('000200020007', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Planen, Durchführen und Auswerten von Wanderungen für die Pfadistufe', 6, '000200000000', '000200020000'), - ('000200020008', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Inklusive Gestaltung des Programms, damit sich alle Pfadis wohlfühlen und ihre Persönlichkeiten individuell entwickeln können', 7, '000200000000', '000200020000'), - ('000200030000', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Der Kurs bildet die TN zu verantwortungsbewussten Mitgliedern eines Leitungsteams und zu Betreuenden von Leitpfadis aus.', 2, '000200000000', NULL), - ('000200030001', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Funktion sowie Rechte und Pflichten als Mitglied eines Leitungsteams der Pfadistufe', 0, '000200000000', '000200030000'), - ('000200030002', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Rolle der Leitpfadis und ihre Betreuung, insbesondere bei Fähnliaktivitäten', 1, '000200000000', '000200030000'), - ('000200030003', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Umgang mit Pfadis mit herausforderndem Verhalten', 2, '000200000000', '000200030000'), - ('000200030004', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Sicherheitskonzepte für sicherheitsrelevante Aktivitäten (inkl. herausfordernde Fähnliaktivitäten) planen und umsetzen', 3, '000200000000', '000200030000'), - ('000200030005', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Angebote und Anlaufstellen des Kantonalverbands / der Region inkl. Krisenkonzept', 4, '000200000000', '000200030000'), - ('000200030006', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'eigene Leiterpersönlichkeit und Rolle im Team', 5, '000200000000', '000200030000'), - ('000200030007', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Regeln für konstruktive Gespräche im Leitungsteam', 6, '000200000000', '000200030000'), - ('000200030008', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Möglichkeiten der Aus- und Weiterbildung', 7, '000200000000', '000200030000'), - ('000200030009', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Sexuelle Ausbeutung, Grenzverletzungen, mögliche heikle Situationen in Aktivitäten und vorbeugende Massnahmen', 8, '000200000000', '000200030000'), - ('000200040000', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Der Kurs befähigt die TN, Aktivitäten pfadistufengerecht zu gestalten.', 3, '000200000000', NULL), - ('000200040001', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Pfadimethode „Persönlicher Fortschritt fördern": Inhalte der Etappen und Spezialitäten in Aktivitäten der Pfadistufen einbauen', 0, '000200000000', '000200040000'), - ('000200040002', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Arbeiten mit Gesetz und Versprechen auf der Pfadistufe', 1, '000200000000', '000200040000'), - ('000200040003', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Gestaltung von Lagerfeuern auf der Pfadistufe', 2, '000200000000', '000200040000'), - ('000200040004', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Vertiefen der Kenntnisse und stufengerechtes Vermitteln der Pfadistufentechnik', 3, '000200000000', '000200040000'), - ('000300010000', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Der Kurs ermöglicht den TN die Auseinandersetzung mit den Pfadigrundlagen.', 0, '000300000000', NULL), - ('000300010001', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Vertiefung über die Bedürfnisse der Kinder der Wolfsstufe', 0, '000300000000', '000300010000'), - ('000300010002', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Wolfsstufensymbolik, ihr Bezug zu den Pfadigrundlagen und ihre Einbindung im Programm', 1, '000300000000', '000300010000'), - ('000300010003', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Pfadigrundlagen als Hilfsmittel zum Sicherstellen der Ausgewogenheit des Programms', 2, '000300000000', '000300010000'), - ('000300010004', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Einbindung der sieben Methoden und fünf Beziehungen in das Programm. als Mittel zur Erreichung der Ziele der Wolfsstufe', 3, '000300000000', '000300010000'), - ('000300010005', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Stille Momente / Förderung der Beziehung zum Spirituellen auf der Wolfsstufe', 4, '000300000000', '000300010000'), - ('000300010006', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Pfadimethode "Persönlichen Fortschritt fördern": Längerfristige Einbindung ins Programm', 5, '000300000000', '000300010000'), - ('000300020000', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Der Kurs bildet die TN zu verantwortlichen Einheitsleitenden oder Stufenleitenden aus.', 1, '000300000000', NULL), - ('000300020001', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Funktion sowie Rechte und Pflichten als Einheitsleitende oder Stufenleitende', 0, '000300000000', '000300020000'), - ('000300020002', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Organisation der Einheit / Stufe und längerfristige Planung', 1, '000300000000', '000300020000'), - ('000300020003', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Pflege von Elternkontakten und Öffenzlichkeitsarbeit', 2, '000300000000', '000300020000'), - ('000300020004', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Leiterpersönlichkeit: Auftreten und Vertreten von Anliegen', 3, '000300000000', '000300020000'), - ('000300020005', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Rolle im kantonalen Krisenkonzept', 4, '000300000000', '000300020000'), - ('000300020006', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Umgang mit Kindern mit herausforderndem Verhalten', 5, '000300000000', '000300020000'), - ('000300020007', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Gesundheitsförderung: psychisches, physisches und soziales Wohlbefinden positiv beeinflussen', 6, '000300000000', '000300020000'), - ('000300020008', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Suchtthematik: Prävention im Programm der Wolfsstufe und Regeln im Leitungsteam', 7, '000300000000', '000300020000'), - ('000300020009', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Sexuelle Ausbeutung und Grenzverletzungen: Verantwortung als Stufenleitende und vorbeugende Massnahmen', 8, '000300000000', '000300020000'), - ('000300020010', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Gewalt: verschiedene Formen von Gewalt (u.a. psychische, physische und strukturelle) und Möglichkeiten, diesen vorzubeugen', 9, '000300000000', '000300020000'), - ('000300030000', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Der Kurs bildet die TN zu verantwortlichen Lagerleitenden aus.', 2, '000300000000', NULL), - ('000300030001', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Funktion, Rechte und Pflichten als Lagerleiter*in', 0, '000300000000', '000300030000'), - ('000300030002', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Funktionen und Aufgaben von Coach und AL, insbesondere bei der Lagerbetreuung', 1, '000300000000', '000300030000'), - ('000300030003', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Einsetzung des Lagerreglements gezielt für die Planung', 2, '000300000000', '000300030000'), - ('000300030004', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Ablauf der Lagerplanung und -administration', 3, '000300000000', '000300030000'), - ('000300030005', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Gestaltung des Lagerprogramms', 4, '000300000000', '000300030000'), - ('000300030006', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Gesundheit im Lager, Umgang mit Krankheiten', 5, '000300000000', '000300030000'), - ('000300030007', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Sicherheitskonzepte für Lager', 6, '000300000000', '000300030000'), - ('000300030008', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'sicherheitsrelevante Aktivitäten planen und analysieren, Durchführungsentscheid, Anpassungen während Aktivitäten', 7, '000300000000', '000300030000'), - ('000300030009', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Anspruchsvolle J+S-Aktivitäten für die Wolfstufe planen, durchführen und auswerten', 8, '000300000000', '000300030000'), - ('000300030010', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Wanderungen auf der Wolfsstufe kritisch beurteilen', 9, '000300000000', '000300030000'), - ('000300040000', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Der Kurs befähigt die TN, junge Leitende anzuleiten und zu betreuen.', 3, '000300000000', NULL), - ('000300040001', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Teamleiter*in sein und Zusammenarbeit im Team', 0, '000300000000', '000300040000'), - ('000300040002', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'inklusive Atmosphäre schaffen in einem Leitungsteam', 1, '000300000000', '000300040000'), - ('000300040003', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Sitzungen vorbereiten und leiten', 2, '000300000000', '000300040000'), - ('000300040004', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'relevante und konstruktive Rückmeldung zu einem Programmteil für die Wolfstufe geben', 3, '000300000000', '000300040000'), - ('000300040005', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Betreuung und Förderung einzelner Teammitglieder', 4, '000300000000', '000300040000'), - ('000400010000', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Der Kurs ermöglicht den TN die Auseinandersetzung mit den Pfadigrundlagen.', 0, '000400000000', NULL), - ('000400010001', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Vertiefung über die Bedürfnisse der Kinder und Jugendlichen der Pfadistufe', 0, '000400000000', '000400010000'), - ('000400010002', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Pfadigrundlagen als Hilfsmittel zum Sicherstellen der Ausgewogenheit des Programms', 1, '000400000000', '000400010000'), - ('000400010003', '2024-09-28 10:00:00', '2024-09-28 10:00:00', '…Einbindung der sieben Methoden und fünf Pfadfinderbeziehungen in das Programm. als Mittel zur Erreichung der Ziele der Wolfsstufe', 2, '000400000000', '000400010000'), - ('000400010004', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Stille Momente / Förderung der Beziehung zum Spirituellen auf der Pfadistufe', 3, '000400000000', '000400010000'), - ('000400010005', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Pfadimethode „Persönlichen Fortschritt fördern": Längerfristige Einbindung ins Programm', 4, '000400000000', '000400010000'), - ('000400020000', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Der Kurs bildet die TN zu verantwortlichen Einheitsleitenden oder Stufenleitenden aus.', 1, '000400000000', NULL), - ('000400020001', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Funktion sowie Rechte und Pflichten als Einheitsleitende oder Stufenleitende', 0, '000400000000', '000400020000'), - ('000400020002', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Organisation der Einheit / Stufe, insbesondere Betreuung der Leitpfadis, und längerfristige Planung', 1, '000400000000', '000400020000'), - ('000400020003', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Pflege von Elternkontakten', 2, '000400000000', '000400020000'), - ('000400020004', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Leiterpersönlichkeit: Auftreten und Vertreten von Anliegen', 3, '000400000000', '000400020000'), - ('000400020005', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Rolle im kantonalen Krisenkonzept', 4, '000400000000', '000400020000'), - ('000400020006', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Gesundheitsförderung: psychisches, physisches und soziales Wohlbefinden positiv beeinflussen', 5, '000400000000', '000400020000'), - ('000400020007', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Umgang mit Kinder mit herausforderndem Verhalten', 6, '000400000000', '000400020000'), - ('000400020008', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Suchtthematik: Prävention im Programm der Pfadistufe und Regeln im Leitungsteam', 7, '000400000000', '000400020000'), - ('000400020009', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Sexuelle Ausbeutung und Grenzverletzungen: Verantwortung als Stufenleitende und vorbeugende Massnahmen', 8, '000400000000', '000400020000'), - ('000400020010', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Gewalt: verschiedene Formen von Gewalt (u.a. psychische, physische und strukturelle) und Möglichkeiten, diesen vorzubeugen', 9, '000400000000', '000400020000'), - ('000400030000', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Der Kurs bildet die TN zu verantwortlichen Lagerleitenden aus.', 2, '000400000000', NULL), - ('000400030001', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Funktion, Rechte und Pflichten als Lagerleiter*in', 0, '000400000000', '000400030000'), - ('000400030002', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Funktionen und Aufgaben von Coach und AL, insbesondere bei der Lagerbetreuung', 1, '000400000000', '000400030000'), - ('000400030003', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Einsetzung des Lagerreglements gezielt für die Planung', 2, '000400000000', '000400030000'), - ('000400030004', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Ablauf der Lagerplanung und -administration', 3, '000400000000', '000400030000'), - ('000400030005', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Fähnliaktivitäten im Lagerprogramm und deren Betreuung', 4, '000400000000', '000400030000'), - ('000400030006', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Gestaltung des Lagerprogramms', 5, '000400000000', '000400030000'), - ('000400030007', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Gesundheit im Lager, Umgang mit Krankheiten', 6, '000400000000', '000400030000'), - ('000400030008', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Sicherheitskonzepte für Lager', 7, '000400000000', '000400030000'), - ('000400030009', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'sicherheitsrelevante Aktivitäten, Durchführungsentscheid, Anpassungen während Aktivitäten', 8, '000400000000', '000400030000'), - ('000400030010', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Spiele abändern und grössere sportliche Aktivitäten organisieren', 9, '000400000000', '000400030000'), - ('000400030011', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Wanderungen auf der Pfadistufe', 10, '000400000000', '000400030000'), - ('000400040000', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Der Kurs befähigt die TN, junge Leitende anzuleiten und zu betreuen.', 3, '000400000000', NULL), - ('000400040001', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Teamleiter*in sein und Zusammenarbeit im Team', 0, '000400000000', '000400040000'), - ('000400040002', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'inklusive Atmosphäre schaffen in einem Leitungsteam', 1, '000400000000', '000400040000'), - ('000400040003', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'relevante und konstruktive Rückmeldung zu einem Programmteilgeben', 2, '000400000000', '000400040000'), - ('000400040004', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Sitzungen vorbereiten und leiten', 3, '000400000000', '000400040000'), - ('000400040005', '2024-09-28 10:00:00', '2024-09-28 10:00:00', 'Betreuung und Förderung einzelner Teammitglieder', 4, '000400000000', '000400040000'), ('d89bc5e145fc', '2024-09-28 16:41:19', '2024-09-28 16:41:19', 'Entwicklungsstand und Bedürfnisse der Kinder der Wolfsstufe', 0, 'ebbd0c61eb85', '18db4adbe9b1'), ('4f5f857dd79c', '2024-09-28 16:41:19', '2024-09-28 16:41:19', 'Wolfsstufensymbolik', 1, 'ebbd0c61eb85', '18db4adbe9b1'), ('2526365b41bd', '2024-09-28 16:41:19', '2024-09-28 16:41:19', 'Persönliche Auseinandersetzung mit Gesetz und Versprechen der Roverstufe', 2, 'ebbd0c61eb85', '18db4adbe9b1'), diff --git a/api/migrations/schema/Version20260124154000.php b/api/migrations/schema/Version20260124154000.php new file mode 100644 index 0000000000..62e07c23a5 --- /dev/null +++ b/api/migrations/schema/Version20260124154000.php @@ -0,0 +1,29 @@ +addSql("UPDATE public.checklist c SET isprototype = FALSE WHERE c.id IN ('000100000000', '000200000000', '000300000000', '000400000000');"); + } + + #[\Override] + public function down(Schema $schema): void { + $this->addSql("UPDATE public.checklist c SET isprototype = TRUE WHERE c.id IN ('000100000000', '000200000000', '000300000000', '000400000000');"); + } +} From 66c1d35265969fabde24f53622844b1af066015a Mon Sep 17 00:00:00 2001 From: carlobeltrame Date: Sat, 24 Jan 2026 17:30:16 +0100 Subject: [PATCH 4/7] Remove invalid cross-camp connections from checklist nodes to checklist items --- .../schema/Version20260124154500.php | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 api/migrations/schema/Version20260124154500.php diff --git a/api/migrations/schema/Version20260124154500.php b/api/migrations/schema/Version20260124154500.php new file mode 100644 index 0000000000..ce78609dae --- /dev/null +++ b/api/migrations/schema/Version20260124154500.php @@ -0,0 +1,51 @@ + checklist item connections (across camps)'; + } + + public function up(Schema $schema): void { + $this->addSql(<<<'EOF' + DELETE FROM public.checklistnode_checklistitem + WHERE (checklistnode_checklistitem.checklistnode_id || '#' || checklistnode_checklistitem.checklistitem_id) IN + (SELECT (cnci.checklistnode_id || '#' || cnci.checklistitem_id) FROM checklistnode_checklistitem cnci + INNER JOIN content_node cn ON cn.id=cnci.checklistnode_id + INNER JOIN content_node root ON root.id=COALESCE(cn.rootid, cn.id) + INNER JOIN activity a ON a.rootcontentnodeid=root.id + INNER JOIN checklist_item ci ON ci.id=cnci.checklistitem_id + INNER JOIN checklist c ON ci.checklistid = c.id + WHERE c.campid != a.campid); + EOF); + + $this->addSql(<<<'EOF' + DELETE FROM public.checklistnode_checklistitem + WHERE (checklistnode_checklistitem.checklistnode_id || '#' || checklistnode_checklistitem.checklistitem_id) IN + (SELECT (cnci.checklistnode_id || '#' || cnci.checklistitem_id) FROM checklistnode_checklistitem cnci + INNER JOIN content_node cn ON cn.id=cnci.checklistnode_id + INNER JOIN content_node root ON root.id=COALESCE(cn.rootid, cn.id) + INNER JOIN category cat ON cat.rootcontentnodeid=root.id + INNER JOIN checklist_item ci ON ci.id=cnci.checklistitem_id + INNER JOIN checklist c ON ci.checklistid = c.id + WHERE c.campid != cat.campid); + EOF); + } + + #[\Override] + public function down(Schema $schema): void { + // not possible + } +} From 1643a02f7a2fe4475a71be06b6564673af24c3b9 Mon Sep 17 00:00:00 2001 From: carlobeltrame Date: Sat, 24 Jan 2026 17:54:10 +0100 Subject: [PATCH 5/7] Only copy the checklistNode-checklistItem connections when similar checklist items are present in target camp --- api/src/Entity/ContentNode/ChecklistNode.php | 63 ++++- .../Api/Activities/CreateActivityTest.php | 51 +++- api/tests/Entity/ChecklistNodeTest.php | 218 ++++++++++++++++++ 3 files changed, 323 insertions(+), 9 deletions(-) create mode 100644 api/tests/Entity/ChecklistNodeTest.php diff --git a/api/src/Entity/ContentNode/ChecklistNode.php b/api/src/Entity/ContentNode/ChecklistNode.php index bdebb1c625..e94b2137d2 100644 --- a/api/src/Entity/ContentNode/ChecklistNode.php +++ b/api/src/Entity/ContentNode/ChecklistNode.php @@ -9,6 +9,7 @@ use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; +use App\Entity\Checklist; use App\Entity\ChecklistItem; use App\Entity\ContentNode; use App\Repository\ChecklistNodeRepository; @@ -102,14 +103,70 @@ public function removeChecklistItem(ChecklistItem $checklistItem) { public function copyFromPrototype($prototype, $entityMap): void { parent::copyFromPrototype($prototype, $entityMap); - // copy all checklist-items - foreach ($prototype->checklistItems as $itemPrototype) { - if ($this->getCamp()->getId() === $itemPrototype->getCamp()->getId()) { + // copy the connections to checklist items + if ($entityMap->belongsToTargetCamp($prototype)) { + foreach ($prototype->checklistItems as $itemPrototype) { /** @var ChecklistItem $itemPrototype */ /** @var ChecklistItem $checklistItem */ $checklistItem = $entityMap->get($itemPrototype); $this->addChecklistItem($checklistItem); } + } else { + /** @var ChecklistItem[] $checklistItemsInCamp */ + $checklistItemsInCamp = array_merge(...array_values(array_map(function (Checklist $checklist) { + return $checklist->getChecklistItems(); + }, $entityMap->getTargetCamp()->getChecklists()))); + foreach ($prototype->checklistItems as $itemPrototype) { + /** @var ChecklistItem $itemPrototype */ + /** @var ChecklistItem $knownEquivalent */ + // First, look up whether we already know a replacement + $knownEquivalent = $entityMap->get($itemPrototype); + if ($knownEquivalent !== $itemPrototype) { + $this->addChecklistItem($knownEquivalent); + + continue; + } + + // Calculate a score for how well each item in the target camp matches the prototype item + $matches = array_map(function (ChecklistItem $existingItem) use ($itemPrototype) { + $score = 0; + if ($existingItem->text !== $itemPrototype->text) { + return $score; + } + ++$score; + + /** @var ChecklistItem $parent */ + $parent = $itemPrototype->getParent(); + + /** @var ChecklistItem $existingParent */ + $existingParent = $existingItem->getParent(); + while (null !== $parent && null !== $existingParent && $score < 10) { + if ($existingParent->text !== $parent->text) { + return $score; + } + ++$score; + + /** @var ChecklistItem $parent */ + $parent = $parent->getParent(); + } + if ($existingItem->checklist->name !== $itemPrototype->checklist->name) { + return $score; + } + + return $score + 1; + }, $checklistItemsInCamp); + + // Use the checklist with the largest positive score, if any + $maxScore = max([0, ...$matches]); + $bestMatchIndex = array_find_key($matches, function ($match) use ($maxScore) { + return $match === $maxScore; + }); + if ($maxScore > 0 && null !== $bestMatchIndex) { + $result = $checklistItemsInCamp[$bestMatchIndex]; + $entityMap->add($itemPrototype, $result); + $this->addChecklistItem($result); + } + } } } } diff --git a/api/tests/Api/Activities/CreateActivityTest.php b/api/tests/Api/Activities/CreateActivityTest.php index 031234bdd2..84215c377d 100644 --- a/api/tests/Api/Activities/CreateActivityTest.php +++ b/api/tests/Api/Activities/CreateActivityTest.php @@ -5,6 +5,7 @@ use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\Post; use App\Entity\Activity; +use App\Entity\ContentNode\ChecklistNode; use App\Entity\User; use App\Tests\Api\ECampApiTestCase; use App\Tests\Constraints\CompatibleHalResponse; @@ -584,7 +585,7 @@ public function testCreateActivityFromCopySourceValidatesAccess() { } public function testCreateActivityFromCopySourceWithinSameCamp() { - static::createClientWithCredentials()->request( + $response = static::createClientWithCredentials()->request( 'POST', '/activities', ['json' => $this->getExampleWritePayload( @@ -598,6 +599,34 @@ public function testCreateActivityFromCopySourceWithinSameCamp() { // Activity created $this->assertResponseStatusCodeSame(201); + $responseArray = $response->toArray(); + $contentNodes = $responseArray['_embedded']['contentNodes']; + + // Embedded MaterialNode -> MaterialItems + // Test MaterialList is not nulled + $materialNodes = array_filter($contentNodes, fn ($cn) => 'Material' == $cn['contentTypeName']); + $this->assertCount(1, $materialNodes); + + $materialNode = reset($materialNodes); + $materialItems = $materialNode['_embedded']['materialItems']; + $this->assertCount(1, $materialItems); + + $materialItem = reset($materialItems); + $this->assertEquals($this->getIriFor('materialList1'), $materialItem['_links']['materialList']['href']); + + // Embedded ChecklistNode -> ChecklistItems + // Test ChecklistItem connections are copied + $checklistNodes = array_filter($contentNodes, fn ($cn) => 'Checklist' == $cn['contentTypeName']); + $this->assertCount(1, $checklistNodes); + + $checklistNodeId = reset($checklistNodes)['id']; + $checklistNode = $this->getEntityManager()->getRepository(ChecklistNode::class)->find($checklistNodeId); + + $connectedChecklistItems = $checklistNode->getChecklistItems(); + $this->assertCount(1, $connectedChecklistItems); + $connectedChecklistItem = reset($connectedChecklistItems); + + $this->assertEquals($this->getIriFor('checklistItem1_1_1'), $this->getIriFor($connectedChecklistItem)); } public function testCreateActivityFromCopySourceAcrossCamp() { @@ -622,12 +651,11 @@ public function testCreateActivityFromCopySourceAcrossCamp() { // Activity created $this->assertResponseStatusCodeSame(201); - - // Embedded MaterialNode -> MaterialItems - // Test MaterialList is nulled $responseArray = $response->toArray(); $contentNodes = $responseArray['_embedded']['contentNodes']; + // Embedded MaterialNode -> MaterialItems + // Test MaterialList is nulled $materialNodes = array_filter($contentNodes, fn ($cn) => 'Material' == $cn['contentTypeName']); $this->assertCount(1, $materialNodes); @@ -635,8 +663,19 @@ public function testCreateActivityFromCopySourceAcrossCamp() { $materialItems = $materialNode['_embedded']['materialItems']; $this->assertCount(1, $materialItems); - $materailItem = reset($materialItems); - $this->assertNull($materailItem['_links']['materialList']); + $materialItem = reset($materialItems); + $this->assertNull($materialItem['_links']['materialList']); + + // Embedded ChecklistNode -> ChecklistItems + // Test ChecklistItem connections are not copied because no matching checklist items are present in target camp + $checklistNodes = array_filter($contentNodes, fn ($cn) => 'Checklist' == $cn['contentTypeName']); + $this->assertCount(1, $checklistNodes); + + $checklistNodeId = reset($checklistNodes)['id']; + $checklistNode = $this->getEntityManager()->getRepository(ChecklistNode::class)->find($checklistNodeId); + + $connectedChecklistItems = $checklistNode->getChecklistItems(); + $this->assertCount(0, $connectedChecklistItems); } /** diff --git a/api/tests/Entity/ChecklistNodeTest.php b/api/tests/Entity/ChecklistNodeTest.php new file mode 100644 index 0000000000..8d3f420f3b --- /dev/null +++ b/api/tests/Entity/ChecklistNodeTest.php @@ -0,0 +1,218 @@ +camp = new Camp(); + $this->checklist = new Checklist(); + $this->checklist->name = 'checklist1'; + $this->camp->addChecklist($this->checklist); + + $this->rootNode = new ColumnLayout(); + $campRootContentNode = new CampRootContentNode(); + $campRootContentNode->camp = $this->camp; + $campRootContentNode->rootContentNode = $this->rootNode; + $this->rootNode->campRootContentNodes->add($campRootContentNode); + + $this->itemPrototype1 = new ChecklistItem(); + $this->itemPrototype1->text = 'item1'; + $this->checklist->addChecklistItem($this->itemPrototype1); + $this->itemPrototype2 = new ChecklistItem(); + $this->itemPrototype2->text = 'item2'; + $this->checklist->addChecklistItem($this->itemPrototype2); + $this->itemPrototype3 = new ChecklistItem(); + $this->itemPrototype3->text = 'item3'; + $this->itemPrototype2->addChild($this->itemPrototype3); + $this->checklist->addChecklistItem($this->itemPrototype3); + + $this->checklistNodePrototype = new ChecklistNode(); + $this->checklistNodePrototype->root = $this->rootNode; + + $this->checklistNode = new ChecklistNode(); + } + + public function testCopyFromPrototypeInSameCamp() { + // given + $this->checklistNodePrototype->addChecklistItem($this->itemPrototype1); + $this->checklistNodePrototype->addChecklistItem($this->itemPrototype3); + + // when + $this->checklistNode->copyFromPrototype($this->checklistNodePrototype, new EntityMap($this->camp)); + + // then + $this->assertCount(2, $this->checklistNode->getChecklistItems()); + $item1 = $this->checklistNode->getChecklistItems()[0]; + $item2 = $this->checklistNode->getChecklistItems()[1]; + $this->assertEquals($this->itemPrototype1->text, $item1->text); + $this->assertEquals($this->itemPrototype3->text, $item2->text); + $this->assertEquals($this->itemPrototype1->checklist->getCamp(), $item1->checklist->getCamp()); + $this->assertEquals($this->itemPrototype3->checklist->getCamp(), $item2->checklist->getCamp()); + } + + public function testCopyFromPrototypeAcrossCampsDoesNotAddFaultyConnections() { + // given + $this->checklistNodePrototype->addChecklistItem($this->itemPrototype1); + $this->checklistNodePrototype->addChecklistItem($this->itemPrototype3); + $targetCamp = new Camp(); + + // when + $this->checklistNode->copyFromPrototype($this->checklistNodePrototype, new EntityMap($targetCamp)); + + // then + $this->assertCount(0, $this->checklistNode->getChecklistItems()); + } + + public function testCopyFromPrototypeAcrossCampsSearchesForItemOfSameName() { + // given + $this->checklistNodePrototype->addChecklistItem($this->itemPrototype1); + $this->checklistNodePrototype->addChecklistItem($this->itemPrototype3); + $targetCamp = new Camp(); + + $targetChecklist = new Checklist(); + $targetChecklist->name = 'target checklist'; + $targetCamp->addChecklist($targetChecklist); + + $targetChecklistItem = new ChecklistItem(); + $targetChecklistItem->text = 'item3'; + $targetChecklist->addChecklistItem($targetChecklistItem); + + // when + $this->checklistNode->copyFromPrototype($this->checklistNodePrototype, new EntityMap($targetCamp)); + + // then + $this->assertCount(1, $this->checklistNode->getChecklistItems()); + $item1 = $this->checklistNode->getChecklistItems()[0]; + $this->assertEquals($this->itemPrototype3->text, $item1->text); + $this->assertNotEquals($this->itemPrototype3->checklist->getCamp(), $item1->checklist->getCamp()); + } + + public function testCopyFromPrototypeAcrossCampsPrefersItemWithSameNameAndSameHierarchy() { + // given + $this->checklistNodePrototype->addChecklistItem($this->itemPrototype1); + $this->checklistNodePrototype->addChecklistItem($this->itemPrototype3); + $targetCamp = new Camp(); + + $targetChecklist = new Checklist(); + $targetChecklist->name = 'target checklist'; + $targetCamp->addChecklist($targetChecklist); + + $targetChecklistItem1 = new ChecklistItem(); + $targetChecklistItem1->text = 'item3'; + $targetChecklist->addChecklistItem($targetChecklistItem1); + $targetChecklistItem2 = new ChecklistItem(); + $targetChecklistItem2->text = 'item2'; + $targetChecklist->addChecklistItem($targetChecklistItem2); + $targetChecklistItem3 = new ChecklistItem(); + $targetChecklistItem3->text = 'item3'; + $targetChecklistItem2->addChild($targetChecklistItem3); + $targetChecklist->addChecklistItem($targetChecklistItem3); + + // when + $this->checklistNode->copyFromPrototype($this->checklistNodePrototype, new EntityMap($targetCamp)); + + // then + $this->assertCount(1, $this->checklistNode->getChecklistItems()); + $resultItem = $this->checklistNode->getChecklistItems()[0]; + $this->assertEquals($targetChecklistItem3, $resultItem); + $this->assertEquals($this->itemPrototype3->text, $resultItem->text); + $this->assertNotEquals($this->itemPrototype3->checklist->getCamp(), $resultItem->checklist->getCamp()); + $this->assertEquals($targetCamp, $resultItem->checklist->getCamp()); + } + + public function testCopyFromPrototypeAcrossCampsPrefersItemWithSameChecklistName() { + // given + $this->checklistNodePrototype->addChecklistItem($this->itemPrototype1); + $this->checklistNodePrototype->addChecklistItem($this->itemPrototype3); + $targetCamp = new Camp(); + + $targetChecklist = new Checklist(); + $targetChecklist->name = 'checklist'; + $targetCamp->addChecklist($targetChecklist); + + $targetChecklist2 = new Checklist(); + $targetChecklist2->name = 'checklist with other name'; + $targetCamp->addChecklist($targetChecklist2); + + $targetChecklistItem1 = new ChecklistItem(); + $targetChecklistItem1->text = 'item2'; + $targetChecklist->addChecklistItem($targetChecklistItem1); + $targetChecklistItem2 = new ChecklistItem(); + $targetChecklistItem2->text = 'item2'; + $targetChecklist->addChecklistItem($targetChecklistItem2); + $targetChecklistItem3 = new ChecklistItem(); + $targetChecklistItem3->text = 'item3'; + $targetChecklistItem2->addChild($targetChecklistItem3); + $targetChecklist2->addChecklistItem($targetChecklistItem3); + $targetChecklistItem4 = new ChecklistItem(); + $targetChecklistItem4->text = 'item3'; + $targetChecklistItem1->addChild($targetChecklistItem4); + $targetChecklist->addChecklistItem($targetChecklistItem4); + + // when + $this->checklistNode->copyFromPrototype($this->checklistNodePrototype, new EntityMap($targetCamp)); + + // then + $this->assertCount(1, $this->checklistNode->getChecklistItems()); + $resultItem = $this->checklistNode->getChecklistItems()[0]; + $this->assertEquals($targetChecklistItem4, $resultItem); + $this->assertEquals($this->itemPrototype3->text, $resultItem->text); + $this->assertNotEquals($this->itemPrototype3->checklist->getCamp(), $resultItem->checklist->getCamp()); + $this->assertEquals($targetCamp, $resultItem->checklist->getCamp()); + } + + public function testCopyFromPrototypeAcrossCampsReusesExistingEntityMapping() { + // given + $this->checklistNodePrototype->addChecklistItem($this->itemPrototype1); + $this->checklistNodePrototype->addChecklistItem($this->itemPrototype3); + $targetCamp = new Camp(); + + $targetChecklist = new Checklist(); + $targetChecklist->name = 'target checklist'; + $targetCamp->addChecklist($targetChecklist); + + $targetChecklistItem1 = new ChecklistItem(); + $targetChecklistItem1->text = 'item3-preferred'; + $targetChecklist->addChecklistItem($targetChecklistItem1); + $targetChecklistItem2 = new ChecklistItem(); + $targetChecklistItem2->text = 'item2'; + $targetChecklist->addChecklistItem($targetChecklistItem2); + $targetChecklistItem3 = new ChecklistItem(); + $targetChecklistItem3->text = 'item3'; + $targetChecklistItem2->addChild($targetChecklistItem3); + $targetChecklist->addChecklistItem($targetChecklistItem3); + + $entityMap = new EntityMap($targetCamp); + $entityMap->add($this->itemPrototype3, $targetChecklistItem1); + + // when + $this->checklistNode->copyFromPrototype($this->checklistNodePrototype, $entityMap); + + // then + $this->assertCount(1, $this->checklistNode->getChecklistItems()); + $item1 = $this->checklistNode->getChecklistItems()[0]; + $this->assertEquals($targetChecklistItem1->text, $item1->text); + $this->assertNotEquals($this->itemPrototype3->checklist->getCamp(), $item1->checklist->getCamp()); + } +} From 527614e2f4b2d1cd30507b2b80f85941b65c9ffe Mon Sep 17 00:00:00 2001 From: carlobeltrame Date: Sun, 25 Jan 2026 16:28:12 +0100 Subject: [PATCH 6/7] Extract max checklist item nesting depth into a constant and reuse it --- api/src/Entity/ChecklistItem.php | 7 ++++--- api/src/Entity/ContentNode/ChecklistNode.php | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/api/src/Entity/ChecklistItem.php b/api/src/Entity/ChecklistItem.php index 236c04f7de..2c429c0b59 100644 --- a/api/src/Entity/ChecklistItem.php +++ b/api/src/Entity/ChecklistItem.php @@ -84,6 +84,7 @@ #[ORM\UniqueConstraint(name: 'checklistitem_checklistid_parentid_position_unique', columns: ['checklistid', 'parentid', 'position'])] class ChecklistItem extends BaseEntity implements BelongsToCampInterface, CopyFromPrototypeInterface, HasParentInterface { public const CHECKLIST_SUBRESOURCE_URI_TEMPLATE = '/checklists/{checklistId}/checklist_items{._format}'; + public const MAX_NESTING_DEPTH = 3; /** * The Checklist this Item belongs to. @@ -100,14 +101,14 @@ class ChecklistItem extends BaseEntity implements BelongsToCampInterface, CopyFr * root of a ChecklistItem tree. For non-root ChecklistItems, the parent can be changed, as long * as the new parent is in the same checklist as the old one. * - * Nesting has maximum depth of 3 Levels (root - child - grandchild) + * Nesting has maximum depth of 3 levels (root - child - grandchild) * => CurrentNesting + SubtreeDepth < 3 */ #[AssertBelongsToSameChecklist] #[AssertNoLoop] #[Assert\Expression( - '(this.getNestingLevel() + this.getSubtreeDepth()) < 3', - 'Nesting can be a maximum of 3 levels deep.' + '(this.getNestingLevel() + this.getSubtreeDepth()) < '.self::MAX_NESTING_DEPTH, + 'Nesting can be a maximum of '.self::MAX_NESTING_DEPTH.' levels deep.' )] #[ApiProperty(example: '/checklist_items/1a2b3c4d')] #[Gedmo\SortableGroup] diff --git a/api/src/Entity/ContentNode/ChecklistNode.php b/api/src/Entity/ContentNode/ChecklistNode.php index e94b2137d2..a4a3266776 100644 --- a/api/src/Entity/ContentNode/ChecklistNode.php +++ b/api/src/Entity/ContentNode/ChecklistNode.php @@ -140,7 +140,7 @@ public function copyFromPrototype($prototype, $entityMap): void { /** @var ChecklistItem $existingParent */ $existingParent = $existingItem->getParent(); - while (null !== $parent && null !== $existingParent && $score < 10) { + while (null !== $parent && null !== $existingParent && $score <= ChecklistItem::MAX_NESTING_DEPTH) { if ($existingParent->text !== $parent->text) { return $score; } From e50735c7b1279047513a51e62fc1efda1df864f1 Mon Sep 17 00:00:00 2001 From: carlobeltrame Date: Tue, 27 Jan 2026 20:34:25 +0100 Subject: [PATCH 7/7] Also distinguish by checklist prototype id --- api/src/Entity/ContentNode/ChecklistNode.php | 6 +++ api/tests/Entity/ChecklistNodeTest.php | 45 ++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/api/src/Entity/ContentNode/ChecklistNode.php b/api/src/Entity/ContentNode/ChecklistNode.php index a4a3266776..38aab31a30 100644 --- a/api/src/Entity/ContentNode/ChecklistNode.php +++ b/api/src/Entity/ContentNode/ChecklistNode.php @@ -149,6 +149,12 @@ public function copyFromPrototype($prototype, $entityMap): void { /** @var ChecklistItem $parent */ $parent = $parent->getParent(); } + + if ($existingItem->checklist->checklistPrototypeId !== $itemPrototype->checklist->checklistPrototypeId) { + return $score; + } + ++$score; + if ($existingItem->checklist->name !== $itemPrototype->checklist->name) { return $score; } diff --git a/api/tests/Entity/ChecklistNodeTest.php b/api/tests/Entity/ChecklistNodeTest.php index 8d3f420f3b..733737d8e0 100644 --- a/api/tests/Entity/ChecklistNodeTest.php +++ b/api/tests/Entity/ChecklistNodeTest.php @@ -141,6 +141,51 @@ public function testCopyFromPrototypeAcrossCampsPrefersItemWithSameNameAndSameHi $this->assertEquals($targetCamp, $resultItem->checklist->getCamp()); } + public function testCopyFromPrototypeAcrossCampsPrefersItemWithSameChecklistPrototypeId() { + // given + $this->checklistNodePrototype->addChecklistItem($this->itemPrototype1); + $this->checklistNodePrototype->addChecklistItem($this->itemPrototype3); + $targetCamp = new Camp(); + + $targetChecklist = new Checklist(); + $targetChecklist->name = 'checklist'; + $targetCamp->addChecklist($targetChecklist); + + $targetChecklist2 = new Checklist(); + $targetChecklist2->name = 'checklist with other name'; + $targetCamp->addChecklist($targetChecklist2); + + $targetChecklistItem1 = new ChecklistItem(); + $targetChecklistItem1->text = 'item2'; + $targetChecklist->addChecklistItem($targetChecklistItem1); + $targetChecklistItem2 = new ChecklistItem(); + $targetChecklistItem2->text = 'item2'; + $targetChecklist->addChecklistItem($targetChecklistItem2); + $targetChecklistItem3 = new ChecklistItem(); + $targetChecklistItem3->text = 'item3'; + $targetChecklistItem2->addChild($targetChecklistItem3); + $targetChecklist2->addChecklistItem($targetChecklistItem3); + $targetChecklistItem4 = new ChecklistItem(); + $targetChecklistItem4->text = 'item3'; + $targetChecklistItem1->addChild($targetChecklistItem4); + $targetChecklist->addChecklistItem($targetChecklistItem4); + + $this->checklist->checklistPrototypeId = 'abc'; + $targetChecklist->checklistPrototypeId = 'abc'; + $targetChecklist2->checklistPrototypeId = 'def'; + + // when + $this->checklistNode->copyFromPrototype($this->checklistNodePrototype, new EntityMap($targetCamp)); + + // then + $this->assertCount(1, $this->checklistNode->getChecklistItems()); + $resultItem = $this->checklistNode->getChecklistItems()[0]; + $this->assertEquals($targetChecklistItem4, $resultItem); + $this->assertEquals($this->itemPrototype3->text, $resultItem->text); + $this->assertNotEquals($this->itemPrototype3->checklist->getCamp(), $resultItem->checklist->getCamp()); + $this->assertEquals($targetCamp, $resultItem->checklist->getCamp()); + } + public function testCopyFromPrototypeAcrossCampsPrefersItemWithSameChecklistName() { // given $this->checklistNodePrototype->addChecklistItem($this->itemPrototype1);