diff --git a/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSpeakersApiController.php b/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSpeakersApiController.php index 96717d694..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') @@ -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..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; @@ -240,6 +241,63 @@ 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 + 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 + 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 + 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 + 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..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; @@ -252,6 +253,111 @@ 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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..01ae86586 100644 --- a/tests/oauth2/OAuth2SummitSpeakersApiTest.php +++ b/tests/oauth2/OAuth2SummitSpeakersApiTest.php @@ -733,6 +733,148 @@ 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->setStartDate($start); + $pres->setEndDate($end); + $pres->addSpeaker($speaker); + // Deliberately unfinished (default progress/status), NOT published and NOT + // 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' => 100, + '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->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'); + } + + /** + * 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 = [ @@ -2575,7 +2717,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 +2771,81 @@ 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->setStartDate($start); + $pres->setEndDate($end); + $pres->addSpeaker($speaker); + // Deliberately unfinished (default progress/status), NOT published and NOT + // 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" + ]; + + $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..0d74ea581 100644 --- a/tests/oauth2/OAuth2SummitSubmittersApiTest.php +++ b/tests/oauth2/OAuth2SummitSubmittersApiTest.php @@ -134,6 +134,138 @@ public function testGetCurrentSummitSubmittersWithAcceptedPresentations() $this->assertTrue(!is_null($submitters)); } + 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')); + + $pres = new Presentation(); + self::$summit->addEvent($pres); + $pres->setTitle("Pending Test Presentation"); + $pres->setAbstract("Abstract"); + $pres->setCategory(self::$defaultTrack); + $pres->setType(self::$defaultPresentationType); + $pres->setStartDate($start); + $pres->setEndDate($end); + $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' => 100, + '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->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'); + } + + /** + * 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 = [ @@ -444,4 +576,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 +}