From d9cf735168a1462b2e12583eb0848cbad86aeae7 Mon Sep 17 00:00:00 2001 From: gbutler Date: Thu, 7 May 2026 10:30:40 -0500 Subject: [PATCH] feat(speakers): add unique activities count endpoints for speakers and submitters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add GET /summits/{id}/speakers/all/events/count and GET /summits/{id}/submitters/all/events/count controller methods with OpenAPI attributes and route registrations - Implement getUniqueActivitiesCountBySummit in DoctrineSpeakerRepository and DoctrineMemberRepository using two-stage DQL→raw SQL COUNT(DISTINCT) - Add interface declarations to ISpeakerRepository and IMemberRepository - Register both endpoints in ApiEndpointsSeeder with ReadSummitData scopes - Add PHPUnit tests for both HTTP endpoints and repository-level method - Fix null-guard bug in DoctrineMemberRepository::applyExtraJoins --- .env.example | 4 +- .gitignore | 1 + .../OAuth2SummitSpeakersApiController.php | 104 +++++++++++++++ .../OAuth2SummitSubmittersApiController.php | 107 ++++++++++++++++ .../Summit/SummitSerializer.php | 3 +- .../Main/Repositories/IMemberRepository.php | 8 ++ .../Repositories/ISpeakerRepository.php | 8 ++ .../Summit/DoctrineMemberRepository.php | 44 ++++++- .../Summit/DoctrineSpeakerRepository.php | 33 +++++ .../config/Version20260603000000.php | 69 ++++++++++ database/seeders/ApiEndpointsSeeder.php | 28 +++++ database/seeders/ConfigSeeder.php | 1 + routes/api_v1.php | 2 + tests/SubmitterRepositoryTest.php | 52 ++++++-- tests/oauth2/OAuth2SummitSpeakersApiTest.php | 118 ++++++++++++++++++ .../oauth2/OAuth2SummitSubmittersApiTest.php | 61 +++++++++ 16 files changed, 625 insertions(+), 18 deletions(-) create mode 100644 database/migrations/config/Version20260603000000.php diff --git a/.env.example b/.env.example index ecfc74dbc8..a76c0ab5b3 100644 --- a/.env.example +++ b/.env.example @@ -38,8 +38,8 @@ QUEUE_DATABASE= MAIL_DRIVER=sendgrid SENDGRID_API_KEY='YOUR_SENDGRID_API_KEY' -CORS_ALLOWED_HEADERS=origin, content-type, accept, authorization, x-requested-with -CORS_ALLOWED_METHODS=GET, POST, OPTIONS, PUT, DELETE +CORS_ALLOWED_HEADERS="origin, content-type, accept, authorization, x-requested-with" +CORS_ALLOWED_METHODS="GET, POST, OPTIONS, PUT, DELETE" CORS_USE_PRE_FLIGHT_CACHING=true CORS_MAX_AGE=3200 CORS_EXPOSED_HEADERS= diff --git a/.gitignore b/.gitignore index e90b4d509c..1af1e0a00a 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,4 @@ public/apc.php .nvmrc .codegraph docs/ +docker-compose.override.yml diff --git a/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSpeakersApiController.php b/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSpeakersApiController.php index 01a169c6b4..3a4a78db5e 100644 --- a/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSpeakersApiController.php +++ b/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSpeakersApiController.php @@ -392,6 +392,110 @@ function ($page, $per_page, $filter, $order, $applyExtraFilters) use ($summit) { ); } + #[OA\Get( + path: '/api/v1/summits/{id}/speakers/all/events/count', + operationId: 'getSpeakersActivitiesCount', + description: 'Get the count of unique activities associated with speakers matching the filter criteria', + tags: ['Summit Speakers'], + security: [['summit_speakers_oauth2' => [ + SummitScopes::ReadSummitData, + SummitScopes::ReadAllSummitData + ]]], + parameters: [ + new OA\Parameter( + name: 'id', + description: 'Summit ID', + in: 'path', + required: true, + schema: new OA\Schema(type: 'integer') + ), + 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, 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') + ), + ], + responses: [ + new OA\Response( + response: Response::HTTP_OK, + description: 'Unique activities count', + content: new OA\JsonContent( + properties: [new OA\Property(property: 'count', type: 'integer')] + ) + ), + new OA\Response(response: Response::HTTP_NOT_FOUND, description: 'Not Found'), + new OA\Response(response: Response::HTTP_INTERNAL_SERVER_ERROR, description: 'Server Error'), + ] + )] + public function getSpeakersActivitiesCount($summit_id) + { + return $this->processRequest(function () use ($summit_id) { + + $summit = SummitFinderStrategyFactory::build($this->getRepository(), $this->getResourceServerContext())->find($summit_id); + if (is_null($summit)) return $this->error404(); + + $filter = null; + + if (Request::has('filter')) { + $filter = FilterParser::parse(Request::input('filter'), [ + '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' => ['=='], + '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' => ['=='], + ]); + } + + if (!is_null($filter)) { + $filter->validate([ + 'id' => 'sometimes|integer', + 'not_id' => 'sometimes|integer', + 'first_name' => 'sometimes|string', + 'last_name' => 'sometimes|string', + 'email' => 'sometimes|string', + 'full_name' => 'sometimes|string', + 'member_id' => 'sometimes|integer', + 'member_user_external_id' => 'sometimes|integer', + 'has_accepted_presentations' => 'sometimes|required|string|in:true,false', + 'has_alternate_presentations' => 'sometimes|required|string|in:true,false', + 'has_rejected_presentations' => 'sometimes|required|string|in:true,false', + 'presentations_track_id' => 'sometimes|integer', + 'presentations_track_group_id' => 'sometimes|integer', + 'presentations_selection_plan_id' => 'sometimes|integer', + 'presentations_type_id' => 'sometimes|integer', + 'presentations_title' => 'sometimes|string', + 'presentations_abstract' => 'sometimes|string', + 'presentations_submitter_full_name' => 'sometimes|string', + 'presentations_submitter_email' => 'sometimes|string', + 'has_media_upload_with_type' => 'sometimes|integer', + 'has_not_media_upload_with_type' => 'sometimes|integer', + ]); + } + + $count = $this->speaker_repository->getUniqueActivitiesCountBySummit($summit, $filter); + + return $this->ok(['count' => $count]); + }); + } + /** * @param $summit_id * @return mixed diff --git a/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSubmittersApiController.php b/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSubmittersApiController.php index 0a4a84827f..86653a432d 100644 --- a/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSubmittersApiController.php +++ b/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSubmittersApiController.php @@ -495,4 +495,111 @@ public function send($summit_id) return $this->ok(); }); } + + #[OA\Get( + path: "/api/v1/summits/{id}/submitters/all/events/count", + summary: "Get unique activities count for submitters", + operationId: "getSubmittersActivitiesCount", + tags: ["Summit Submitters"], + security: [['summit_submitters_oauth2' => [ + SummitScopes::ReadSummitData, + SummitScopes::ReadAllSummitData, + ]]], + parameters: [ + new OA\Parameter( + name: "id", + in: "path", + required: true, + description: "Summit ID", + schema: new OA\Schema(type: "integer") + ), + new OA\Parameter( + 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, 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") + ), + ], + responses: [ + new OA\Response( + response: Response::HTTP_OK, + description: "Unique activities count", + content: new OA\JsonContent( + properties: [new OA\Property(property: "count", type: "integer")] + ) + ), + new OA\Response(response: Response::HTTP_BAD_REQUEST, description: "Bad Request"), + new OA\Response(response: Response::HTTP_UNAUTHORIZED, description: "Unauthorized"), + new OA\Response(response: Response::HTTP_FORBIDDEN, description: "Forbidden"), + new OA\Response(response: Response::HTTP_NOT_FOUND, description: "Summit not found"), + new OA\Response(response: Response::HTTP_INTERNAL_SERVER_ERROR, description: "Server Error"), + ] + )] + public function getSubmittersActivitiesCount($summit_id) + { + return $this->processRequest(function () use ($summit_id) { + + $summit = SummitFinderStrategyFactory::build($this->summit_repository, $this->getResourceServerContext())->find(intval($summit_id)); + if (is_null($summit)) return $this->error404(); + + $filter = null; + + if (Request::has('filter')) { + $filter = FilterParser::parse(Request::input('filter'), [ + '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' => ['=='], + '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' => ['=='], + ]); + } + + if (!is_null($filter)) { + $filter->validate([ + 'id' => 'sometimes|integer', + 'not_id' => 'sometimes|integer', + 'first_name' => 'sometimes|string', + 'last_name' => 'sometimes|string', + 'email' => 'sometimes|string', + 'full_name' => 'sometimes|string', + 'member_id' => 'sometimes|integer', + 'member_user_external_id' => 'sometimes|integer', + 'has_accepted_presentations' => 'sometimes|string|in:true,false', + 'has_alternate_presentations' => 'sometimes|string|in:true,false', + 'has_rejected_presentations' => 'sometimes|string|in:true,false', + 'presentations_track_id' => 'sometimes|integer', + 'presentations_selection_plan_id' => 'sometimes|integer', + 'presentations_type_id' => 'sometimes|integer', + 'presentations_title' => 'sometimes|string', + 'presentations_abstract' => 'sometimes|string', + 'presentations_submitter_full_name' => 'sometimes|string', + 'presentations_submitter_email' => 'sometimes|string', + 'is_speaker' => 'sometimes|string|in:true,false', + 'has_media_upload_with_type' => 'sometimes|integer', + 'has_not_media_upload_with_type' => 'sometimes|integer', + ]); + } + + $count = $this->repository->getUniqueActivitiesCountBySummit($summit, $filter); + + return $this->ok(['count' => $count]); + }); + } } diff --git a/app/ModelSerializers/Summit/SummitSerializer.php b/app/ModelSerializers/Summit/SummitSerializer.php index 45f71321c8..3e4929b86b 100644 --- a/app/ModelSerializers/Summit/SummitSerializer.php +++ b/app/ModelSerializers/Summit/SummitSerializer.php @@ -320,14 +320,13 @@ public function serialize($expand = null, array $fields = [], array $relations = if (!$has_registration_profile && !is_null($build_default_payment_gateway_profile_strategy) ) { - + $values['payment_profiles'][] = SerializerRegistry::getInstance()->getSerializer ( $build_default_payment_gateway_profile_strategy->build(IPaymentConstants::ApplicationTypeRegistration), $this->getSerializerType() )->serialize(AbstractSerializer::filterExpandByPrefix($expand, 'payment_profiles')); - } if (!$has_bookable_rooms_profile && diff --git a/app/Models/Foundation/Main/Repositories/IMemberRepository.php b/app/Models/Foundation/Main/Repositories/IMemberRepository.php index 94b1802cc7..324273c714 100644 --- a/app/Models/Foundation/Main/Repositories/IMemberRepository.php +++ b/app/Models/Foundation/Main/Repositories/IMemberRepository.php @@ -90,4 +90,12 @@ public function getSubmittersBySummit(Summit $summit, PagingInfo $paging_info, F * @throws \Doctrine\DBAL\Exception */ public function getSubmittersIdsBySummit(Summit $summit, PagingInfo $paging_info, Filter $filter = null, Order $order = null); + + /** + * @param Summit $summit + * @param Filter|null $filter + * @return int + * @throws \Doctrine\DBAL\Exception + */ + public function getUniqueActivitiesCountBySummit(Summit $summit, Filter $filter = null): int; } \ No newline at end of file diff --git a/app/Models/Foundation/Summit/Repositories/ISpeakerRepository.php b/app/Models/Foundation/Summit/Repositories/ISpeakerRepository.php index 7bc8c74964..e6587be3e1 100644 --- a/app/Models/Foundation/Summit/Repositories/ISpeakerRepository.php +++ b/app/Models/Foundation/Summit/Repositories/ISpeakerRepository.php @@ -93,4 +93,12 @@ public function getSpeakersIdsBySummit(Summit $summit, PagingInfo $paging_info, * @return PagingResponse */ public function getAllCompaniesByPage(PagingInfo $paging_info, Filter $filter = null, Order $order = null); + + /** + * @param Summit $summit + * @param Filter|null $filter + * @return int + * @throws \Doctrine\DBAL\Exception + */ + public function getUniqueActivitiesCountBySummit(Summit $summit, Filter $filter = null): int; } \ No newline at end of file diff --git a/app/Repositories/Summit/DoctrineMemberRepository.php b/app/Repositories/Summit/DoctrineMemberRepository.php index 8bc166e14b..8b09a26f4e 100644 --- a/app/Repositories/Summit/DoctrineMemberRepository.php +++ b/app/Repositories/Summit/DoctrineMemberRepository.php @@ -58,7 +58,7 @@ protected function getBaseEntity() */ protected function applyExtraJoins(QueryBuilder $query, ?Filter $filter = null, ?Order $order = null): QueryBuilder { - if($filter->hasFilter("summit_id") || $filter->hasFilter("schedule_event_id")){ + if(!is_null($filter) && ($filter->hasFilter("summit_id") || $filter->hasFilter("schedule_event_id"))){ $query ->leftJoin("e.schedule","sch") ->leftJoin("sch.event", "evt") @@ -638,6 +638,48 @@ function ($query) { }); } + /** + * @param Summit $summit + * @param Filter|null $filter + * @return int + * @throws \Doctrine\DBAL\Exception + */ + public function getUniqueActivitiesCountBySummit(Summit $summit, Filter $filter = null): int + { + // Collect distinct member IDs matching the summit + filter using the + // same base query / filter mappings as getSubmittersBySummit. + $qb = $this->getEntityManager()->createQueryBuilder() + ->distinct(true) + ->select("e.id") + ->from($this->getBaseEntity(), "e") + ->where(" + EXISTS ( + SELECT __p.id FROM models\summit\Presentation __p + WHERE __p.created_by = e AND __p.summit = :summit + )") + ->setParameter("summit", $summit); + + $qb = $this->applyExtraJoins($qb, $filter); + + if (!is_null($filter)) { + $filter->apply2Query($qb, $this->getFilterMappings($filter)); + } + + // Count distinct presentations using the member query as a subquery — no PHP ID materialization. + $countQb = $this->getEntityManager()->createQueryBuilder() + ->select("COUNT(DISTINCT p.id)") + ->from('models\summit\Presentation', 'p') + ->where('p.summit = :summit_outer') + ->andWhere("p.created_by IN ({$qb->getDQL()})"); + + $countQb->setParameter('summit_outer', $summit); + foreach ($qb->getParameters() as $param) { + $countQb->setParameter($param->getName(), $param->getValue(), $param->getType()); + } + + return intval($countQb->getQuery()->getSingleScalarResult()); + } + /** * @param PagingInfo $paging_info * @param Filter|null $filter diff --git a/app/Repositories/Summit/DoctrineSpeakerRepository.php b/app/Repositories/Summit/DoctrineSpeakerRepository.php index 272f7c86ed..b57ab9fbea 100644 --- a/app/Repositories/Summit/DoctrineSpeakerRepository.php +++ b/app/Repositories/Summit/DoctrineSpeakerRepository.php @@ -697,6 +697,39 @@ function ($query) { } + /** + * @param Summit $summit + * @param Filter|null $filter + * @return int + * @throws \Doctrine\DBAL\Exception + */ + public function getUniqueActivitiesCountBySummit(Summit $summit, Filter $filter = null): int + { + // Single query: cross-join Presentation × PresentationSpeaker, then filter to + // rows where the speaker is either an assigned speaker OR the moderator. + // COUNT(DISTINCT p.id) deduplicates in SQL — no PHP-side array_unique needed. + // All aliases (e, m, rr) are top-level, so getFilterMappings() applies unchanged. + $countQb = $this->getEntityManager()->createQueryBuilder() + ->select('COUNT(DISTINCT p.id)') + ->from('models\summit\Presentation', 'p') + ->from('models\summit\PresentationSpeaker', 'e') + ->leftJoin('e.registration_request', 'rr') + ->leftJoin('e.member', 'm') + ->where('p.summit = :summit') + ->andWhere( + 'EXISTS (SELECT 1 FROM App\Models\Foundation\Summit\Speakers\PresentationSpeakerAssignment __cnt' + . ' WHERE __cnt.presentation = p AND __cnt.speaker = e)' + . ' OR p.moderator = e' + ) + ->setParameter('summit', $summit); + + if (!is_null($filter)) { + $filter->apply2Query($countQb, $this->getFilterMappings($filter)); + } + + return intval($countQb->getQuery()->getSingleScalarResult()); + } + /** * @param Summit $summit * @param PagingInfo $paging_info diff --git a/database/migrations/config/Version20260603000000.php b/database/migrations/config/Version20260603000000.php new file mode 100644 index 0000000000..1ec7354c6d --- /dev/null +++ b/database/migrations/config/Version20260603000000.php @@ -0,0 +1,69 @@ + self::SUBMITTERS_ROUTE, self::SPEAKERS_ENDPOINT => self::SPEAKERS_ROUTE] as $name => $route) { + $this->addSql($this->insertEndpoint(self::API_NAME, $name, $route, 'GET')); + + $this->addSql($this->insertEndpointScope(self::API_NAME, $name, SummitScopes::ReadSummitData)); + $this->addSql($this->insertEndpointScope(self::API_NAME, $name, SummitScopes::ReadAllSummitData)); + + foreach (self::AUTHZ_GROUPS as $group) { + $this->addSql($this->insertEndpointAuthzGroup(self::API_NAME, $name, $group)); + } + } + } + + public function down(Schema $schema): void + { + $this->addSql($this->deleteEndpoint(self::API_NAME, self::SUBMITTERS_ENDPOINT)); + $this->addSql($this->deleteEndpoint(self::API_NAME, self::SPEAKERS_ENDPOINT)); + } +} diff --git a/database/seeders/ApiEndpointsSeeder.php b/database/seeders/ApiEndpointsSeeder.php index fb94f015da..66145da8b2 100644 --- a/database/seeders/ApiEndpointsSeeder.php +++ b/database/seeders/ApiEndpointsSeeder.php @@ -3733,6 +3733,20 @@ private function seedSummitEndpoints() IGroup::SummitRegistrationAdmins ] ], + [ + 'name' => 'get-submitters-activities-count', + 'route' => '/api/v1/summits/{id}/submitters/all/events/count', + 'http_method' => 'GET', + 'scopes' => [ + SummitScopes::ReadSummitData, + SummitScopes::ReadAllSummitData, + ], + 'authz_groups' => [ + IGroup::SuperAdmins, + IGroup::Administrators, + IGroup::SummitAdministrators, + ] + ], // speakers [ 'name' => 'get-speakers', @@ -4085,6 +4099,20 @@ private function seedSummitEndpoints() IGroup::SummitRegistrationAdmins ] ], + [ + 'name' => 'get-speakers-activities-count', + 'route' => '/api/v1/summits/{id}/speakers/all/events/count', + 'http_method' => 'GET', + 'scopes' => [ + SummitScopes::ReadSummitData, + SummitScopes::ReadAllSummitData, + ], + 'authz_groups' => [ + IGroup::SuperAdmins, + IGroup::Administrators, + IGroup::SummitAdministrators, + ] + ], // events [ 'name' => 'get-events', diff --git a/database/seeders/ConfigSeeder.php b/database/seeders/ConfigSeeder.php index 2137dceb95..205794981b 100644 --- a/database/seeders/ConfigSeeder.php +++ b/database/seeders/ConfigSeeder.php @@ -31,6 +31,7 @@ public function run() // clear all $em = Registry::getManager(ResourceServerEntity::EntityManager); $em->clear(); + $connection = $em->getConnection(); $connection->beginTransaction(); $statements = [ diff --git a/routes/api_v1.php b/routes/api_v1.php index fad69a60c4..98c312312a 100644 --- a/routes/api_v1.php +++ b/routes/api_v1.php @@ -591,6 +591,7 @@ Route::get('csv', ['middleware' => 'auth.user', 'uses' => 'OAuth2SummitSubmittersApiController@getAllBySummitCSV']); Route::group(['prefix' => 'all'], function () { Route::put('send', ['middleware' => 'auth.user', 'uses' => 'OAuth2SummitSubmittersApiController@send']); + Route::get('events/count', ['middleware' => 'auth.user', 'uses' => 'OAuth2SummitSubmittersApiController@getSubmittersActivitiesCount']); }); }); @@ -604,6 +605,7 @@ Route::get('me', 'OAuth2SummitSpeakersApiController@getMySummitSpeaker'); Route::group(['prefix' => 'all'], function () { Route::put('send', ['middleware' => 'auth.user', 'uses' => 'OAuth2SummitSpeakersApiController@send']); + Route::get('events/count', ['middleware' => 'auth.user', 'uses' => 'OAuth2SummitSpeakersApiController@getSpeakersActivitiesCount']); }); Route::group(['prefix' => '{speaker_id}'], function () { Route::get('', ['middleware' => diff --git a/tests/SubmitterRepositoryTest.php b/tests/SubmitterRepositoryTest.php index 357c096350..589c38598e 100644 --- a/tests/SubmitterRepositoryTest.php +++ b/tests/SubmitterRepositoryTest.php @@ -2,7 +2,6 @@ use App\ModelSerializers\IMemberSerializerTypes; use LaravelDoctrine\ORM\Facades\EntityManager; use models\main\Member; -use models\summit\Summit; use ModelSerializers\SerializerRegistry; use utils\FilterParser; use utils\Order; @@ -23,16 +22,28 @@ **/ /** - * Class AuditModelTest + * Class SubmitterRepositoryTest */ class SubmitterRepositoryTest extends ProtectedApiTestCase { + use InsertSummitTestData; + + protected function setUp(): void + { + parent::setUp(); + self::$defaultMember = self::$member; + self::insertSummitTestData(); + } + + protected function tearDown(): void + { + self::clearSummitTestData(); + parent::tearDown(); + } + public function testGetSubmittersBySummit(){ $submitter_repository = EntityManager::getRepository(Member::class); - $summit_repository = EntityManager::getRepository(Summit::class); - - $summit = $summit_repository->find(3401); $filter = FilterParser::parse( ["filter" => "is_speaker==false"], @@ -43,10 +54,10 @@ public function testGetSubmittersBySummit(){ OrderElement::buildDescFor("id"), ]); - $page = $submitter_repository->getSubmittersBySummit($summit, new PagingInfo(1, 5), $filter, $order); + $page = $submitter_repository->getSubmittersBySummit(self::$summit, new PagingInfo(1, 5), $filter, $order); $params = [ - "summit" => $summit + "summit" => self::$summit ]; foreach ($page->getItems() as $submitter) { @@ -59,9 +70,6 @@ public function testGetSubmittersBySummit(){ public function testGetSubmittersIdsBySummit(){ $submitter_repository = EntityManager::getRepository(Member::class); - $summit_repository = EntityManager::getRepository(Summit::class); - - $summit = $summit_repository->find(3363); $filter = FilterParser::parse( ["filter" => "has_rejected_presentations==false"], @@ -72,8 +80,26 @@ public function testGetSubmittersIdsBySummit(){ OrderElement::buildDescFor("id"), ]); - $submitterIds = $submitter_repository->getSubmittersIdsBySummit($summit, new PagingInfo(1, 5), $filter, $order); + $submitterIds = $submitter_repository->getSubmittersIdsBySummit(self::$summit, new PagingInfo(1, 5), $filter, $order); + + self::assertIsArray($submitterIds); + } + + public function testGetUniqueActivitiesCountBySummit(){ + $submitter_repository = EntityManager::getRepository(Member::class); + + $totalCount = $submitter_repository->getUniqueActivitiesCountBySummit(self::$summit, null); + self::assertIsInt($totalCount); + self::assertGreaterThanOrEqual(0, $totalCount); + + $filter = FilterParser::parse( + ["filter" => "is_speaker==false"], + ["is_speaker" => ['==']] + ); + + $filteredCount = $submitter_repository->getUniqueActivitiesCountBySummit(self::$summit, $filter); - self::assertNotEmpty($submitterIds); + self::assertIsInt($filteredCount); + self::assertLessThanOrEqual($totalCount, $filteredCount); } -} \ No newline at end of file +} diff --git a/tests/oauth2/OAuth2SummitSpeakersApiTest.php b/tests/oauth2/OAuth2SummitSpeakersApiTest.php index 6003096e64..2f1417370e 100644 --- a/tests/oauth2/OAuth2SummitSpeakersApiTest.php +++ b/tests/oauth2/OAuth2SummitSpeakersApiTest.php @@ -16,6 +16,9 @@ use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Date; +use LaravelDoctrine\ORM\Facades\EntityManager; +use models\summit\Presentation; +use models\summit\PresentationSpeaker; use models\summit\SpeakersSummitRegistrationPromoCode; final class OAuth2SummitSpeakersApiTest extends ProtectedApiTestCase @@ -1871,4 +1874,119 @@ public function testDeclineSpeakerEditPermission() $this->assertEquals(200, $response->getStatusCode()); } + public function testGetCurrentSummitSpeakersActivitiesCount() + { + $baseline = EntityManager::getRepository(PresentationSpeaker::class) + ->getUniqueActivitiesCountBySummit(self::$summit, null); + + // Add a presentation where speaker_a is both an assigned speaker AND the + // moderator, and speaker_b is a second assigned speaker. Without array_unique + // this presentation is counted 3 times in the raw join results (speaker_a + // assigned, speaker_b assigned, speaker_a moderator). Correct dedup must + // add exactly 1 to the baseline, not 2 or 3. + $speaker_a = new PresentationSpeaker(); + $speaker_a->setFirstName("Alice"); + $speaker_a->setLastName("Test"); + self::$em->persist($speaker_a); + + $speaker_b = new PresentationSpeaker(); + $speaker_b->setFirstName("Bob"); + $speaker_b->setLastName("Test"); + self::$em->persist($speaker_b); + + $extra = new Presentation(); + self::$summit->addEvent($extra); + $extra->setTitle("Dedup Test Presentation"); + $extra->setAbstract("Dedup Test Abstract"); + $extra->setCategory(self::$defaultTrack); + $extra->setType(self::$defaultPresentationType); + $extra->setProgress(Presentation::PHASE_COMPLETE); + $extra->setStatus(Presentation::STATUS_RECEIVED); + $extra->addSpeaker($speaker_a); + $extra->addSpeaker($speaker_b); + $extra->setModerator($speaker_a); + self::$em->flush(); + + $response = $this->action( + "GET", + "OAuth2SummitSpeakersApiController@getSpeakersActivitiesCount", + ['id' => self::$summit->getId()], + [], [], [], + ["HTTP_Authorization" => " Bearer " . $this->access_token, "CONTENT_TYPE" => "application/json"] + ); + + $this->assertResponseStatus(200); + $data = json_decode($response->getContent()); + $this->assertNotNull($data); + $this->assertTrue(isset($data->count)); + $this->assertEquals($baseline + 1, $data->count); + } + + public function testGetCurrentSummitSpeakersActivitiesCountFilteredBySelPlan() + { + $headers = [ + "HTTP_Authorization" => " Bearer " . $this->access_token, + "CONTENT_TYPE" => "application/json" + ]; + + $unfilteredResponse = $this->action( + "GET", + "OAuth2SummitSpeakersApiController@getSpeakersActivitiesCount", + ['id' => self::$summit->getId()], + [], [], [], $headers + ); + $this->assertResponseStatus(200); + $unfilteredData = json_decode($unfilteredResponse->getContent()); + $this->assertGreaterThan(0, $unfilteredData->count); + + $filteredResponse = $this->action( + "GET", + "OAuth2SummitSpeakersApiController@getSpeakersActivitiesCount", + [ + 'id' => self::$summit->getId(), + 'filter' => [ + sprintf('presentations_selection_plan_id==%s', self::$default_selection_plan->getId()), + ], + ], + [], [], [], $headers + ); + $this->assertResponseStatus(200); + $filteredData = json_decode($filteredResponse->getContent()); + $this->assertNotNull($filteredData); + $this->assertTrue(isset($filteredData->count)); + $this->assertLessThanOrEqual($unfilteredData->count, $filteredData->count); + } + + public function testGetCurrentSummitSpeakersActivitiesCountWithAcceptedPresentations() + { + $params = [ + 'id' => self::$summit->getId(), + 'filter' => [ + 'has_accepted_presentations==true', + ], + ]; + + $headers = [ + "HTTP_Authorization" => " Bearer " . $this->access_token, + "CONTENT_TYPE" => "application/json" + ]; + + $response = $this->action( + "GET", + "OAuth2SummitSpeakersApiController@getSpeakersActivitiesCount", + $params, + [], + [], + [], + $headers + ); + + $content = $response->getContent(); + $this->assertResponseStatus(200); + $data = json_decode($content); + $this->assertNotNull($data); + $this->assertTrue(isset($data->count)); + $this->assertGreaterThan(0, $data->count); + } + } \ No newline at end of file diff --git a/tests/oauth2/OAuth2SummitSubmittersApiTest.php b/tests/oauth2/OAuth2SummitSubmittersApiTest.php index 13547f8c47..eb9c8a5ebb 100644 --- a/tests/oauth2/OAuth2SummitSubmittersApiTest.php +++ b/tests/oauth2/OAuth2SummitSubmittersApiTest.php @@ -239,4 +239,65 @@ public function testGetSubmittersWithSubmittedMediaUploadsWithType() $submitters = json_decode($content); $this->assertTrue(!is_null($submitters)); } + + public function testGetCurrentSummitSubmittersActivitiesCount() + { + $params = [ + 'id' => self::$summit->getId(), + ]; + + $headers = [ + "HTTP_Authorization" => " Bearer " . $this->access_token, + "CONTENT_TYPE" => "application/json" + ]; + + $response = $this->action( + "GET", + "OAuth2SummitSubmittersApiController@getSubmittersActivitiesCount", + $params, + [], + [], + [], + $headers + ); + + $content = $response->getContent(); + $this->assertResponseStatus(200); + $data = json_decode($content); + $this->assertNotNull($data); + $this->assertTrue(isset($data->count)); + $this->assertGreaterThanOrEqual(0, $data->count); + } + + public function testGetCurrentSummitSubmittersActivitiesCountWithAcceptedPresentations() + { + $params = [ + 'id' => self::$summit->getId(), + 'filter' => [ + 'has_accepted_presentations==true', + ], + ]; + + $headers = [ + "HTTP_Authorization" => " Bearer " . $this->access_token, + "CONTENT_TYPE" => "application/json" + ]; + + $response = $this->action( + "GET", + "OAuth2SummitSubmittersApiController@getSubmittersActivitiesCount", + $params, + [], + [], + [], + $headers + ); + + $content = $response->getContent(); + $this->assertResponseStatus(200); + $data = json_decode($content); + $this->assertNotNull($data); + $this->assertTrue(isset($data->count)); + $this->assertGreaterThanOrEqual(0, $data->count); + } } \ No newline at end of file