Skip to content
Merged
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 @@ -1085,9 +1085,48 @@ public function addEvent($summit_id)
*/
public function updateEvent($summit_id, $event_id)
{
return $this->processRequest(function() use($summit_id, $event_id){
return $this->_updateEvent($summit_id, $event_id);
}

#[OA\Put(
path: '/api/v1/summits/{id}/events/{event_id}/draft',
operationId: 'updateDraftEvent',
summary: 'Update an existing draft event',
description: 'Updates an existing draft event for a specific summit.',
security: [['summit_events_api_oauth2' => [SummitScopes::WriteSummitData, SummitScopes::WriteEventData]]],
x: ['required-groups' => [IGroup::SuperAdmins, IGroup::Administrators, IGroup::SummitAdministrators, IGroup::TrackChairsAdmins]],
tags: ['Summit Events'],
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: 'event_id', in: 'path', required: true, description: 'Event ID', schema: new OA\Schema(type: 'integer')),
],
requestBody: new OA\RequestBody(
required: true,
content: new OA\JsonContent(ref: '#/components/schemas/UpdateSummitEventRequest')
),
responses: [
new OA\Response(response: Response::HTTP_OK, description: 'Draft event updated successfully', content: new OA\JsonContent(ref: '#/components/schemas/SummitEvent')),
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_PRECONDITION_FAILED, description: 'Validation error'),
new OA\Response(response: Response::HTTP_INTERNAL_SERVER_ERROR, description: 'Server Error'),
]
)]
/**
* @param $summit_id
* @param $event_id
* @return mixed
*/
public function updateDraftEvent($summit_id, $event_id)
{
return $this->_updateEvent($summit_id, $event_id, true);
}

Log::debug(sprintf("OAuth2SummitEventsApiController::updateEvent summit id %s event id %s", $summit_id, $event_id));
private function _updateEvent($summit_id, $event_id, bool $saveAsIncomplete = false)
{
return $this->processRequest(function() use($summit_id, $event_id, $saveAsIncomplete) {

$summit = SummitFinderStrategyFactory::build($this->repository, $this->resource_server_context)->find($summit_id);
if (is_null($summit))
Expand All @@ -1106,42 +1145,30 @@ public function updateEvent($summit_id, $event_id)

$payload = $this->getJsonData();

// Creates a Validator instance and validates the data.
$rules = $isAdmin ? SummitEventValidationRulesFactory::build($payload, true) : null;
if(is_null($rules)){
if (is_null($rules)) {
$rules = $isTrackChair ? SummitEventValidationRulesFactory::buildForTrackChair($payload, true) : null;
}

if(is_null($rules))
if (is_null($rules))
return $this->error403();


$payload = $this->getJsonPayload($rules, true);

$fields = [
'title',
'description',
'social_summary',
];
$fields = ['title', 'description', 'social_summary'];

if($isAdmin) {
Log::debug(sprintf("OAuth2SummitEventsApiController::updateEvent summit id %s event id %s updating event", $summit_id, $event_id));
$event = $this->service->updateEvent($summit, $event_id, HTMLCleaner::cleanData($payload, $fields));
}
else{
Log::debug(sprintf("OAuth2SummitEventsApiController::updateEvent summit id %s event id %s updating duration", $summit_id, $event_id));
if ($isAdmin) {
$event = $this->service->updateEvent($summit, $event_id, HTMLCleaner::cleanData($payload, $fields), true, $saveAsIncomplete);
} else {
$event = $this->service->updateDuration($payload, $summit, $event);
}

return $this->ok(SerializerRegistry::getInstance()->getSerializer($event, $this->getSerializerType())
->serialize
(
->serialize(
SerializerUtils::getExpand(),
SerializerUtils::getFields(),
SerializerUtils::getRelations(),
[
'current_user' => $current_member
]
['current_user' => $current_member]
)
);
});
Expand Down
3 changes: 2 additions & 1 deletion app/Services/Model/ISummitService.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,10 @@ public function addEvent(Summit $summit, array $data);
* @param int $event_id
* @param array $data
* @param bool $trigger_data_update
* @param bool $saveAsIncomplete
* @return SummitEvent
*/
public function updateEvent(Summit $summit, $event_id, array $data, bool $trigger_data_update = true);
public function updateEvent(Summit $summit, $event_id, array $data, bool $trigger_data_update = true, bool $saveAsIncomplete = false);

/**
* @param Summit $summit
Expand Down
52 changes: 31 additions & 21 deletions app/Services/Model/Imp/SummitService.php
Original file line number Diff line number Diff line change
Expand Up @@ -617,9 +617,9 @@ public function addEvent(Summit $summit, array $data)
* @param array $data
* @return SummitEvent
*/
public function updateEvent(Summit $summit, $event_id, array $data, bool $trigger_data_update = true)
public function updateEvent(Summit $summit, $event_id, array $data, bool $trigger_data_update = true, bool $saveAsIncomplete = false)
{
return $this->saveOrUpdateEvent($summit, $data, $event_id, $trigger_data_update);
return $this->saveOrUpdateEvent($summit, $data, $event_id, $trigger_data_update, $saveAsIncomplete);
}

/**
Expand Down Expand Up @@ -660,9 +660,9 @@ private function canPerformEventTypeTransition(SummitEventType $old_event_type,
* @return SummitEvent
* @throws Exception
*/
private function saveOrUpdateEvent(Summit $summit, array $data, $event_id = null, bool $trigger_data_update = true)
private function saveOrUpdateEvent(Summit $summit, array $data, $event_id = null, bool $trigger_data_update = true, bool $saveAsIncomplete = false)
{
return $this->tx_service->transaction(function () use ($summit, $data, $event_id, $trigger_data_update) {
return $this->tx_service->transaction(function () use ($summit, $data, $event_id, $trigger_data_update, $saveAsIncomplete) {

Log::debug
(
Expand Down Expand Up @@ -832,7 +832,7 @@ private function saveOrUpdateEvent(Summit $summit, array $data, $event_id = null
}
}

$this->saveOrUpdatePresentationData($event, $event_type, $data);
$this->saveOrUpdatePresentationData($event, $event_type, $data, $saveAsIncomplete);
$this->saveOrUpdateSummitGroupEventData($event, $event_type, $data);

if (!$event_type->isAllowsLocation())
Expand Down Expand Up @@ -881,17 +881,23 @@ private function saveOrUpdateSummitGroupEventData(SummitEvent $event, SummitEven
* @param SummitEvent $event
* @param SummitEventType $event_type
* @param array $data
* @param bool $saveAsIncomplete
* @throws EntityNotFoundException
* @throws ValidationException
*/
private function saveOrUpdatePresentationData(SummitEvent $event, SummitEventType $event_type, array $data)
private function saveOrUpdatePresentationData(SummitEvent $event, SummitEventType $event_type, array $data, bool $saveAsIncomplete = false)
{
if (!$event instanceof Presentation) return;

// if we are creating the presentation from admin, then
// we should mark it as received and complete
$event->setStatus(Presentation::STATUS_RECEIVED);
$event->setProgress(Presentation::PHASE_COMPLETE);
if ($saveAsIncomplete && $event->isPublished())
throw new ValidationException('Cannot save a published event as incomplete.');

if (!$saveAsIncomplete || $event->isNew()) {
Comment thread
romanetar marked this conversation as resolved.
// if we are creating the presentation from admin, then
// we should mark it as received and complete
$event->setStatus(Presentation::STATUS_RECEIVED);
$event->setProgress(Presentation::PHASE_COMPLETE);
}

// speakers

Expand All @@ -900,13 +906,15 @@ private function saveOrUpdatePresentationData(SummitEvent $event, SummitEventTyp
$shouldClearSpeakers = isset($data['speakers']) && count($data['speakers']) == 0;
$speakers = $data['speakers'] ?? [];

if ($event_type->isAreSpeakersMandatory()) {
if ($shouldClearSpeakers || ($event->isNew() && count($speakers) == 0))
throw new ValidationException('Speakers are mandatory.');
}
if (!$saveAsIncomplete || $event->isNew()) {
if ($event_type->isAreSpeakersMandatory()) {
if ($shouldClearSpeakers || ($event->isNew() && count($speakers) == 0))
throw new ValidationException('Speakers are mandatory.');
}

if ($shouldClearSpeakers) {
$event->clearSpeakers();
if ($shouldClearSpeakers) {
$event->clearSpeakers();
}
}
Comment on lines +909 to 918

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Draft updates ignore explicit speaker/moderator clears on existing presentations.

At Line 909 and Line 937, the (!$saveAsIncomplete || $event->isNew()) gate wraps both mandatory validation and the clear/unset mutations. For existing draft saves, speakers: [] and moderator_speaker_id: 0 are silently ignored, so the payload cannot persist those incomplete states.

🔧 Proposed fix
-            if (!$saveAsIncomplete || $event->isNew()) {
-                if ($event_type->isAreSpeakersMandatory()) {
-                    if ($shouldClearSpeakers || ($event->isNew() && count($speakers) == 0))
-                        throw new ValidationException('Speakers are mandatory.');
-                }
-
-                if ($shouldClearSpeakers) {
-                    $event->clearSpeakers();
-                }
-            }
+            if ($event_type->isAreSpeakersMandatory() && (!$saveAsIncomplete || $event->isNew())) {
+                if ($shouldClearSpeakers || ($event->isNew() && count($speakers) == 0))
+                    throw new ValidationException('Speakers are mandatory.');
+            }
+
+            if ($shouldClearSpeakers) {
+                $event->clearSpeakers();
+            }
-            if (!$saveAsIncomplete || $event->isNew()) {
-                if ($event_type->isModeratorMandatory()) {
-                    if ($shouldClearModerator || ($event->isNew() && $moderator_id == 0))
-                        throw new ValidationException('moderator_speaker_id is mandatory.');
-                }
-
-                if ($shouldClearModerator) $event->unsetModerator();
-            }
+            if ($event_type->isModeratorMandatory() && (!$saveAsIncomplete || $event->isNew())) {
+                if ($shouldClearModerator || ($event->isNew() && $moderator_id == 0))
+                    throw new ValidationException('moderator_speaker_id is mandatory.');
+            }
+
+            if ($shouldClearModerator) $event->unsetModerator();

Also applies to: 937-944

🤖 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/Services/Model/Imp/SummitService.php` around lines 909 - 918, The current
conditional (! $saveAsIncomplete || $event->isNew()) incorrectly prevents
explicit clears for existing presentations when saving drafts; move the mutation
(calls that clear/unset speakers and moderator like $event->clearSpeakers() and
the moderator unset logic) out of that gate so they run whenever
$shouldClearSpeakers or the moderator-clear flag is set, but keep the
mandatory-speaker validation (the isAreSpeakersMandatory() check and its throw)
inside the original (!$saveAsIncomplete || $event->isNew()) block; apply the
same change to the corresponding moderator handling around the code that unsets
moderator_speaker_id (the block referenced at lines ~937-944) so explicit empty
payloads persist for existing drafts.


if (count($speakers) > 0) {
Expand All @@ -926,12 +934,14 @@ private function saveOrUpdatePresentationData(SummitEvent $event, SummitEventTyp
$shouldClearModerator = isset($data['moderator_speaker_id']) && intval($data['moderator_speaker_id']) == 0;
$moderator_id = isset($data['moderator_speaker_id']) ? intval($data['moderator_speaker_id']) : 0;

if ($event_type->isModeratorMandatory()) {
if ($shouldClearModerator || ($event->isNew() && $moderator_id == 0))
throw new ValidationException('moderator_speaker_id is mandatory.');
}
if (!$saveAsIncomplete || $event->isNew()) {
if ($event_type->isModeratorMandatory()) {
if ($shouldClearModerator || ($event->isNew() && $moderator_id == 0))
throw new ValidationException('moderator_speaker_id is mandatory.');
}

if ($shouldClearModerator) $event->unsetModerator();
if ($shouldClearModerator) $event->unsetModerator();
}

if ($moderator_id > 0) {
$moderator = $this->speaker_repository->getById($moderator_id);
Expand Down
81 changes: 81 additions & 0 deletions database/migrations/config/Version20260609175051.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?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\Models\Foundation\Main\IGroup;
use App\Security\SummitScopes;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Seed the update-draft-event endpoint.
*
* Idempotent via WHERE NOT EXISTS in APIEndpointsMigrationHelper.
*/
final class Version20260609175051 extends AbstractMigration
{
use APIEndpointsMigrationHelper;

private const API_NAME = 'summits';
private const ENDPOINT_NAME = 'update-draft-event';
private const ENDPOINT_ROUTE = '/api/v1/summits/{id}/events/{event_id}/draft';

private const AUTH_GROUPS = [
IGroup::SuperAdmins,
IGroup::Administrators,
IGroup::SummitAdministrators,
IGroup::TrackChairsAdmins,
];

private const SCOPES = [
SummitScopes::WriteSummitData,
SummitScopes::WriteEventData
];

public function getDescription(): string
{
return 'Seed update-draft-event endpoint with scope and authz groups';
}

public function up(Schema $schema): void
{
$this->addSql($this->insertEndpoint(
self::API_NAME,
self::ENDPOINT_NAME,
self::ENDPOINT_ROUTE,
'PUT'
));
Comment thread
coderabbitai[bot] marked this conversation as resolved.

foreach (self::SCOPES as $scope) {
$this->addSql($this->insertEndpointScope(
self::API_NAME,
self::ENDPOINT_NAME,
$scope
));
}

foreach (self::AUTH_GROUPS as $groupSlug) {
$this->addSql($this->insertEndpointAuthzGroup(self::API_NAME, self::ENDPOINT_NAME, $groupSlug));
}
}

public function down(Schema $schema): void
{
foreach (self::AUTH_GROUPS as $groupSlug) {
$this->addSql($this->deleteEndpointAuthzGroup(self::API_NAME, self::ENDPOINT_NAME, $groupSlug));
}

$this->addSql($this->deleteScopesEndpoints(self::API_NAME, self::SCOPES));
$this->addSql($this->deleteEndpoint(self::API_NAME, self::ENDPOINT_NAME));
}
}
15 changes: 15 additions & 0 deletions database/seeders/ApiEndpointsSeeder.php
Original file line number Diff line number Diff line change
Expand Up @@ -4267,6 +4267,21 @@ private function seedSummitEndpoints()
IGroup::TrackChairsAdmins,
]
],
[
'name' => 'update-draft-event',
'route' => '/api/v1/summits/{id}/events/{event_id}/draft',
'http_method' => 'PUT',
'scopes' => [
SummitScopes::WriteSummitData,
SummitScopes::WriteEventData
],
'authz_groups' => [
IGroup::SuperAdmins,
IGroup::Administrators,
IGroup::SummitAdministrators,
IGroup::TrackChairsAdmins,
]
],
[
'name' => 'update-event-live-info',
'route' => '/api/v1/summits/{id}/events/{event_id}/live-info',
Expand Down
1 change: 1 addition & 0 deletions routes/api_v1.php
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,7 @@
});

Route::put('', ['middleware' => 'auth.user', 'uses' => 'OAuth2SummitEventsApiController@updateEvent']);
Route::put('/draft', ['middleware' => 'auth.user', 'uses' => 'OAuth2SummitEventsApiController@updateDraftEvent']);
Route::put('live-info', ['uses' => 'OAuth2SummitEventsApiController@updateEventLiveInfo']);
Route::delete('', ['middleware' => 'auth.user', 'uses' => 'OAuth2SummitEventsApiController@deleteEvent']);
Route::put('/publish', ['middleware' => 'auth.user', 'uses' => 'OAuth2SummitEventsApiController@publishEvent']);
Expand Down
12 changes: 11 additions & 1 deletion tests/InsertOrdersTestData.php
Original file line number Diff line number Diff line change
Expand Up @@ -139,4 +139,14 @@ protected static function InsertOrdersTestData()
self::$em->persist(self::$summit);
self::$em->flush();
}
}

protected static function clearOrdersTestData(): void
{
if (!is_null(self::$default_ticket_type) && !is_null(self::$default_ticket_type->getId())) {
DB::table('SummitTicketType')
->where('ID', self::$default_ticket_type->getId())
->update(['QuantitySold' => 0]);
self::$em->clear();
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
Loading
Loading