From d2bd7d44ceb686524100c2742b338ac6b77999b0 Mon Sep 17 00:00:00 2001 From: romanetar Date: Tue, 31 Mar 2026 18:36:19 +0200 Subject: [PATCH] feat: remove selection plan ref from presentation when a category is removed from a category group Signed-off-by: romanetar --- .../Presentations/PresentationCategory.php | 22 +++++++++++++ .../PresentationCategoryGroup.php | 33 ++++++++++++++++++- .../Foundation/Summit/SelectionPlan.php | 4 ++- .../Imp/PresentationCategoryGroupService.php | 23 ++++++++++++- tests/oauth2/OAuth2TrackGroupsApiTest.php | 24 +++++++++++++- 5 files changed, 102 insertions(+), 4 deletions(-) diff --git a/app/Models/Foundation/Summit/Events/Presentations/PresentationCategory.php b/app/Models/Foundation/Summit/Events/Presentations/PresentationCategory.php index 876b49257..0685bfd13 100644 --- a/app/Models/Foundation/Summit/Events/Presentations/PresentationCategory.php +++ b/app/Models/Foundation/Summit/Events/Presentations/PresentationCategory.php @@ -679,6 +679,28 @@ public function calculateSlug(){ return $this; } + /** + * @param int[] $selection_plan_ids + * @return Presentation[] + */ + public function getPresentationsBySelectionPlanIds(array $selection_plan_ids): array + { + if (empty($selection_plan_ids)) return []; + + $query = <<getEM() + ->createQuery($query) + ->setParameter('track', $this) + ->setParameter('selection_plan_ids', $selection_plan_ids) + ->getResult(); + } + /** * @return SummitEvent[] */ diff --git a/app/Models/Foundation/Summit/Events/Presentations/PresentationCategoryGroup.php b/app/Models/Foundation/Summit/Events/Presentations/PresentationCategoryGroup.php index 30eb1c6d2..ec0450bdf 100644 --- a/app/Models/Foundation/Summit/Events/Presentations/PresentationCategoryGroup.php +++ b/app/Models/Foundation/Summit/Events/Presentations/PresentationCategoryGroup.php @@ -13,6 +13,7 @@ **/ use App\Models\Foundation\Summit\ScheduleEntity; +use App\Models\Foundation\Summit\SelectionPlan; use Doctrine\Common\Collections\Criteria; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Log; @@ -70,6 +71,13 @@ class PresentationCategoryGroup extends SilverstripeBaseModel protected $max_attendee_votes; + /** + * inverse side + * @var SelectionPlan[] + */ + #[ORM\ManyToMany(targetEntity: \App\Models\Foundation\Summit\SelectionPlan::class, mappedBy: 'category_groups', fetch: 'EXTRA_LAZY')] + protected $selection_plans; + public function __construct() { parent::__construct(); @@ -77,6 +85,29 @@ public function __construct() $this->end_attendee_voting_period_date = null; $this->max_attendee_votes = 0; $this->categories = new ArrayCollection; + $this->selection_plans = new ArrayCollection; + } + + /** + * @return int[] + */ + public function getSelectionPlanIds(): array + { + return $this->selection_plans->map(function ($sp) { + return $sp->getId(); + })->toArray(); + } + + public function addSelectionPlan(SelectionPlan $selection_plan): void + { + if ($this->selection_plans->contains($selection_plan)) return; + $this->selection_plans->add($selection_plan); + } + + public function removeSelectionPlan(SelectionPlan $selection_plan): void + { + if (!$this->selection_plans->contains($selection_plan)) return; + $this->selection_plans->removeElement($selection_plan); } @@ -382,4 +413,4 @@ public function clearAttendeeVotingPeriod():void{ } use ScheduleEntity; -} \ No newline at end of file +} diff --git a/app/Models/Foundation/Summit/SelectionPlan.php b/app/Models/Foundation/Summit/SelectionPlan.php index c602dc9e0..0e00d48ba 100644 --- a/app/Models/Foundation/Summit/SelectionPlan.php +++ b/app/Models/Foundation/Summit/SelectionPlan.php @@ -169,7 +169,7 @@ class SelectionPlan extends SilverstripeBaseModel #[ORM\JoinTable(name: 'SelectionPlan_CategoryGroups')] #[ORM\JoinColumn(name: 'SelectionPlanID', referencedColumnName: 'ID')] #[ORM\InverseJoinColumn(name: 'PresentationCategoryGroupID', referencedColumnName: 'ID')] - #[ORM\ManyToMany(targetEntity: \models\summit\PresentationCategoryGroup::class, fetch: 'EXTRA_LAZY')] + #[ORM\ManyToMany(targetEntity: \models\summit\PresentationCategoryGroup::class, inversedBy: 'selection_plans', fetch: 'EXTRA_LAZY')] private $category_groups; /** @@ -596,6 +596,7 @@ public function addTrackGroup(PresentationCategoryGroup $track_group) { if ($this->category_groups->contains($track_group)) return; $this->category_groups->add($track_group); + $track_group->addSelectionPlan($this); } /** @@ -605,6 +606,7 @@ public function removeTrackGroup(PresentationCategoryGroup $track_group) { if (!$this->category_groups->contains($track_group)) return; $this->category_groups->removeElement($track_group); + $track_group->removeSelectionPlan($this); } public function addEventType(SummitEventType $eventType) diff --git a/app/Services/Model/Imp/PresentationCategoryGroupService.php b/app/Services/Model/Imp/PresentationCategoryGroupService.php index ad569cf1d..99b67ab17 100644 --- a/app/Services/Model/Imp/PresentationCategoryGroupService.php +++ b/app/Services/Model/Imp/PresentationCategoryGroupService.php @@ -252,6 +252,27 @@ public function disassociateTrack2TrackGroup(Summit $summit, $track_group_id, $t ); } + $presentations = $track->getPresentationsBySelectionPlanIds($track_group->getSelectionPlanIds()); + + // Only clear the selection plan if no other category group in that plan + // still contains $track. A selection plan can span multiple groups, so + // removing $track from $track_group does not necessarily invalidate the + // plan for presentations under $track — another group may still cover it. + // We exclude $track_group from the check because removeCategory has not + // been called yet at this point. + foreach ($presentations as $presentation) { + $selection_plan = $presentation->getSelectionPlan(); + $track_reachable_via_another_group = $selection_plan->getCategoryGroups()->exists( + function ($key, $group) use ($track, $track_group) { + return $group->getId() !== $track_group->getId() + && $group->hasCategory($track->getId()); + } + ); + if (!$track_reachable_via_another_group) { + $presentation->clearSelectionPlan(); + } + } + $track_group->removeCategory($track); }); @@ -380,4 +401,4 @@ public function disassociateAllowedGroup2TrackGroup(Summit $summit, $track_group }); } -} \ No newline at end of file +} diff --git a/tests/oauth2/OAuth2TrackGroupsApiTest.php b/tests/oauth2/OAuth2TrackGroupsApiTest.php index b53bd009a..ed2277797 100644 --- a/tests/oauth2/OAuth2TrackGroupsApiTest.php +++ b/tests/oauth2/OAuth2TrackGroupsApiTest.php @@ -1,4 +1,8 @@ assertResponseStatus(201); + // verify presentations of defaultTrack have a selection plan before disassociation + $presentations = self::$defaultTrack->getPresentationsBySelectionPlanIds( + self::$defaultTrackGroup->getSelectionPlanIds() + ); + $this->assertNotEmpty($presentations); + foreach ($presentations as $presentation) { + $this->assertTrue($presentation->getSelectionPlanId() > 0); + } + $presentation_ids = array_map(fn($p) => $p->getId(), $presentations); + // now disassociate $response = $this->action( "DELETE", @@ -353,6 +367,14 @@ public function testDisassociateTrack2TrackGroup(){ ); $this->assertResponseStatus(204); + + // reset EM (closed after the API transaction) and re-fetch presentations to verify selection plan was cleared + self::$em = Registry::resetManager(SilverstripeBaseModel::EntityManager); + foreach ($presentation_ids as $id) { + $presentation = self::$em->find(Presentation::class, $id); + $this->assertNotNull($presentation); + $this->assertEquals(0, $presentation->getSelectionPlanId()); + } } public function testAddPrivateTrackGroup(){ @@ -480,4 +502,4 @@ public function testDeleteExistentTrackGroup(){ $content = $response->getContent(); $this->assertResponseStatus(204); } -} \ No newline at end of file +}