From bd000e6788a02662313ed58647d12f8ada4c88b5 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 ++++++++++++++++++ ...Auth2BearerAccessTokenRequestValidator.php | 2 +- .../Summit/SummitSerializer.php | 30 ++--- .../Main/Repositories/IMemberRepository.php | 8 ++ .../Repositories/ISpeakerRepository.php | 8 ++ .../Summit/DoctrineMemberRepository.php | 44 ++++++- .../Summit/DoctrineSpeakerRepository.php | 47 ++++++++ database/seeders/ApiEndpointsSeeder.php | 28 +++++ database/seeders/ConfigSeeder.php | 10 ++ docker-compose.override.testing.yml | 16 +++ routes/api_v1.php | 2 + tests/SubmitterRepositoryTest.php | 24 ++++ tests/oauth2/OAuth2SummitSpeakersApiTest.php | 96 ++++++++++++++++ .../oauth2/OAuth2SummitSubmittersApiTest.php | 61 ++++++++++ 17 files changed, 574 insertions(+), 18 deletions(-) create mode 100644 docker-compose.override.testing.yml 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..4da71801d3 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 or slug", + schema: new OA\Schema(type: "string") + ), + 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/Http/Middleware/OAuth2BearerAccessTokenRequestValidator.php b/app/Http/Middleware/OAuth2BearerAccessTokenRequestValidator.php index 502bb64ebf..902a93acd6 100644 --- a/app/Http/Middleware/OAuth2BearerAccessTokenRequestValidator.php +++ b/app/Http/Middleware/OAuth2BearerAccessTokenRequestValidator.php @@ -173,7 +173,7 @@ public function handle($request, Closure $next) } if ( $token_info->getApplicationType() === 'JS_CLIENT' - && (is_null($origin) || empty($origin)|| str_contains($token_info->getAllowedOrigins(), $origin) === false ) + && (is_null($origin) || empty($origin)|| str_contains($token_info->getAllowedOrigins(), rtrim($origin, '/')) === false ) ) { //check origins throw new OAuth2ResourceServerException( diff --git a/app/ModelSerializers/Summit/SummitSerializer.php b/app/ModelSerializers/Summit/SummitSerializer.php index 45f71321c8..b285d92917 100644 --- a/app/ModelSerializers/Summit/SummitSerializer.php +++ b/app/ModelSerializers/Summit/SummitSerializer.php @@ -320,24 +320,26 @@ 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')); - + try { + $profile = $build_default_payment_gateway_profile_strategy->build(IPaymentConstants::ApplicationTypeRegistration); + $serializer = SerializerRegistry::getInstance()->getSerializer($profile, $this->getSerializerType()); + if (!is_null($serializer)) + $values['payment_profiles'][] = $serializer->serialize(AbstractSerializer::filterExpandByPrefix($expand, 'payment_profiles')); + } catch (\Exception $ex) { + Log::warning($ex->getMessage()); + } } if (!$has_bookable_rooms_profile && !is_null($build_default_payment_gateway_profile_strategy)) { - $values['payment_profiles'][] = - SerializerRegistry::getInstance()->getSerializer - ( - $build_default_payment_gateway_profile_strategy->build(IPaymentConstants::ApplicationTypeBookableRooms), - $this->getSerializerType() - )->serialize(AbstractSerializer::filterExpandByPrefix($expand, 'payment_profiles')); + try { + $profile = $build_default_payment_gateway_profile_strategy->build(IPaymentConstants::ApplicationTypeBookableRooms); + $serializer = SerializerRegistry::getInstance()->getSerializer($profile, $this->getSerializerType()); + if (!is_null($serializer)) + $values['payment_profiles'][] = $serializer->serialize(AbstractSerializer::filterExpandByPrefix($expand, 'payment_profiles')); + } catch (\Exception $ex) { + Log::warning($ex->getMessage()); + } } } 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..7595f53ae6 100644 --- a/app/Repositories/Summit/DoctrineSpeakerRepository.php +++ b/app/Repositories/Summit/DoctrineSpeakerRepository.php @@ -697,6 +697,53 @@ function ($query) { } + /** + * @param Summit $summit + * @param Filter|null $filter + * @return int + * @throws \Doctrine\DBAL\Exception + */ + public function getUniqueActivitiesCountBySummit(Summit $summit, Filter $filter = null): int + { + // Query 1: presentations where an assigned speaker matches the filter. + // Join Presentation → PresentationSpeakerAssignment → PresentationSpeaker(e), + // reusing the same 'e', 'm', 'rr' aliases that getFilterMappings() expects. + $speakerQb = $this->getEntityManager()->createQueryBuilder() + ->select("p.id") + ->from('models\summit\Presentation', 'p') + ->join('p.speakers', '__cnt_assign') + ->join('__cnt_assign.speaker', 'e') + ->leftJoin("e.registration_request", "rr") + ->leftJoin("e.member", "m") + ->where('p.summit = :summit') + ->setParameter('summit', $summit); + + if (!is_null($filter)) { + $filter->apply2Query($speakerQb, $this->getFilterMappings($filter)); + } + + // Query 2: presentations where the moderator matches the filter. + $moderatorQb = $this->getEntityManager()->createQueryBuilder() + ->select("p.id") + ->from('models\summit\Presentation', 'p') + ->join('p.moderator', 'e') + ->leftJoin("e.registration_request", "rr") + ->leftJoin("e.member", "m") + ->where('p.summit = :summit') + ->setParameter('summit', $summit); + + if (!is_null($filter)) { + $filter->apply2Query($moderatorQb, $this->getFilterMappings($filter)); + } + + // Take the union of both result sets and count distinct presentation IDs. + // Presentation counts per summit are small (<1000) so PHP-side deduplication is fine. + $speakerIds = array_column($speakerQb->getQuery()->getScalarResult(), 'id'); + $moderatorIds = array_column($moderatorQb->getQuery()->getScalarResult(), 'id'); + + return count(array_unique(array_merge($speakerIds, $moderatorIds))); + } + /** * @param Summit $summit * @param PagingInfo $paging_info 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..04d16bd22d 100644 --- a/database/seeders/ConfigSeeder.php +++ b/database/seeders/ConfigSeeder.php @@ -31,6 +31,16 @@ public function run() // clear all $em = Registry::getManager(ResourceServerEntity::EntityManager); $em->clear(); + + // evict the L2 (second-level) cache for all resource-server entities so that + // stale cached IDs from previous seeder runs don't cause FK violations after + // the raw SQL DELETEs below reset the auto-increment sequence. + $l2Cache = $em->getCache(); + if ($l2Cache !== null) { + $l2Cache->evictEntityRegions(); + $l2Cache->evictQueryRegions(); + } + $connection = $em->getConnection(); $connection->beginTransaction(); $statements = [ diff --git a/docker-compose.override.testing.yml b/docker-compose.override.testing.yml new file mode 100644 index 0000000000..b3da365a2a --- /dev/null +++ b/docker-compose.override.testing.yml @@ -0,0 +1,16 @@ +# This file is used to run this project locally alongside openstackid without port conflicts. +services: + otel-collector: + ports: !override + - "1889:1888" # pprof extension + - "8890:8888" # Prometheus metrics exposed by the Collector + - "8891:8889" # Prometheus exporter metrics + - "13134:13133" # health_check extension + - "4319:4317" # OTLP gRPC receiver + - "4320:4318" # OTLP http receiver + - "55680:55679" # zpages extension + + elasticsearch: + ports: !override + - "9202:9200" + - "9302:9300" 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..7b7ebc56f5 100644 --- a/tests/SubmitterRepositoryTest.php +++ b/tests/SubmitterRepositoryTest.php @@ -33,6 +33,7 @@ public function testGetSubmittersBySummit(){ $summit_repository = EntityManager::getRepository(Summit::class); $summit = $summit_repository->find(3401); + if (is_null($summit)) $this->markTestSkipped('Summit 3401 not in test DB'); $filter = FilterParser::parse( ["filter" => "is_speaker==false"], @@ -62,6 +63,7 @@ public function testGetSubmittersIdsBySummit(){ $summit_repository = EntityManager::getRepository(Summit::class); $summit = $summit_repository->find(3363); + if (is_null($summit)) $this->markTestSkipped('Summit 3363 not in test DB'); $filter = FilterParser::parse( ["filter" => "has_rejected_presentations==false"], @@ -76,4 +78,26 @@ public function testGetSubmittersIdsBySummit(){ self::assertNotEmpty($submitterIds); } + + public function testGetUniqueActivitiesCountBySummit(){ + $submitter_repository = EntityManager::getRepository(Member::class); + $summit_repository = EntityManager::getRepository(Summit::class); + + $summit = $summit_repository->find(3401); + if (is_null($summit)) $this->markTestSkipped('Summit 3401 not in test DB'); + + $totalCount = $submitter_repository->getUniqueActivitiesCountBySummit($summit, null); + self::assertIsInt($totalCount); + self::assertGreaterThan(0, $totalCount); + + $filter = FilterParser::parse( + ["filter" => "is_speaker==false"], + ["is_speaker" => ['==']] + ); + + $filteredCount = $submitter_repository->getUniqueActivitiesCountBySummit($summit, $filter); + + 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..36ae7f01a7 100644 --- a/tests/oauth2/OAuth2SummitSpeakersApiTest.php +++ b/tests/oauth2/OAuth2SummitSpeakersApiTest.php @@ -1871,4 +1871,100 @@ public function testDeclineSpeakerEditPermission() $this->assertEquals(200, $response->getStatusCode()); } + public function testGetCurrentSummitSpeakersActivitiesCount() + { + $params = [ + 'id' => self::$summit->getId(), + ]; + + $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); + } + + 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