Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2902,4 +2902,64 @@ public function getScheduledEventStreamingInfo($summit_id, $event_id)

});
}


#[OA\Get(
path: '/api/v1/summits/{id}/events/all/published/occupancy/overflow',
operationId: 'getOverflowPublishedEvents',
summary: 'Get all published overflow events for a summit',
description: 'Retrieves a paginated list of all events (published and with occupancy == OVERFLOW) for a specific summit.',
security: [['summit_events_api_oauth2' => [SummitScopes::ReadOverflowEvents]]],
tags: ['Summit Events', 'Overflow'],
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: 'page', in: 'query', required: false, description: 'Page number', schema: new OA\Schema(type: 'integer', default: 1)),
new OA\Parameter(name: 'per_page', in: 'query', required: false, description: 'Items per page', schema: new OA\Schema(type: 'integer', default: 10, maximum: 100)),
new OA\Parameter(name: 'filter[]', in: 'query', required: false, description: 'Filter expressions', style: 'form', explode: true, schema: new OA\Schema(type: 'array', items: new OA\Items(type: 'string'))),
new OA\Parameter(name: 'order', in: 'query', required: false, description: 'Order by field(s)', schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'expand', in: 'query', required: false, description: 'Expand relationships', schema: new OA\Schema(type: 'string')),
],
responses: [
new OA\Response(response: Response::HTTP_OK, description: 'Events retrieved successfully', content: new OA\JsonContent(ref: '#/components/schemas/PaginatedSummitEventsResponse')),
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: 'Not Found'),
new OA\Response(response: Response::HTTP_INTERNAL_SERVER_ERROR, description: 'Server Error'),
]
)]

/**
* @param $summit_id
* @return mixed
*/
public function getOverflowPublishedEvents($summit_id)
{
$req = request();
$req->attributes->set('timing.controller_start', microtime(true));
return $this->processRequest(function () use ($summit_id, $req) {
$current_user = $this->resource_server_context->getCurrentUser(true);
return $this->withReplica(function() use ($summit_id, $current_user, $req) {
$strategy = new RetrieveAllOverflowPublishedSummitEventsStrategy($this->repository, $this->event_repository, $this->resource_server_context);
$response = $strategy->getEvents(['summit_id' => $summit_id]);
$req->attributes->set('timing.serializer_start', microtime(true));
$data = $response->toArray
(
SerializerUtils::getExpand(),
SerializerUtils::getFields(),
SerializerUtils::getRelations(),
[
'current_user' => $current_user
],
IPresentationSerializerTypes::OverflowStream
);
$req->attributes->set('timing.serializer_end', microtime(true));
$result = $this->ok($data);
$req->attributes->set('timing.controller_end', microtime(true));
return $result;
});

});
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php namespace App\Http\Controllers;
/**
* Copyright 2026 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use models\summit\SummitEvent;
use utils\FilterParser;

class RetrieveAllOverflowPublishedSummitEventsStrategy extends RetrievePublishedSummitEventsBySummitStrategy
{
protected function buildFilter()
{
$filter = parent::buildFilter();
$filter->addFilterCondition(FilterParser::buildFilter('occupancy','==',SummitEvent::OccupancyOverflow));
return $filter;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,4 @@ protected function buildFilter()
$filter->addFilterCondition(FilterParser::buildFilter('type_allows_location','==','1'));
return $filter;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
* Class RetrievePublishedSummitEventsBySummitStrategy
* @package App\Http\Controllers
*/
final class RetrievePublishedSummitEventsBySummitStrategy extends RetrieveAllSummitEventsBySummitStrategy
class RetrievePublishedSummitEventsBySummitStrategy extends RetrieveAllSummitEventsBySummitStrategy
{
/**
* @return array
Expand All @@ -41,4 +41,4 @@ protected function buildFilter()
$filter->addFilterCondition(FilterParser::buildFilter('type_allows_location','==','1'));
return $filter;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ protected function getValidFilters()
'progress' => ['=='],
'status' => ['=='],
'rsvp_type'=> ['==', '<>','=@', '@@'],
'occupancy' => ['=='],
];
}

Expand Down Expand Up @@ -273,6 +274,7 @@ protected function getFilterValidatorRules(): array
'progress' => 'sometimes|integer',
'status' =>'sometimes|string',
'rsvp_type' => 'sometimes|string|in:' . implode(',', SummitEvent::AllowedRSVPTypes),
'occupancy' => 'sometimes|string|in:' . implode(',', SummitEvent::ValidOccupanciesValues),
];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@
* limitations under the License.
**/

use App\ModelSerializers\Traits\RequestCache;
use Libs\ModelSerializers\AbstractSerializer;
use libs\utils\CacheRegions;
use models\summit\SummitEvent;

/**
Expand All @@ -23,7 +21,6 @@
*/
class SummitEventOverflowStreamingSerializer extends AbstractSerializer
{
use RequestCache;
/**
* @param $expand
* @param array $fields
Expand All @@ -33,25 +30,19 @@ class SummitEventOverflowStreamingSerializer extends AbstractSerializer
*/
public function serialize($expand = null, array $fields = [], array $relations = [], array $params = [])
{
return $this->cache(
CacheRegions::getCacheRegionForSummitEvent($this->object->getIdentifier()),
sprintf("SummitEventOverflowStreamingSerializer_%s", $this->object->getIdentifier()),
function () use ($expand, $fields, $relations, $params) {
$event = $this->object;
if (!$event instanceof SummitEvent) return [];

$event = $this->object;
if (!$event instanceof SummitEvent) return [];

$values['id'] = $event->getId();
$values['title'] = $event->getTitle();
$values['start_date'] = $event->getStartDate()->getTimestamp();
$values['end_date'] = $event->getEndDate()->getTimestamp();
$values['overflow_streaming_url'] = $event->getOverflowStreamingUrl();
$values['overflow_stream_is_secure'] = $event->getOverflowStreamIsSecure();
$values['overflow_tokens'] = [];
if($event->getOverflowStreamIsSecure()){
$values['overflow_tokens'] = $event->getOverflowStreamingTokens();
}
return $values;
});
$values['id'] = $event->getId();
$values['title'] = $event->getTitle();
$values['start_date'] = $event->getStartDate()->getTimestamp();
$values['end_date'] = $event->getEndDate()->getTimestamp();
Comment on lines +38 to +39

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Prevent fatal errors on null dates.

getStartDate() and getEndDate() can return null if the event type does not allow publishing dates. Calling ->getTimestamp() on null will cause a fatal error. Use the nullsafe operator (?->) to handle potentially missing dates gracefully.

🛡️ Proposed fix
-        $values['start_date'] = $event->getStartDate()->getTimestamp();
-        $values['end_date'] = $event->getEndDate()->getTimestamp();
+        $values['start_date'] = $event->getStartDate()?->getTimestamp();
+        $values['end_date'] = $event->getEndDate()?->getTimestamp();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$values['start_date'] = $event->getStartDate()->getTimestamp();
$values['end_date'] = $event->getEndDate()->getTimestamp();
$values['start_date'] = $event->getStartDate()?->getTimestamp();
$values['end_date'] = $event->getEndDate()?->getTimestamp();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/ModelSerializers/Summit/SummitEventOverflowStreamingSerializer.php`
around lines 38 - 39, Update the start_date and end_date assignments in
SummitEventOverflowStreamingSerializer to use null-safe timestamp access on the
values returned by getStartDate() and getEndDate(), preserving null when either
date is unavailable.

$values['overflow_streaming_url'] = $event->getOverflowStreamingUrl();
$values['overflow_stream_is_secure'] = $event->getOverflowStreamIsSecure();
$values['overflow_tokens'] = [];
if($event->getOverflowStreamIsSecure()){
$values['overflow_tokens'] = $event->getOverflowStreamingTokens();
}
return $values;
}
}
1 change: 1 addition & 0 deletions app/Repositories/Summit/DoctrineSummitEventRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ protected function getFilterMappings()
'review_status' => 'REVIEW_STATUS(e.id)',
'submission_source' => 'e.submission_source:json_string',
'rsvp_type' => 'e.rsvp_type:json_string',
'occupancy' => 'e.occupancy:json_string',
'summit_id' => "s.id",
'tags' => "t.tag",
'is_chair_visible' => Filter::buildBooleanField("c.chair_visible"),
Expand Down
1 change: 1 addition & 0 deletions app/Security/SummitScopes.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ final class SummitScopes
{
const ReadSummitData = SCOPE_BASE_REALM.'/summits/read';
const ReadAllSummitData = SCOPE_BASE_REALM.'/summits/read/all';
const ReadOverflowEvents = SCOPE_BASE_REALM.'/summits/events/overflow/read';

// me
const MeRead = SCOPE_BASE_REALM.'/me/read';
Expand Down
92 changes: 92 additions & 0 deletions database/migrations/config/APIEndpointsMigrationHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -231,4 +231,96 @@ protected function deleteApiScopes(string $apiName, array $scopes): string
AND s.name IN ({$scopeList});
SQL;
}

/**
* Register one endpoint plus its scope and authz-group associations, issuing every
* addSql() call needed. Unlike the insert/delete builders above (which return a SQL
* string for the caller to pass to addSql()), this method has the side effect itself.
*
* @param string $apiName API identifier (e.g., 'summits')
* @param string $endpointName Endpoint identifier (e.g., 'get-overflow-published-events')
* @param string $route Route pattern, matching the Laravel route exactly (same {param} names)
* @param string $httpMethod 'GET', 'POST', 'PUT', 'DELETE', etc.
* @param array $scopes Scope URIs to associate (e.g. SummitScopes::ReadSummitData)
* @param array $authzGroups Group slugs to associate (e.g. IGroup::SuperAdmins)
* @param bool $active
* @param bool $allowCors
* @param bool $allowCredentials
* @return void
*/
protected function registerEndpoint(
string $apiName,
string $endpointName,
string $route,
string $httpMethod,
array $scopes = [],
array $authzGroups = [],
bool $active = true,
bool $allowCors = true,
bool $allowCredentials = true
): void {
$this->addSql($this->insertEndpoint($apiName, $endpointName, $route, $httpMethod, $active, $allowCors, $allowCredentials));

foreach ($scopes as $scope) {
$this->addSql($this->insertEndpointScope($apiName, $endpointName, $scope));
}

foreach ($authzGroups as $group) {
$this->addSql($this->insertEndpointAuthzGroup($apiName, $endpointName, $group));
}
}

/**
* Register a batch of endpoints via registerEndpoint(). Each entry is an assoc array
* shaped like ApiEndpointsSeeder's endpoint definitions:
* ['name' => ..., 'route' => ..., 'http_method' => ..., 'scopes' => [...], 'authz_groups' => [...]].
*
* @param string $apiName
* @param array $endpoints
* @return void
*/
protected function registerEndpoints(string $apiName, array $endpoints): void
{
foreach ($endpoints as $endpoint) {
$this->registerEndpoint(
$apiName,
$endpoint['name'],
$endpoint['route'],
$endpoint['http_method'],
$endpoint['scopes'] ?? [],
$endpoint['authz_groups'] ?? [],
$endpoint['active'] ?? true,
$endpoint['allow_cors'] ?? true,
$endpoint['allow_credentials'] ?? true
);
}
}

/**
* Remove one endpoint. api_endpoints has ON DELETE CASCADE to endpoint_api_scopes and
* endpoint_api_authz_groups (verified against the config DB schema), so deleting the
* endpoint row alone is sufficient to remove its associations too.
*
* @param string $apiName
* @param string $endpointName
* @return void
*/
protected function unregisterEndpoint(string $apiName, string $endpointName): void
{
$this->addSql($this->deleteEndpoint($apiName, $endpointName));
}

/**
* Remove a batch of endpoints via unregisterEndpoint().
*
* @param string $apiName
* @param array $endpointNames
* @return void
*/
protected function unregisterEndpoints(string $apiName, array $endpointNames): void
{
foreach ($endpointNames as $endpointName) {
$this->unregisterEndpoint($apiName, $endpointName);
}
}
}
69 changes: 69 additions & 0 deletions database/migrations/config/Version20260715120000.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php namespace Database\Migrations\Config;
/**
* Copyright 2026 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/

use App\Security\SummitScopes;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Seed the get-overflow-published-events endpoint.
*
* Adds:
* - 1 api_scopes row (ReadOverflowEvents) - dedicated scope so this endpoint's Mux
* playback tokens are not gated by the broad ReadSummitData/ReadAllSummitData scopes
* shared by unrelated read endpoints
* - 1 api_endpoints row (get-overflow-published-events)
* - 1 endpoint_api_scopes association (ReadOverflowEvents)
*
* All INSERTs are idempotent via WHERE NOT EXISTS.
*/
final class Version20260715120000 extends AbstractMigration
{
use APIEndpointsMigrationHelper;

private const API_NAME = 'summits';
private const ENDPOINT_NAME = 'get-overflow-published-events';
private const ENDPOINT_ROUTE = '/api/v1/summits/{id}/events/all/published/occupancy/overflow';

public function getDescription(): string
{
return 'Seed get-overflow-published-events endpoint with a dedicated read scope.';
}

public function up(Schema $schema): void
{
$this->addSql($this->insertApiScope(
self::API_NAME,
SummitScopes::ReadOverflowEvents,
'Read Summit Overflow Events Data',
'Grants read only access to published summit events currently in OVERFLOW occupancy, including overflow streaming URLs and tokens'
));

$this->registerEndpoint(
self::API_NAME,
self::ENDPOINT_NAME,
self::ENDPOINT_ROUTE,
'GET',
[
SummitScopes::ReadOverflowEvents,
]
);
}

public function down(Schema $schema): void
{
$this->unregisterEndpoint(self::API_NAME, self::ENDPOINT_NAME);
$this->addSql($this->deleteApiScopes(self::API_NAME, [SummitScopes::ReadOverflowEvents]));
}
}
8 changes: 8 additions & 0 deletions database/seeders/ApiEndpointsSeeder.php
Original file line number Diff line number Diff line change
Expand Up @@ -4254,6 +4254,14 @@ private function seedSummitEndpoints()
SummitScopes::ReadAllSummitData
],
],
[
'name' => 'get-overflow-published-events',
'route' => '/api/v1/summits/{id}/events/all/published/occupancy/overflow',
'http_method' => 'GET',
'scopes' => [
SummitScopes::ReadOverflowEvents,
],
],
[
'name' => 'get-schedule-empty-spots',
'route' => '/api/v1/summits/{id}/events/published/empty-spots',
Expand Down
5 changes: 5 additions & 0 deletions database/seeders/ApiScopesSeeder.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ private function seedSummitScopes()
'short_description' => 'Get All Summits Data',
'description' => 'Grants read only access for All Summits Data',
],
[
'name' => SummitScopes::ReadOverflowEvents,
'short_description' => 'Read Summit Overflow Events Data',
'description' => 'Grants read only access to published summit events currently in OVERFLOW occupancy, including overflow streaming URLs and tokens',
],
[
'name' => SummitScopes::MeRead,
'short_description' => 'Get own summit member data',
Expand Down
Loading
Loading