From bdd9a733b14e1b1ca8a4342e00ba067c71721089 Mon Sep 17 00:00:00 2001 From: romanetar Date: Mon, 22 Jun 2026 15:09:12 +0200 Subject: [PATCH 1/4] feat(speakers): add has_pending_presentations filter for speakers and submitters Signed-off-by: romanetar --- .../OAuth2SummitSpeakersApiController.php | 8 +- .../OAuth2SummitSubmittersApiController.php | 10 +- .../ProcessSubmittersEmailRequestJob.php | 1 + .../Summit/DoctrineMemberRepository.php | 55 +++++++++ .../Summit/DoctrineSpeakerRepository.php | 101 ++++++++++++++++ app/Services/Model/ISpeakerFilterFields.php | 2 + app/Services/Model/Imp/SubmitterService.php | 3 +- tests/oauth2/OAuth2SummitSpeakersApiTest.php | 110 +++++++++++++++++- .../oauth2/OAuth2SummitSubmittersApiTest.php | 53 ++++++++- 9 files changed, 335 insertions(+), 8 deletions(-) diff --git a/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSpeakersApiController.php b/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSpeakersApiController.php index 96717d694..c27c94fc3 100644 --- a/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSpeakersApiController.php +++ b/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSpeakersApiController.php @@ -252,7 +252,7 @@ public function __construct ), new OA\Parameter( name: 'filter', - description: 'Filter by id, not_id, first_name, last_name, email, full_name, member_id, member_user_external_id, has_accepted_presentations, has_alternate_presentations, has_rejected_presentations, has_published_presentations, presentations_track_id, presentations_track_group_id, presentations_selection_plan_id, presentations_type_id, presentations_title, presentations_abstract, presentations_submitter_full_name, presentations_submitter_email, has_media_upload_with_type, has_not_media_upload_with_type. Operands supported: == (equal), @@ (contains), =@ (starts with).', + description: 'Filter by id, not_id, first_name, last_name, email, full_name, member_id, member_user_external_id, has_pending_presentations, has_accepted_presentations, has_alternate_presentations, has_rejected_presentations, has_published_presentations, presentations_track_id, presentations_track_group_id, presentations_selection_plan_id, presentations_type_id, presentations_title, presentations_abstract, presentations_submitter_full_name, presentations_submitter_email, has_media_upload_with_type, has_not_media_upload_with_type. Operands supported: == (equal), @@ (contains), =@ (starts with).', in: 'query', required: false, schema: new OA\Schema(type: 'string') @@ -368,7 +368,7 @@ function ($page, $per_page, $filter, $order, $applyExtraFilters) use ($summit) { ), new OA\Parameter( name: 'filter', - description: 'Filter query (supports multiple operators). Filterable fields: id, not_id, first_name, last_name, email, full_name, member_id, member_user_external_id, has_accepted_presentations, has_alternate_presentations, has_rejected_presentations, has_published_presentations, presentations_track_id, presentations_track_group_id, presentations_selection_plan_id, presentations_type_id, presentations_title, presentations_abstract, presentations_submitter_full_name, presentations_submitter_email, has_media_upload_with_type, has_not_media_upload_with_type.', + description: 'Filter query (supports multiple operators). Filterable fields: id, not_id, first_name, last_name, email, full_name, member_id, member_user_external_id, has_pending_presentations, has_accepted_presentations, has_alternate_presentations, has_rejected_presentations, has_published_presentations, presentations_track_id, presentations_track_group_id, presentations_selection_plan_id, presentations_type_id, presentations_title, presentations_abstract, presentations_submitter_full_name, presentations_submitter_email, has_media_upload_with_type, has_not_media_upload_with_type.', in: 'query', required: false, schema: new OA\Schema(type: 'string') @@ -442,7 +442,7 @@ public function getSpeakersActivitiesCount($summit_id) ), new OA\Parameter( name: 'filter', - description: 'Filter by id, not_id, first_name, last_name, email, full_name, member_id, member_user_external_id, has_accepted_presentations, has_alternate_presentations, has_rejected_presentations, has_published_presentations, presentations_track_id, presentations_track_group_id, presentations_selection_plan_id, presentations_type_id, presentations_title, presentations_abstract, presentations_submitter_full_name, presentations_submitter_email, has_media_upload_with_type, has_not_media_upload_with_type. Operands supported: == (equal), @@ (contains), =@ (starts with).', + description: 'Filter by id, not_id, first_name, last_name, email, full_name, member_id, member_user_external_id, has_pending_presentations, has_accepted_presentations, has_alternate_presentations, has_rejected_presentations, has_published_presentations, presentations_track_id, presentations_track_group_id, presentations_selection_plan_id, presentations_type_id, presentations_title, presentations_abstract, presentations_submitter_full_name, presentations_submitter_email, has_media_upload_with_type, has_not_media_upload_with_type. Operands supported: == (equal), @@ (contains), =@ (starts with).', in: 'query', required: false, schema: new OA\Schema(type: 'string') @@ -3054,7 +3054,7 @@ public function deleteSpeakerBigPhoto($speaker_id) ), new OA\Parameter( name: 'filter', - description: 'Filter speakers by id, not_id, first_name, last_name, email, full_name, member_id, member_user_external_id, has_accepted_presentations, has_alternate_presentations, has_rejected_presentations, has_published_presentations, presentations_track_id, presentations_track_group_id, presentations_selection_plan_id, presentations_type_id, presentations_title, presentations_abstract, presentations_submitter_full_name, presentations_submitter_email, has_media_upload_with_type, has_not_media_upload_with_type', + description: 'Filter speakers by id, not_id, first_name, last_name, email, full_name, member_id, member_user_external_id, has_pending_presentations, has_accepted_presentations, has_alternate_presentations, has_rejected_presentations, has_published_presentations, presentations_track_id, presentations_track_group_id, presentations_selection_plan_id, presentations_type_id, presentations_title, presentations_abstract, presentations_submitter_full_name, presentations_submitter_email, has_media_upload_with_type, has_not_media_upload_with_type', in: 'query', required: false, schema: new OA\Schema(type: 'string') diff --git a/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSubmittersApiController.php b/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSubmittersApiController.php index c567283ea..f1ccb04f9 100644 --- a/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSubmittersApiController.php +++ b/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSubmittersApiController.php @@ -152,6 +152,7 @@ function () { 'full_name' => ['=@', '@@', '=='], 'member_id' => ['=='], 'member_user_external_id' => ['=='], + 'has_pending_presentations' => ['=='], 'has_accepted_presentations' => ['=='], 'has_alternate_presentations' => ['=='], 'has_rejected_presentations' => ['=='], @@ -179,6 +180,7 @@ function () { 'full_name' => 'sometimes|string', 'member_id' => 'sometimes|integer', 'member_user_external_id' => 'sometimes|integer', + 'has_pending_presentations' => 'sometimes|string|in:true,false', 'has_accepted_presentations' => 'sometimes|string|in:true,false', 'has_alternate_presentations' => 'sometimes|string|in:true,false', 'has_rejected_presentations' => 'sometimes|string|in:true,false', @@ -300,6 +302,7 @@ function () { 'full_name' => ['=@', '@@', '=='], 'member_id' => ['=='], 'member_user_external_id' => ['=='], + 'has_pending_presentations' => ['=='], 'has_accepted_presentations' => ['=='], 'has_alternate_presentations' => ['=='], 'has_rejected_presentations' => ['=='], @@ -327,6 +330,7 @@ function () { 'full_name' => 'sometimes|string', 'member_id' => 'sometimes|integer', 'member_user_external_id' => 'sometimes|integer', + 'has_pending_presentations' => 'sometimes|string|in:true,false', 'has_accepted_presentations' => 'sometimes|string|in:true,false', 'has_alternate_presentations' => 'sometimes|string|in:true,false', 'has_rejected_presentations' => 'sometimes|string|in:true,false', @@ -455,6 +459,7 @@ public function send($summit_id) 'full_name' => ['=@', '@@', '=='], 'member_id' => ['=='], 'member_user_external_id' => ['=='], + 'has_pending_presentations' => ['=='], 'has_accepted_presentations' => ['=='], 'has_alternate_presentations' => ['=='], 'has_rejected_presentations' => ['=='], @@ -485,6 +490,7 @@ public function send($summit_id) 'full_name' => 'sometimes|string', 'member_id' => 'sometimes|integer', 'member_user_external_id' => 'sometimes|integer', + 'has_pending_presentations' => 'sometimes|string|in:true,false', 'has_accepted_presentations' => 'sometimes|string|in:true,false', 'has_alternate_presentations' => 'sometimes|string|in:true,false', 'has_rejected_presentations' => 'sometimes|string|in:true,false', @@ -529,7 +535,7 @@ public function send($summit_id) name: "filter", in: "query", required: false, - description: "Filter query (supports multiple operators). Filterable fields: id, not_id, first_name, last_name, email, full_name, member_id, member_user_external_id, has_accepted_presentations, has_alternate_presentations, has_rejected_presentations, has_published_presentations, presentations_track_id, presentations_selection_plan_id, presentations_type_id, presentations_title, presentations_abstract, presentations_submitter_full_name, presentations_submitter_email, is_speaker, has_media_upload_with_type, has_not_media_upload_with_type.", + description: "Filter query (supports multiple operators). Filterable fields: id, not_id, first_name, last_name, email, full_name, member_id, member_user_external_id, has_pending_presentations, has_accepted_presentations, has_alternate_presentations, has_rejected_presentations, has_published_presentations, presentations_track_id, presentations_selection_plan_id, presentations_type_id, presentations_title, presentations_abstract, presentations_submitter_full_name, presentations_submitter_email, is_speaker, has_media_upload_with_type, has_not_media_upload_with_type.", schema: new OA\Schema(type: "string", example: "has_accepted_presentations==true") ), ], @@ -567,6 +573,7 @@ public function getSubmittersActivitiesCount($summit_id) 'full_name' => ['=@', '@@', '=='], 'member_id' => ['=='], 'member_user_external_id' => ['=='], + 'has_pending_presentations' => ['=='], 'has_accepted_presentations' => ['=='], 'has_alternate_presentations' => ['=='], 'has_rejected_presentations' => ['=='], @@ -595,6 +602,7 @@ public function getSubmittersActivitiesCount($summit_id) 'full_name' => 'sometimes|string', 'member_id' => 'sometimes|integer', 'member_user_external_id' => 'sometimes|integer', + 'has_pending_presentations' => 'sometimes|string|in:true,false', 'has_accepted_presentations' => 'sometimes|string|in:true,false', 'has_alternate_presentations' => 'sometimes|string|in:true,false', 'has_rejected_presentations' => 'sometimes|string|in:true,false', diff --git a/app/Jobs/Emails/PresentationSubmissions/ProcessSubmittersEmailRequestJob.php b/app/Jobs/Emails/PresentationSubmissions/ProcessSubmittersEmailRequestJob.php index 5d043feea..703bdad05 100644 --- a/app/Jobs/Emails/PresentationSubmissions/ProcessSubmittersEmailRequestJob.php +++ b/app/Jobs/Emails/PresentationSubmissions/ProcessSubmittersEmailRequestJob.php @@ -89,6 +89,7 @@ public function handle 'full_name' => ['=@', '@@', '=='], 'member_id' => ['=='], 'member_user_external_id' => ['=='], + 'has_pending_presentations' => ['=='], 'has_accepted_presentations' => ['=='], 'has_alternate_presentations' => ['=='], 'has_rejected_presentations' => ['=='], diff --git a/app/Repositories/Summit/DoctrineMemberRepository.php b/app/Repositories/Summit/DoctrineMemberRepository.php index e3fa54246..11728e6bc 100644 --- a/app/Repositories/Summit/DoctrineMemberRepository.php +++ b/app/Repositories/Summit/DoctrineMemberRepository.php @@ -240,6 +240,61 @@ protected function getFilterMappings() __p10_1.summit = :summit AND LOWER(__c10.email) :operator LOWER(:value) )" ), + 'has_pending_presentations' => + new DoctrineSwitchFilterMapping([ + 'true' => new DoctrineCaseFilterMapping( + 'true', + sprintf('EXISTS ( + SELECT __p41.id FROM models\summit\Presentation __p41 + JOIN __p41.created_by __c41 WITH __c41 = e.id + JOIN __p41.category __cat41 + JOIN __p41.type __t41 + LEFT JOIN __p41.selection_plan __sel_plan41 + LEFT JOIN models\summit\PresentationMediaUpload __pm41 WITH __pm41.presentation = __p41 + LEFT JOIN __pm41.media_upload_type __mut41 + WHERE + __p41.summit = :summit + AND __p41.published = 0 '. + (!empty($extraSelectionStatusFilter)? sprintf($extraSelectionStatusFilter, '41'): ' '). + ' AND NOT EXISTS ( + SELECT ___sp41.id + FROM models\summit\SummitSelectedPresentation ___sp41 + JOIN ___sp41.presentation ___p41 + JOIN ___sp41.list ___spl41 WITH ___spl41.list_type = \'%1$s\' AND ___spl41.list_class = \'%2$s\' + WHERE ___p41.id = __p41.id + ))', + SummitSelectedPresentationList::Group, + SummitSelectedPresentationList::Session + ) + ), + 'false' => new DoctrineCaseFilterMapping( + 'false', + sprintf(' + NOT EXISTS ( + SELECT __p41.id FROM models\summit\Presentation __p41 + JOIN __p41.created_by __c41 WITH __c41 = e.id + JOIN __p41.category __cat41 + JOIN __p41.type __t41 + LEFT JOIN __p41.selection_plan __sel_plan41 + LEFT JOIN models\summit\PresentationMediaUpload __pm41 WITH __pm41.presentation = __p41 + LEFT JOIN __pm41.media_upload_type __mut41 + WHERE + __p41.summit = :summit + AND __p41.published = 0 '. + (!empty($extraSelectionStatusFilter)? sprintf($extraSelectionStatusFilter, '41'): ' '). + ' AND NOT EXISTS ( + SELECT ___sp41.id + FROM models\summit\SummitSelectedPresentation ___sp41 + JOIN ___sp41.presentation ___p41 + JOIN ___sp41.list ___spl41 WITH ___spl41.list_type = \'%1$s\' AND ___spl41.list_class = \'%2$s\' + WHERE ___p41.id = __p41.id + ))', + SummitSelectedPresentationList::Group, + SummitSelectedPresentationList::Session + ) + ), + ] + ), 'has_accepted_presentations' => new DoctrineSwitchFilterMapping([ 'true' => new DoctrineCaseFilterMapping( diff --git a/app/Repositories/Summit/DoctrineSpeakerRepository.php b/app/Repositories/Summit/DoctrineSpeakerRepository.php index 670a6f179..6ada22339 100644 --- a/app/Repositories/Summit/DoctrineSpeakerRepository.php +++ b/app/Repositories/Summit/DoctrineSpeakerRepository.php @@ -252,6 +252,107 @@ protected function getFilterMappings() WHERE __p10_2.summit = :summit AND LOWER(__cb10_2.email) :operator LOWER(:value) )"), + 'has_pending_presentations' => + new DoctrineSwitchFilterMapping([ + 'true' => new DoctrineCaseFilterMapping( + 'true', + sprintf('EXISTS ( + SELECT __p41.id FROM models\summit\Presentation __p41 + JOIN __p41.speakers __spk41 WITH __spk41.speaker = e.id + JOIN __p41.category __cat41 + JOIN __p41.type __t41 + LEFT JOIN __p41.selection_plan __sel_plan41 + LEFT JOIN models\summit\PresentationMediaUpload __pm41 WITH __pm41.presentation = __p41 + LEFT JOIN __pm41.media_upload_type __mut41 + WHERE + __p41.summit = :summit + AND __p41.published = 0'. + (!empty($extraSelectionStatusFilter)? sprintf($extraSelectionStatusFilter, '41'): ' '). + 'AND NOT EXISTS ( + SELECT ___sp41.id + FROM models\summit\SummitSelectedPresentation ___sp41 + JOIN ___sp41.presentation ___p41 + JOIN ___sp41.list ___spl41 WITH ___spl41.list_type = \'%1$s\' AND ___spl41.list_class = \'%2$s\' + WHERE ___p41.id = __p41.id + ))', + SummitSelectedPresentationList::Group, + SummitSelectedPresentationList::Session + ). + ' OR '. + sprintf('EXISTS ( + SELECT __p42.id FROM models\summit\Presentation __p42 + JOIN __p42.moderator __md42 WITH __md42.id = e.id + JOIN __p42.category __cat42 + JOIN __p42.type __t42 + LEFT JOIN __p42.selection_plan __sel_plan42 + LEFT JOIN models\summit\PresentationMediaUpload __pm42 WITH __pm42.presentation = __p42 + LEFT JOIN __pm42.media_upload_type __mut42 + WHERE + __p42.summit = :summit + AND __p42.published = 0'. + (!empty($extraSelectionStatusFilter)? sprintf($extraSelectionStatusFilter, '42'): ' '). + 'AND NOT EXISTS ( + SELECT ___sp42.id + FROM models\summit\SummitSelectedPresentation ___sp42 + JOIN ___sp42.presentation ___p42 + JOIN ___sp42.list ___spl42 WITH ___spl42.list_type = \'%1$s\' AND ___spl42.list_class = \'%2$s\' + WHERE ___p42.id = __p42.id + ))', + SummitSelectedPresentationList::Group, + SummitSelectedPresentationList::Session + ) + ), + 'false' => new DoctrineCaseFilterMapping( + 'false', + sprintf(' + NOT EXISTS ( + SELECT __p41.id FROM models\summit\Presentation __p41 + JOIN __p41.speakers __spk41 WITH __spk41.speaker = e.id + JOIN __p41.category __cat41 + JOIN __p41.type __t41 + LEFT JOIN __p41.selection_plan __sel_plan41 + LEFT JOIN models\summit\PresentationMediaUpload __pm41 WITH __pm41.presentation = __p41 + LEFT JOIN __pm41.media_upload_type __mut41 + WHERE + __p41.summit = :summit + AND __p41.published = 0'. + (!empty($extraSelectionStatusFilter)? sprintf($extraSelectionStatusFilter, '41'): ' '). + 'AND NOT EXISTS ( + SELECT ___sp41.id + FROM models\summit\SummitSelectedPresentation ___sp41 + JOIN ___sp41.presentation ___p41 + JOIN ___sp41.list ___spl41 WITH ___spl41.list_type = \'%1$s\' AND ___spl41.list_class = \'%2$s\' + WHERE ___p41.id = __p41.id + ))', + SummitSelectedPresentationList::Group, + SummitSelectedPresentationList::Session + ). + ' AND '. + sprintf('NOT EXISTS ( + SELECT __p42.id FROM models\summit\Presentation __p42 + JOIN __p42.moderator __md42 WITH __md42.id = e.id + JOIN __p42.category __cat42 + JOIN __p42.type __t42 + LEFT JOIN __p42.selection_plan __sel_plan42 + LEFT JOIN models\summit\PresentationMediaUpload __pm42 WITH __pm42.presentation = __p42 + LEFT JOIN __pm42.media_upload_type __mut42 + WHERE + __p42.summit = :summit + AND __p42.published = 0'. + (!empty($extraSelectionStatusFilter)? sprintf($extraSelectionStatusFilter, '42'): ' '). + 'AND NOT EXISTS ( + SELECT ___sp42.id + FROM models\summit\SummitSelectedPresentation ___sp42 + JOIN ___sp42.presentation ___p42 + JOIN ___sp42.list ___spl42 WITH ___spl42.list_type = \'%1$s\' AND ___spl42.list_class = \'%2$s\' + WHERE ___p42.id = __p42.id + ))', + SummitSelectedPresentationList::Group, + SummitSelectedPresentationList::Session + ) + ), + ] + ), 'has_accepted_presentations' => new DoctrineSwitchFilterMapping([ 'true' => new DoctrineCaseFilterMapping( diff --git a/app/Services/Model/ISpeakerFilterFields.php b/app/Services/Model/ISpeakerFilterFields.php index 8fb445bc4..0d5c54c3f 100644 --- a/app/Services/Model/ISpeakerFilterFields.php +++ b/app/Services/Model/ISpeakerFilterFields.php @@ -33,6 +33,7 @@ interface ISpeakerFilterFields 'full_name' => ['=@', '@@', '=='], 'member_id' => ['=='], 'member_user_external_id' => ['=='], + 'has_pending_presentations' => ['=='], 'has_accepted_presentations' => ['=='], 'has_alternate_presentations' => ['=='], 'has_rejected_presentations' => ['=='], @@ -58,6 +59,7 @@ interface ISpeakerFilterFields 'full_name' => 'sometimes|string', 'member_id' => 'sometimes|integer', 'member_user_external_id' => 'sometimes|integer', + 'has_pending_presentations' => 'sometimes|string|in:true,false', 'has_accepted_presentations' => 'sometimes|string|in:true,false', 'has_alternate_presentations' => 'sometimes|string|in:true,false', 'has_rejected_presentations' => 'sometimes|string|in:true,false', diff --git a/app/Services/Model/Imp/SubmitterService.php b/app/Services/Model/Imp/SubmitterService.php index 2b4627f87..0debb8e18 100644 --- a/app/Services/Model/Imp/SubmitterService.php +++ b/app/Services/Model/Imp/SubmitterService.php @@ -143,6 +143,7 @@ function 'full_name' => ['=@', '@@', '=='], 'member_id' => ['=='], 'member_user_external_id' => ['=='], + 'has_pending_presentations' => ['=='], 'has_accepted_presentations' => ['=='], 'has_alternate_presentations' => ['=='], 'has_rejected_presentations' => ['=='], @@ -184,4 +185,4 @@ function ($summit, $outcome_email_recipient, $report) { }, $filter); } -} \ No newline at end of file +} diff --git a/tests/oauth2/OAuth2SummitSpeakersApiTest.php b/tests/oauth2/OAuth2SummitSpeakersApiTest.php index 05e5b5ff8..909a38671 100644 --- a/tests/oauth2/OAuth2SummitSpeakersApiTest.php +++ b/tests/oauth2/OAuth2SummitSpeakersApiTest.php @@ -733,6 +733,62 @@ public function testGetCurrentSummitSpeakersWithRejectedPresentations() $this->assertTrue(!is_null($speakers)); } + public function testGetCurrentSummitSpeakersWithPendingPresentations() + { + $speaker = new PresentationSpeaker(); + $speaker->setFirstName("Pending"); + $speaker->setLastName("Speaker"); + self::$em->persist($speaker); + + $start = new \DateTime('now', new \DateTimeZone('UTC')); + $end = (clone $start)->add(new \DateInterval('PT2H')); + + $pres = new Presentation(); + self::$summit->addEvent($pres); + $pres->setTitle("Pending Test Presentation"); + $pres->setAbstract("Abstract"); + $pres->setCategory(self::$defaultTrack); + $pres->setType(self::$defaultPresentationType); + $pres->setProgress(Presentation::PHASE_COMPLETE); + $pres->setStatus(Presentation::STATUS_RECEIVED); + $pres->setStartDate($start); + $pres->setEndDate($end); + $pres->addSpeaker($speaker); + // Deliberately NOT published and NOT added to any SummitSelectedPresentation group list + self::$em->flush(); + + $params = [ + 'id' => self::$summit->getId(), + 'page' => 1, + 'per_page' => 10, + 'filter' => [ + 'has_pending_presentations==true', + ], + 'order' => '+id' + ]; + + $headers = [ + "HTTP_Authorization" => " Bearer " . $this->access_token, + "CONTENT_TYPE" => "application/json" + ]; + + $response = $this->action( + "GET", + "OAuth2SummitSpeakersApiController@getSpeakers", + $params, + [], + [], + [], + $headers + ); + + $content = $response->getContent(); + $this->assertResponseStatus(200); + $speakers = json_decode($content); + $this->assertTrue(!is_null($speakers)); + $this->assertTrue(count($speakers->data) > 0); + } + public function testGetCurrentSummitSpeakersFilteredByMemberExternalUserID() { $params = [ @@ -2575,7 +2631,7 @@ public function testGetCurrentSummitSpeakersActivitiesCountWithAcceptedPresentat { // Get the filtered baseline before seeding so the assertion is exact. // A broken filter that returns all results would produce a count far - // greater than baseline + 1, causing the assertEquals to fail. + // greater than baseline + 1, causing the assertEquals to fail. $baseline = EntityManager::getRepository(PresentationSpeaker::class) ->getUniqueActivitiesCountBySummit( self::$summit, @@ -2629,6 +2685,58 @@ public function testGetCurrentSummitSpeakersActivitiesCountWithAcceptedPresentat $this->assertEquals($baseline + 1, $data->count); } + public function testGetCurrentSummitSpeakersActivitiesCountWithPendingPresentations() + { + $baseline = EntityManager::getRepository(PresentationSpeaker::class) + ->getUniqueActivitiesCountBySummit( + self::$summit, + FilterParser::parse( + ['filter' => 'has_pending_presentations==true'], + ['has_pending_presentations' => ['==']] + ) + ); + + $speaker = new PresentationSpeaker(); + $speaker->setFirstName("Pending"); + $speaker->setLastName("Test"); + self::$em->persist($speaker); + + $start = new \DateTime('now', new \DateTimeZone('UTC')); + $end = (clone $start)->add(new \DateInterval('PT2H')); + + $pres = new Presentation(); + self::$summit->addEvent($pres); + $pres->setTitle("Pending Test Presentation"); + $pres->setAbstract("Abstract"); + $pres->setCategory(self::$defaultTrack); + $pres->setType(self::$defaultPresentationType); + $pres->setProgress(Presentation::PHASE_COMPLETE); + $pres->setStatus(Presentation::STATUS_RECEIVED); + $pres->setStartDate($start); + $pres->setEndDate($end); + $pres->addSpeaker($speaker); + // Deliberately NOT published and NOT added to any SummitSelectedPresentation group list + self::$em->flush(); + + $headers = [ + "HTTP_Authorization" => " Bearer " . $this->access_token, + "CONTENT_TYPE" => "application/json" + ]; + + $response = $this->action( + "GET", + "OAuth2SummitSpeakersApiController@getSpeakersActivitiesCount", + ['id' => self::$summit->getId(), 'filter' => ['has_pending_presentations==true']], + [], [], [], $headers + ); + + $this->assertResponseStatus(200); + $data = json_decode($response->getContent()); + $this->assertNotNull($data); + $this->assertTrue(isset($data->count)); + $this->assertEquals($baseline + 1, $data->count); + } + private function resetEmIfNeeded(): void { if (!self::$em->isOpen()) { diff --git a/tests/oauth2/OAuth2SummitSubmittersApiTest.php b/tests/oauth2/OAuth2SummitSubmittersApiTest.php index a7dc6a98e..0820a5e1b 100644 --- a/tests/oauth2/OAuth2SummitSubmittersApiTest.php +++ b/tests/oauth2/OAuth2SummitSubmittersApiTest.php @@ -134,6 +134,57 @@ public function testGetCurrentSummitSubmittersWithAcceptedPresentations() $this->assertTrue(!is_null($submitters)); } + public function testGetCurrentSummitSubmittersWithPendingPresentations() + { + $start = new \DateTime('now', new \DateTimeZone('UTC')); + $end = (clone $start)->add(new \DateInterval('PT2H')); + + $pres = new Presentation(); + self::$summit->addEvent($pres); + $pres->setTitle("Pending Test Presentation"); + $pres->setAbstract("Abstract"); + $pres->setCategory(self::$defaultTrack); + $pres->setType(self::$defaultPresentationType); + $pres->setProgress(Presentation::PHASE_COMPLETE); + $pres->setStatus(Presentation::STATUS_RECEIVED); + $pres->setStartDate($start); + $pres->setEndDate($end); + $pres->setCreatedBy(self::$defaultMember); + // Deliberately NOT published and NOT added to any SummitSelectedPresentation group list + self::$em->flush(); + + $params = [ + 'id' => self::$summit->getId(), + 'page' => 1, + 'per_page' => 10, + 'filter' => [ + 'has_pending_presentations==true', + ], + 'order' => '+id' + ]; + + $headers = [ + "HTTP_Authorization" => " Bearer " . $this->access_token, + "CONTENT_TYPE" => "application/json" + ]; + + $response = $this->action( + "GET", + "OAuth2SummitSubmittersApiController@getAllBySummit", + $params, + [], + [], + [], + $headers + ); + + $content = $response->getContent(); + $this->assertResponseStatus(200); + $submitters = json_decode($content); + $this->assertTrue(!is_null($submitters)); + $this->assertTrue(count($submitters->data) > 0); + } + public function testExportCurrentSummitSubmittersWhoAreSpeakers() { $params = [ @@ -444,4 +495,4 @@ public function testGetCurrentSummitSubmittersActivitiesCountWithPublishedPresen $this->assertEquals(1, $data->count, 'exactly one published presentation was seeded; count must be 1'); } -} \ No newline at end of file +} From de860aedff06ad778827b66124ceff85c1d51ad7 Mon Sep 17 00:00:00 2001 From: romanetar Date: Mon, 7 Sep 2026 16:26:09 +0200 Subject: [PATCH 2/4] fix(speakers): require an incomplete submission for has_pending_presentations The filter only checked published=0 and the absence of a selection-list entry, so a speaker/submitter whose presentation was already complete/received (PHASE_COMPLETE/STATUS_RECEIVED) still matched pending==true whenever it hadn't been selected yet by a track chair. During an open CFP this matched nearly every submission, which would have sent "finish your submission" reminders to people who already had. Adds a check on Presentation.progress/status so the filter now also requires the submission itself to be unfinished, in both DoctrineSpeakerRepository and the equivalent mapping in DoctrineMemberRepository. --- .../Summit/DoctrineMemberRepository.php | 7 +- .../Summit/DoctrineSpeakerRepository.php | 13 ++-- tests/oauth2/OAuth2SummitSpeakersApiTest.php | 72 +++++++++++++++++-- .../oauth2/OAuth2SummitSubmittersApiTest.php | 64 ++++++++++++++++- 4 files changed, 141 insertions(+), 15 deletions(-) diff --git a/app/Repositories/Summit/DoctrineMemberRepository.php b/app/Repositories/Summit/DoctrineMemberRepository.php index 11728e6bc..b5846ec8c 100644 --- a/app/Repositories/Summit/DoctrineMemberRepository.php +++ b/app/Repositories/Summit/DoctrineMemberRepository.php @@ -23,6 +23,7 @@ use models\main\IMemberRepository; use models\main\Member; use App\Repositories\SilverStripeDoctrineRepository; +use models\summit\Presentation; use models\summit\Summit; use models\summit\SummitSelectedPresentation; use models\summit\SummitSelectedPresentationList; @@ -254,7 +255,8 @@ protected function getFilterMappings() LEFT JOIN __pm41.media_upload_type __mut41 WHERE __p41.summit = :summit - AND __p41.published = 0 '. + AND __p41.published = 0 + AND (__p41.progress != '.Presentation::PHASE_COMPLETE.' OR __p41.status IS NULL OR __p41.status != \''.Presentation::STATUS_RECEIVED.'\') '. (!empty($extraSelectionStatusFilter)? sprintf($extraSelectionStatusFilter, '41'): ' '). ' AND NOT EXISTS ( SELECT ___sp41.id @@ -280,7 +282,8 @@ protected function getFilterMappings() LEFT JOIN __pm41.media_upload_type __mut41 WHERE __p41.summit = :summit - AND __p41.published = 0 '. + AND __p41.published = 0 + AND (__p41.progress != '.Presentation::PHASE_COMPLETE.' OR __p41.status IS NULL OR __p41.status != \''.Presentation::STATUS_RECEIVED.'\') '. (!empty($extraSelectionStatusFilter)? sprintf($extraSelectionStatusFilter, '41'): ' '). ' AND NOT EXISTS ( SELECT ___sp41.id diff --git a/app/Repositories/Summit/DoctrineSpeakerRepository.php b/app/Repositories/Summit/DoctrineSpeakerRepository.php index 6ada22339..a533cf229 100644 --- a/app/Repositories/Summit/DoctrineSpeakerRepository.php +++ b/app/Repositories/Summit/DoctrineSpeakerRepository.php @@ -21,6 +21,7 @@ use Libs\Utils\Doctrine\DoctrineStatementValueBinder; use models\main\Member; use models\summit\ISpeakerRepository; +use models\summit\Presentation; use models\summit\PresentationSpeaker; use models\summit\Summit; use models\summit\SummitSelectedPresentation; @@ -266,7 +267,8 @@ protected function getFilterMappings() LEFT JOIN __pm41.media_upload_type __mut41 WHERE __p41.summit = :summit - AND __p41.published = 0'. + AND __p41.published = 0 + AND (__p41.progress != '.Presentation::PHASE_COMPLETE.' OR __p41.status IS NULL OR __p41.status != \''.Presentation::STATUS_RECEIVED.'\')'. (!empty($extraSelectionStatusFilter)? sprintf($extraSelectionStatusFilter, '41'): ' '). 'AND NOT EXISTS ( SELECT ___sp41.id @@ -289,7 +291,8 @@ protected function getFilterMappings() LEFT JOIN __pm42.media_upload_type __mut42 WHERE __p42.summit = :summit - AND __p42.published = 0'. + AND __p42.published = 0 + AND (__p42.progress != '.Presentation::PHASE_COMPLETE.' OR __p42.status IS NULL OR __p42.status != \''.Presentation::STATUS_RECEIVED.'\')'. (!empty($extraSelectionStatusFilter)? sprintf($extraSelectionStatusFilter, '42'): ' '). 'AND NOT EXISTS ( SELECT ___sp42.id @@ -315,7 +318,8 @@ protected function getFilterMappings() LEFT JOIN __pm41.media_upload_type __mut41 WHERE __p41.summit = :summit - AND __p41.published = 0'. + AND __p41.published = 0 + AND (__p41.progress != '.Presentation::PHASE_COMPLETE.' OR __p41.status IS NULL OR __p41.status != \''.Presentation::STATUS_RECEIVED.'\')'. (!empty($extraSelectionStatusFilter)? sprintf($extraSelectionStatusFilter, '41'): ' '). 'AND NOT EXISTS ( SELECT ___sp41.id @@ -338,7 +342,8 @@ protected function getFilterMappings() LEFT JOIN __pm42.media_upload_type __mut42 WHERE __p42.summit = :summit - AND __p42.published = 0'. + AND __p42.published = 0 + AND (__p42.progress != '.Presentation::PHASE_COMPLETE.' OR __p42.status IS NULL OR __p42.status != \''.Presentation::STATUS_RECEIVED.'\')'. (!empty($extraSelectionStatusFilter)? sprintf($extraSelectionStatusFilter, '42'): ' '). 'AND NOT EXISTS ( SELECT ___sp42.id diff --git a/tests/oauth2/OAuth2SummitSpeakersApiTest.php b/tests/oauth2/OAuth2SummitSpeakersApiTest.php index 909a38671..ccf2f2249 100644 --- a/tests/oauth2/OAuth2SummitSpeakersApiTest.php +++ b/tests/oauth2/OAuth2SummitSpeakersApiTest.php @@ -749,12 +749,11 @@ public function testGetCurrentSummitSpeakersWithPendingPresentations() $pres->setAbstract("Abstract"); $pres->setCategory(self::$defaultTrack); $pres->setType(self::$defaultPresentationType); - $pres->setProgress(Presentation::PHASE_COMPLETE); - $pres->setStatus(Presentation::STATUS_RECEIVED); $pres->setStartDate($start); $pres->setEndDate($end); $pres->addSpeaker($speaker); - // Deliberately NOT published and NOT added to any SummitSelectedPresentation group list + // Deliberately unfinished (default progress/status), NOT published and NOT + // added to any SummitSelectedPresentation group list self::$em->flush(); $params = [ @@ -789,6 +788,68 @@ public function testGetCurrentSummitSpeakersWithPendingPresentations() $this->assertTrue(count($speakers->data) > 0); } + /** + * Regression test: has_pending_presentations must reflect an unfinished + * submission, not merely "not yet selected by a track chair". A speaker + * whose presentation is already complete/received must NOT be returned, + * even if it is still unpublished and has no selection-list entry. + */ + public function testGetCurrentSummitSpeakersWithPendingPresentationsExcludesCompletedSubmissions() + { + $speaker = new PresentationSpeaker(); + $speaker->setFirstName("Completed"); + $speaker->setLastName("Speaker"); + self::$em->persist($speaker); + + $start = new \DateTime('now', new \DateTimeZone('UTC')); + $end = (clone $start)->add(new \DateInterval('PT2H')); + + $pres = new Presentation(); + self::$summit->addEvent($pres); + $pres->setTitle("Completed Submission Presentation"); + $pres->setAbstract("Abstract"); + $pres->setCategory(self::$defaultTrack); + $pres->setType(self::$defaultPresentationType); + $pres->setProgress(Presentation::PHASE_COMPLETE); + $pres->setStatus(Presentation::STATUS_RECEIVED); + $pres->setStartDate($start); + $pres->setEndDate($end); + $pres->addSpeaker($speaker); + // Submission is complete/received, but deliberately NOT published and NOT + // added to any SummitSelectedPresentation group list + self::$em->flush(); + + $params = [ + 'id' => self::$summit->getId(), + 'page' => 1, + 'per_page' => 10, + 'filter' => [ + 'has_pending_presentations==true', + ], + 'order' => '+id' + ]; + + $headers = [ + "HTTP_Authorization" => " Bearer " . $this->access_token, + "CONTENT_TYPE" => "application/json" + ]; + + $response = $this->action( + "GET", + "OAuth2SummitSpeakersApiController@getSpeakers", + $params, + [], + [], + [], + $headers + ); + + $this->assertResponseStatus(200); + $ids = array_map(fn($s) => $s->id, json_decode($response->getContent())->data); + $this->assertNotContains($speaker->getId(), $ids, + 'speaker with a completed/received submission must not be treated as pending'); + } + public function testGetCurrentSummitSpeakersFilteredByMemberExternalUserID() { $params = [ @@ -2710,12 +2771,11 @@ public function testGetCurrentSummitSpeakersActivitiesCountWithPendingPresentati $pres->setAbstract("Abstract"); $pres->setCategory(self::$defaultTrack); $pres->setType(self::$defaultPresentationType); - $pres->setProgress(Presentation::PHASE_COMPLETE); - $pres->setStatus(Presentation::STATUS_RECEIVED); $pres->setStartDate($start); $pres->setEndDate($end); $pres->addSpeaker($speaker); - // Deliberately NOT published and NOT added to any SummitSelectedPresentation group list + // Deliberately unfinished (default progress/status), NOT published and NOT + // added to any SummitSelectedPresentation group list self::$em->flush(); $headers = [ diff --git a/tests/oauth2/OAuth2SummitSubmittersApiTest.php b/tests/oauth2/OAuth2SummitSubmittersApiTest.php index 0820a5e1b..02a19833e 100644 --- a/tests/oauth2/OAuth2SummitSubmittersApiTest.php +++ b/tests/oauth2/OAuth2SummitSubmittersApiTest.php @@ -145,12 +145,11 @@ public function testGetCurrentSummitSubmittersWithPendingPresentations() $pres->setAbstract("Abstract"); $pres->setCategory(self::$defaultTrack); $pres->setType(self::$defaultPresentationType); - $pres->setProgress(Presentation::PHASE_COMPLETE); - $pres->setStatus(Presentation::STATUS_RECEIVED); $pres->setStartDate($start); $pres->setEndDate($end); $pres->setCreatedBy(self::$defaultMember); - // Deliberately NOT published and NOT added to any SummitSelectedPresentation group list + // Deliberately unfinished (default progress/status), NOT published and NOT + // added to any SummitSelectedPresentation group list self::$em->flush(); $params = [ @@ -185,6 +184,65 @@ public function testGetCurrentSummitSubmittersWithPendingPresentations() $this->assertTrue(count($submitters->data) > 0); } + /** + * Regression test: has_pending_presentations must reflect an unfinished + * submission, not merely "not yet selected by a track chair". A submitter + * whose presentation is already complete/received must NOT be returned, + * even if it is still unpublished and has no selection-list entry. + */ + public function testGetCurrentSummitSubmittersWithPendingPresentationsExcludesCompletedSubmissions() + { + $member = self::$em->find(Member::class, self::$defaultMember2->getId()); + + $start = new \DateTime('now', new \DateTimeZone('UTC')); + $end = (clone $start)->add(new \DateInterval('PT2H')); + + $pres = new Presentation(); + self::$summit->addEvent($pres); + $pres->setTitle("Completed Submission Presentation"); + $pres->setAbstract("Abstract"); + $pres->setCategory(self::$defaultTrack); + $pres->setType(self::$defaultPresentationType); + $pres->setProgress(Presentation::PHASE_COMPLETE); + $pres->setStatus(Presentation::STATUS_RECEIVED); + $pres->setStartDate($start); + $pres->setEndDate($end); + $pres->setCreatedBy($member); + // Submission is complete/received, but deliberately NOT published and NOT + // added to any SummitSelectedPresentation group list + self::$em->flush(); + + $params = [ + 'id' => self::$summit->getId(), + 'page' => 1, + 'per_page' => 10, + 'filter' => [ + 'has_pending_presentations==true', + ], + 'order' => '+id' + ]; + + $headers = [ + "HTTP_Authorization" => " Bearer " . $this->access_token, + "CONTENT_TYPE" => "application/json" + ]; + + $response = $this->action( + "GET", + "OAuth2SummitSubmittersApiController@getAllBySummit", + $params, + [], + [], + [], + $headers + ); + + $this->assertResponseStatus(200); + $ids = array_map(fn($s) => $s->id, json_decode($response->getContent())->data); + $this->assertNotContains($member->getId(), $ids, + 'submitter with a completed/received submission must not be treated as pending'); + } + public function testExportCurrentSummitSubmittersWhoAreSpeakers() { $params = [ From b0df665e6ab4f22413eaa3a9f5fcba9e6df1cadf Mon Sep 17 00:00:00 2001 From: romanetar Date: Mon, 7 Sep 2026 16:29:18 +0200 Subject: [PATCH 3/4] test(speakers): assert exclusion in has_pending_presentations tests The list/count tests for has_pending_presentations only asserted count > 0 or baseline + 1, which held true even if the filter were a complete no-op: the base speaker/submitter query already returns everyone with summit activity, and getUniqueActivitiesCountBySummit would just count "all activities" if the filter were ignored, still landing on baseline + 1 after adding one presentation. Add a published-presentation negative control to each test and assert on the exact set of returned/counted IDs, so the tests actually fail if the pending filter stops filtering. --- tests/oauth2/OAuth2SummitSpeakersApiTest.php | 59 +++++++++++++++++-- .../oauth2/OAuth2SummitSubmittersApiTest.php | 35 +++++++++-- 2 files changed, 83 insertions(+), 11 deletions(-) diff --git a/tests/oauth2/OAuth2SummitSpeakersApiTest.php b/tests/oauth2/OAuth2SummitSpeakersApiTest.php index ccf2f2249..01ae86586 100644 --- a/tests/oauth2/OAuth2SummitSpeakersApiTest.php +++ b/tests/oauth2/OAuth2SummitSpeakersApiTest.php @@ -756,10 +756,33 @@ public function testGetCurrentSummitSpeakersWithPendingPresentations() // added to any SummitSelectedPresentation group list self::$em->flush(); + // Negative control: a speaker with a published presentation must NOT be + // returned. Without this control, the assertion below would pass even if + // the filter were a complete no-op, since the base query already returns + // every speaker with summit activity. + $notPendingSpeaker = new PresentationSpeaker(); + $notPendingSpeaker->setFirstName("NotPending"); + $notPendingSpeaker->setLastName("Speaker"); + self::$em->persist($notPendingSpeaker); + + $publishedPres = new Presentation(); + self::$summit->addEvent($publishedPres); + $publishedPres->setTitle("Published Control Presentation"); + $publishedPres->setAbstract("Abstract"); + $publishedPres->setCategory(self::$defaultTrack); + $publishedPres->setType(self::$defaultPresentationType); + $publishedPres->setProgress(Presentation::PHASE_COMPLETE); + $publishedPres->setStatus(Presentation::STATUS_RECEIVED); + $publishedPres->setStartDate($start); + $publishedPres->setEndDate($end); + $publishedPres->addSpeaker($notPendingSpeaker); + $publishedPres->publish(); + self::$em->flush(); + $params = [ 'id' => self::$summit->getId(), 'page' => 1, - 'per_page' => 10, + 'per_page' => 100, 'filter' => [ 'has_pending_presentations==true', ], @@ -781,11 +804,13 @@ public function testGetCurrentSummitSpeakersWithPendingPresentations() $headers ); - $content = $response->getContent(); $this->assertResponseStatus(200); - $speakers = json_decode($content); - $this->assertTrue(!is_null($speakers)); - $this->assertTrue(count($speakers->data) > 0); + $ids = array_map(fn($s) => $s->id, json_decode($response->getContent())->data); + + $this->assertContains($speaker->getId(), $ids, + 'speaker with an unfinished, unpublished, unselected presentation must be returned'); + $this->assertNotContains($notPendingSpeaker->getId(), $ids, + 'speaker with a published presentation must not be treated as pending'); } /** @@ -2778,6 +2803,30 @@ public function testGetCurrentSummitSpeakersActivitiesCountWithPendingPresentati // added to any SummitSelectedPresentation group list self::$em->flush(); + // Negative control: a published presentation must NOT count as pending. + // Without this control, baseline + 1 would hold even if the filter were a + // complete no-op — getUniqueActivitiesCountBySummit would then just count + // "all activities", and one extra presentation always adds exactly 1 + // regardless of whether the filter actually excludes it. + $notPendingSpeaker = new PresentationSpeaker(); + $notPendingSpeaker->setFirstName("NotPending"); + $notPendingSpeaker->setLastName("Test"); + self::$em->persist($notPendingSpeaker); + + $publishedPres = new Presentation(); + self::$summit->addEvent($publishedPres); + $publishedPres->setTitle("Published Control Presentation"); + $publishedPres->setAbstract("Abstract"); + $publishedPres->setCategory(self::$defaultTrack); + $publishedPres->setType(self::$defaultPresentationType); + $publishedPres->setProgress(Presentation::PHASE_COMPLETE); + $publishedPres->setStatus(Presentation::STATUS_RECEIVED); + $publishedPres->setStartDate($start); + $publishedPres->setEndDate($end); + $publishedPres->addSpeaker($notPendingSpeaker); + $publishedPres->publish(); + self::$em->flush(); + $headers = [ "HTTP_Authorization" => " Bearer " . $this->access_token, "CONTENT_TYPE" => "application/json" diff --git a/tests/oauth2/OAuth2SummitSubmittersApiTest.php b/tests/oauth2/OAuth2SummitSubmittersApiTest.php index 02a19833e..0d74ea581 100644 --- a/tests/oauth2/OAuth2SummitSubmittersApiTest.php +++ b/tests/oauth2/OAuth2SummitSubmittersApiTest.php @@ -136,6 +136,9 @@ public function testGetCurrentSummitSubmittersWithAcceptedPresentations() public function testGetCurrentSummitSubmittersWithPendingPresentations() { + $member = self::$em->find(Member::class, self::$defaultMember->getId()); + $member2 = self::$em->find(Member::class, self::$defaultMember2->getId()); + $start = new \DateTime('now', new \DateTimeZone('UTC')); $end = (clone $start)->add(new \DateInterval('PT2H')); @@ -147,15 +150,33 @@ public function testGetCurrentSummitSubmittersWithPendingPresentations() $pres->setType(self::$defaultPresentationType); $pres->setStartDate($start); $pres->setEndDate($end); - $pres->setCreatedBy(self::$defaultMember); + $pres->setCreatedBy($member); // Deliberately unfinished (default progress/status), NOT published and NOT // added to any SummitSelectedPresentation group list + + // Negative control: a submitter with a published presentation must NOT be + // returned. Without this control, the assertion below would pass even if + // the filter were a complete no-op, since the base query already returns + // every submitter with summit activity. + $publishedPres = new Presentation(); + self::$summit->addEvent($publishedPres); + $publishedPres->setTitle("Published Control Presentation"); + $publishedPres->setAbstract("Abstract"); + $publishedPres->setCategory(self::$defaultTrack); + $publishedPres->setType(self::$defaultPresentationType); + $publishedPres->setProgress(Presentation::PHASE_COMPLETE); + $publishedPres->setStatus(Presentation::STATUS_RECEIVED); + $publishedPres->setStartDate($start); + $publishedPres->setEndDate($end); + $publishedPres->setCreatedBy($member2); + $publishedPres->publish(); + self::$em->flush(); $params = [ 'id' => self::$summit->getId(), 'page' => 1, - 'per_page' => 10, + 'per_page' => 100, 'filter' => [ 'has_pending_presentations==true', ], @@ -177,11 +198,13 @@ public function testGetCurrentSummitSubmittersWithPendingPresentations() $headers ); - $content = $response->getContent(); $this->assertResponseStatus(200); - $submitters = json_decode($content); - $this->assertTrue(!is_null($submitters)); - $this->assertTrue(count($submitters->data) > 0); + $ids = array_map(fn($s) => $s->id, json_decode($response->getContent())->data); + + $this->assertContains($member->getId(), $ids, + 'submitter with an unfinished, unpublished, unselected presentation must be returned'); + $this->assertNotContains($member2->getId(), $ids, + 'submitter with a published presentation must not be treated as pending'); } /** From 2324cb92f3b279f65ca4b902b26761b2e57f968d Mon Sep 17 00:00:00 2001 From: romanetar Date: Mon, 7 Sep 2026 16:44:37 +0200 Subject: [PATCH 4/4] docs(speakers): sync getSpeakers OpenAPI filter description with its siblings The protected getSpeakers filter parameter still had the stale, truncated description ("...has_accepted_presentations, etc.") while the public getSpeakersPublic and the submitters endpoints were already updated with the full enumerated filter list, including has_pending_presentations and has_published_presentations. Align it so the generated docs advertise the same filters on both routes. --- .../Apis/Protected/Summit/OAuth2SummitSpeakersApiController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSpeakersApiController.php b/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSpeakersApiController.php index c27c94fc3..2a22f56ea 100644 --- a/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSpeakersApiController.php +++ b/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSpeakersApiController.php @@ -179,7 +179,7 @@ public function __construct ), new OA\Parameter( name: 'filter', - description: 'Filter by id, first_name, last_name, email, full_name, member_id, has_accepted_presentations, etc.', + description: 'Filter by id, not_id, first_name, last_name, email, full_name, member_id, member_user_external_id, has_pending_presentations, has_accepted_presentations, has_alternate_presentations, has_rejected_presentations, has_published_presentations, presentations_track_id, presentations_track_group_id, presentations_selection_plan_id, presentations_type_id, presentations_title, presentations_abstract, presentations_submitter_full_name, presentations_submitter_email, has_media_upload_with_type, has_not_media_upload_with_type. Operands supported: == (equal), @@ (contains), =@ (starts with).', in: 'query', required: false, schema: new OA\Schema(type: 'string')