feat: add per-activity CFP reopen, an admin-set time-boxed submission override - #581
feat: add per-activity CFP reopen, an admin-set time-boxed submission override#581caseylocker wants to merge 2 commits into
Conversation
… override
A summit admin can reopen CFP submission for a single presentation for a
chosen window (default 24h, ceiling 168h), letting the speaker edit that one
talk through the existing submission flow after the selection plan's window
has closed. Expiry is passive: no cron, no cleanup job.
Three nullable columns on Presentation store the grant (raw hours, stamp date,
granting member). The deadline is derived on read and never stored, so the
duration and the window end cannot drift out of lockstep. A new
isSubmissionReopened() predicate gates on that derived deadline plus plan
invariants (assigned plan, enabled, submission window already ended), and is
folded into isSubmissionClosed() as an early return so both delete guards
honor a reopen with no change at either call site.
Implemented in eight steps:
1. Presentation gains SubmissionReopenedHours, SubmissionReopenedDate and
SubmissionReopenedByID plus the derived accessor, the predicate and the
isSubmissionClosed() fold, with the schema migration.
2. updatePresentationSubmission and completePresentationSubmission relax only
their isSubmissionOpen() condition. The adjacent IsEnabled() and
isAllowedMember() checks stay enforced, so a plan disabled after a grant
still refuses.
3. New PresentationSubmissionReopenService owns the whole hours rule, default
and ceiling together, and rejects a grant on a plan that could never honor
it. closeNow() is deliberately exempt so a stale grant is always clearable.
4. Two admin-only endpoints, PUT and DELETE on
summits/{id}/presentations/{presentation_id}/submission-period/reopen.
5. Endpoint registration in both the seeder and a config migration. The deploy
flow does not re-run seeders, and an unregistered route returns 400 before
the controller runs.
6. submission_reopened_until serialized as an epoch on the Submission and
Admin subclasses only, with the by-fields Admin-only. All names are added to
$allowed_fields or they are dropped from any response omitting fields=.
7. The two speaker presentation-list endpoints now request the Submission
serializer, since PagingResponse::toArray() defaults to Public and the CFP
portal table would otherwise never see the field.
8. 91 tests. 40 integration tests across three files cover the model
predicates, the endpoints, acceptance behavior under an active grant, and
everything requiring a non-admin identity. 51 database-free unit tests
cover the derived deadline and the predicate matrix, including half-states
and boundaries, plus every validation branch of the service, one of which
is unreachable over HTTP and has no other coverage. All five test files are
registered in the CI matrix, since no job covers the tests/ root and
tests/Unit/Models was not previously sharded.
Decisions that look wrong without context:
- The new routes carry no auth.user middleware and register no authz groups.
UserAuthEndpoint returns 403 when an endpoint's required group list is empty,
so auth.user would reject every request. The controller performs the summit
aware admin check instead, matching the media upload routes.
- Serialization uses SerializerType_Private, not _Admin. Presentation has no
Admin key and an unknown type silently falls back to Public, which would
strip the new fields.
- Scopes are the OR trio WriteSummitData, WriteEventData and
WritePresentationData. Validation is any of, so the trio admits existing Show
Admin tokens; narrowing to WritePresentationData alone would 403 every
current caller.
- submission_reopened_by renders "Full Name (email)" as a plain Admin-only
scalar rather than the usual id plus expand idiom, because the expand switch
lives in the base PresentationSerializer and a case there would be reachable
from the Public and Submission variants.
ref: https://app.clickup.com/t/86bba82ch
Co-Authored-By: Claude <noreply@anthropic.com>
📝 WalkthroughWalkthroughThis change adds configurable, administrator-controlled reopening windows for presentation submissions. It persists reopening metadata, validates plan and time conditions, exposes reopen and close APIs, updates serializers, and adds model, service, API, authorization, and CI coverage. ChangesPresentation submission reopening
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
Suggested reviewers: Sequence Diagram(s)sequenceDiagram
participant Administrator
participant PresentationAPI
participant ReopenService
participant Presentation
Administrator->>PresentationAPI: Reopen submission period
PresentationAPI->>ReopenService: Validate and reopen presentation
ReopenService->>Presentation: Persist reopening grant
Presentation-->>PresentationAPI: Return serialized presentation
Administrator->>PresentationAPI: Close submission period
PresentationAPI->>ReopenService: Clear reopening grant
ReopenService->>Presentation: Persist closure
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
📘 OpenAPI / Swagger preview ➡️ https://OpenStackweb.github.io/summit-api/openapi/pr-581/ This page is automatically updated on each push to this PR. |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (5)
app/Http/Controllers/Apis/Protected/Summit/OAuth2PresentationApiController.php (1)
594-596: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valuePass the request serialization parameters.
Every other action in this controller forwards
SerializerUtils::getExpand(),getFields(), andgetRelations()toserialize(). This call passes none, soexpandandfieldsquery parameters are ignored for this endpoint only. The inline comment explains the serializer type choice but not the omitted arguments.♻️ Proposed change
return $this->updated(SerializerRegistry::getInstance()->getSerializer( $presentation, SerializerRegistry::SerializerType_Private - )->serialize()); + )->serialize( + SerializerUtils::getExpand(), + SerializerUtils::getFields(), + SerializerUtils::getRelations() + ));🤖 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/Http/Controllers/Apis/Protected/Summit/OAuth2PresentationApiController.php` around lines 594 - 596, Update the presentation serialization call in the controller action to pass SerializerUtils::getExpand(), getFields(), and getRelations() as the request serialization parameters, matching the other actions in this controller while preserving SerializerType_Private.config/cfp.php (1)
21-23: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider guarding against
default_reopen_hoursgreater thanmax_reopen_hours.The two values are independent. If an operator sets
CFP_DEFAULT_REOPEN_HOURSaboveCFP_MAX_REOPEN_HOURS, then every reopen request that omitshoursfails validation inPresentationSubmissionReopenService::reopen. A clamp at resolution time, or a startup check, prevents this silent misconfiguration.🤖 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 `@config/cfp.php` around lines 21 - 23, Validate the resolved CFP reopen-hour settings in config/cfp.php so default_reopen_hours cannot exceed max_reopen_hours, preferably clamping the default to the maximum while preserving the existing environment defaults. Ensure PresentationSubmissionReopenService::reopen receives a valid default for requests that omit hours.tests/PresentationReopenApiTest.php (1)
124-132: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDuplicated entity-manager reload helper clears the whole identity map. Both test classes carry a copy of the same refresh block, and both call
self::$em->clear().clear()detaches every managed entity, including the fixture statics thatclearSummitTestData()uses duringtearDown(). Extract one shared helper and confirm the teardown path still holds managed entities.
tests/PresentationReopenApiTest.php#L124-L132: move the refresh block into a shared trait or base helper, and replaceclear()with a targetedrefresh()on the presentation if the teardown depends on managed fixtures.tests/PresentationReopenAuthzTest.php#L187-L195: delete the local copy and call the shared helper with the presentation id.🤖 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 `@tests/PresentationReopenApiTest.php` around lines 124 - 132, In tests/PresentationReopenApiTest.php#L124-L132, extract the duplicated reloadPresentation entity-manager logic into a shared trait or base helper, replace the full self::$em->clear() with a targeted refresh of the presentation, and verify clearSummitTestData() still has its managed teardown fixtures. In tests/PresentationReopenAuthzTest.php#L187-L195, remove the local helper copy and call the shared helper using the presentation id.app/Services/Model/Imp/PresentationSubmissionReopenService.php (1)
77-88: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
closeNowaccepts$actorbut never uses it.The
useclause omits$actor, so the acting administrator is discarded. The reopen path records the granting member, but the close path records nothing. Either log the actor for the audit trail, or remove the parameter from the interface and the two call sites. Logging is the smaller change and keeps the administrative action traceable.♻️ Proposed change: record the acting administrator
+use Illuminate\Support\Facades\Log;public function closeNow(Summit $summit, int $presentation_id, Member $actor): void { - $this->tx_service->transaction(function () use ($summit, $presentation_id) { + $this->tx_service->transaction(function () use ($summit, $presentation_id, $actor) { $presentation = $summit->getEvent($presentation_id); if (!$presentation instanceof Presentation) throw new EntityNotFoundException(sprintf("Presentation %s not found.", $presentation_id)); + Log::info(sprintf( + "PresentationSubmissionReopenService::closeNow presentation %s closed by member %s", + $presentation_id, $actor->getId() + )); // no plan-state checks on purpose: a stale grant must always be clearable $presentation->closeSubmissionNow(); }); }🤖 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/PresentationSubmissionReopenService.php` around lines 77 - 88, Update closeNow to capture the provided actor in its transaction closure and record that Member through the presentation’s existing close/audit mechanism, matching how the reopen path records its granting member. Keep the $actor parameter and both call sites unchanged.Source: Linters/SAST tools
.github/workflows/push.yml (1)
72-72: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRename or split the
PresentationMediaUploadssuite entry.This entry now runs six files, three of which are reopen tests. The suite name and the uploaded artifact name
results_PresentationMediaUploadsno longer describe the contents. The six files also run serially in one matrix slot while other slots are free. Add a separatePresentationReopenentry for the three new files.♻️ Proposed split
- - { name: "PresentationMediaUploads", filter: "tests/PresentationMediaUploadsTest.php tests/PresentationMediaUploadsVisibilityTest.php tests/PresentationSerializerCacheKeyTest.php tests/PresentationReopenModelTest.php tests/PresentationReopenApiTest.php tests/PresentationReopenAuthzTest.php" } + - { name: "PresentationMediaUploads", filter: "tests/PresentationMediaUploadsTest.php tests/PresentationMediaUploadsVisibilityTest.php tests/PresentationSerializerCacheKeyTest.php" } + - { name: "PresentationReopen", filter: "tests/PresentationReopenModelTest.php tests/PresentationReopenApiTest.php tests/PresentationReopenAuthzTest.php" }🤖 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 @.github/workflows/push.yml at line 72, Split the suite configuration entry in the workflow matrix: keep the three media-upload/serializer tests under PresentationMediaUploads, and add a separate PresentationReopen entry containing the three PresentationReopen* test files. This ensures the suite and generated artifact names accurately reflect their contents and allows the reopen tests to run in their own matrix slot.
🤖 Prompt for all review comments with 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.
Inline comments:
In
`@app/Http/Controllers/Apis/Protected/Summit/OAuth2PresentationApiController.php`:
- Around line 556-560: Update the OpenAPI response declaration in the
presentation action to use Response::HTTP_OK instead of Response::HTTP_CREATED,
matching the 200 status returned by $this->updated(...).
In `@app/Services/ModelServicesProvider.php`:
- Around line 198-199: Add IPresentationSubmissionReopenService::class to the
provides() array in ModelServicesProvider, matching the deferred singleton
registration so the provider loads when this interface is resolved directly.
In `@tests/PresentationReopenApiTest.php`:
- Around line 462-473: Update
testReopenFieldsNeverAppearOnAPublicSerializedResponse to assert that
reopen(['hours' => 24]) succeeds before serializing and checking field absence.
Also add the equivalent success assertion to
testByFieldsAreAbsentFromTheSubmissionSerializer, preserving their existing
serializer assertions.
---
Nitpick comments:
In @.github/workflows/push.yml:
- Line 72: Split the suite configuration entry in the workflow matrix: keep the
three media-upload/serializer tests under PresentationMediaUploads, and add a
separate PresentationReopen entry containing the three PresentationReopen* test
files. This ensures the suite and generated artifact names accurately reflect
their contents and allows the reopen tests to run in their own matrix slot.
In
`@app/Http/Controllers/Apis/Protected/Summit/OAuth2PresentationApiController.php`:
- Around line 594-596: Update the presentation serialization call in the
controller action to pass SerializerUtils::getExpand(), getFields(), and
getRelations() as the request serialization parameters, matching the other
actions in this controller while preserving SerializerType_Private.
In `@app/Services/Model/Imp/PresentationSubmissionReopenService.php`:
- Around line 77-88: Update closeNow to capture the provided actor in its
transaction closure and record that Member through the presentation’s existing
close/audit mechanism, matching how the reopen path records its granting member.
Keep the $actor parameter and both call sites unchanged.
In `@config/cfp.php`:
- Around line 21-23: Validate the resolved CFP reopen-hour settings in
config/cfp.php so default_reopen_hours cannot exceed max_reopen_hours,
preferably clamping the default to the maximum while preserving the existing
environment defaults. Ensure PresentationSubmissionReopenService::reopen
receives a valid default for requests that omit hours.
In `@tests/PresentationReopenApiTest.php`:
- Around line 124-132: In tests/PresentationReopenApiTest.php#L124-L132, extract
the duplicated reloadPresentation entity-manager logic into a shared trait or
base helper, replace the full self::$em->clear() with a targeted refresh of the
presentation, and verify clearSummitTestData() still has its managed teardown
fixtures. In tests/PresentationReopenAuthzTest.php#L187-L195, remove the local
helper copy and call the shared helper using the presentation id.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 621f037a-3017-4fc7-b8e5-0277ea4d1d97
📒 Files selected for processing (21)
.env.example.github/workflows/push.ymlapp/Http/Controllers/Apis/Protected/Summit/OAuth2PresentationApiController.phpapp/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSpeakersApiController.phpapp/ModelSerializers/Summit/Presentation/AdminPresentationSerializer.phpapp/ModelSerializers/Summit/Presentation/SubmissionPresentationSerializer.phpapp/Models/Foundation/Summit/Events/Presentations/Presentation.phpapp/Services/Model/IPresentationSubmissionReopenService.phpapp/Services/Model/Imp/PresentationService.phpapp/Services/Model/Imp/PresentationSubmissionReopenService.phpapp/Services/ModelServicesProvider.phpconfig/cfp.phpdatabase/migrations/config/Version20260807130000.phpdatabase/migrations/model/Version20260807120000.phpdatabase/seeders/ApiEndpointsSeeder.phproutes/api_v1.phptests/PresentationReopenApiTest.phptests/PresentationReopenAuthzTest.phptests/PresentationReopenModelTest.phptests/Unit/Models/PresentationSubmissionReopenTest.phptests/Unit/Services/PresentationSubmissionReopenServiceTest.php
Three review follow ups on the per-activity CFP reopen endpoints. The reopen response now forwards expand, fields and relations to serialize(), matching every sibling action in this controller. The Private serializer type is unchanged, so the Admin only fields stay Admin only. A resolved default_reopen_hours above max_reopen_hours is now clamped to the ceiling instead of rejecting every request that omits hours. That is a config error the caller can neither see nor fix, so failing them is the wrong behaviour. An explicitly supplied hours is still validated strictly and never clamped, which the existing out of range test continues to cover. testReopenFieldsNeverAppearOnAPublicSerializedResponse now asserts the reopen returned 201 and that the grant persisted before asserting the three fields are absent from the Public payload. Without that the absence assertions held vacuously: a failed reopen leaves no grant, so Public omits the fields whether or not the mappings are correctly scoped to the Admin subclass. ref: https://app.clickup.com/t/86bba82ch Co-Authored-By: Claude <noreply@anthropic.com>
|
Dispositions for the five non-blocking items in the review body, since they are not inline threads. Two taken, three declined with reasons. The two inline bug reports are refuted in their own threads. Taken, in d673aa7:
Declined:
The docstring coverage warning is not a PR specific correctness finding. |
|
📘 OpenAPI / Swagger preview ➡️ https://OpenStackweb.github.io/summit-api/openapi/pr-581/ This page is automatically updated on each push to this PR. |
There was a problem hiding this comment.
Pull request overview
Adds a per-presentation, time-boxed “CFP submission reopen” override that allows summit admins to temporarily re-enable edits for a single presentation after the selection plan’s submission window has ended, without persisting the derived deadline.
Changes:
- Adds nullable reopen-grant columns to
Presentation(hours, stamp date, granting member) plus derived deadline + predicates, and folds the predicate intoisSubmissionClosed(). - Introduces admin-only reopen/close endpoints and wires serializers so admin reads include grant metadata while speaker submission flows receive only the derived deadline.
- Adds comprehensive integration + unit test coverage and updates CI sharding so the new tests run in
push.yml.
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
app/Models/Foundation/Summit/Events/Presentations/Presentation.php |
Stores reopen grant fields, derives submission_reopened_until, adds isSubmissionReopened() and folds into isSubmissionClosed(). |
database/migrations/model/Version20260807120000.php |
Adds DB columns/index/FK for reopen grant on Presentation. |
config/cfp.php |
Adds configurable max/default reopen hours. |
app/Services/Model/IPresentationSubmissionReopenService.php |
Defines service contract for reopen/close operations. |
app/Services/Model/Imp/PresentationSubmissionReopenService.php |
Implements reopen/close logic (config-based hours rules + plan invariants + summit scoping). |
app/Services/ModelServicesProvider.php |
Registers the new reopen service in the container. |
app/Http/Controllers/Apis/Protected/Summit/OAuth2PresentationApiController.php |
Adds admin-only reopen/close endpoints with controller-level summit-admin authorization. |
routes/api_v1.php |
Registers the new reopen/close routes. |
database/migrations/config/Version20260807130000.php |
Registers new API endpoints in config DB so OAuth validation recognizes the routes. |
database/seeders/ApiEndpointsSeeder.php |
Seeds endpoint definitions for fresh installs. |
app/Services/Model/Imp/PresentationService.php |
Allows update/complete when plan window is closed but a valid reopen grant is active. |
app/ModelSerializers/Summit/Presentation/AdminPresentationSerializer.php |
Exposes reopen fields for admin/private presentation serialization. |
app/ModelSerializers/Summit/Presentation/SubmissionPresentationSerializer.php |
Exposes submission_reopened_until for submission serializer output. |
app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSpeakersApiController.php |
Switches speaker presentation list endpoints to Submission serializer type so the derived deadline is returned. |
tests/PresentationReopenApiTest.php |
Integration coverage for reopen/close endpoints, serialization, and reopened edit/complete acceptance paths. |
tests/PresentationReopenAuthzTest.php |
Integration coverage for non-admin authorization (403s), delete-guard behavior, authorship invariants, and non-leakage of admin-only fields. |
tests/PresentationReopenModelTest.php |
Integration coverage for model predicate/deadline behavior and isSubmissionClosed() fold behavior. |
tests/Unit/Models/PresentationSubmissionReopenTest.php |
Unit coverage for full deadline/predicate state matrix and boundary conditions. |
tests/Unit/Services/PresentationSubmissionReopenServiceTest.php |
Unit coverage for service validation branches and config-driven default/ceiling behavior. |
.github/workflows/push.yml |
Adds new test files/paths to CI matrix so they execute. |
.env.example |
Documents new CFP reopen environment variables. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
ref: https://app.clickup.com/t/86bba82ch
Spec:
sds/per-activity-cfp-reopen.mdin the ftn-docsnsklz vault, onmain(mergedbd2c2c3), sections 3, 4 and 7.What this does
A summit admin can reopen CFP submission for a single presentation for a chosen window (default 24h, ceiling 168h), so the speaker can edit that one talk through the existing submission flow after the selection plan's window has closed. Expiry is passive: no cron, no cleanup job.
This is step 1 of 3. The call-for-presentations and summit-admin work are ClickUp
86bba82phand86bba82y3. Until step 2 ships, nothing a speaker can see changes.How it works
Three nullable columns on
Presentationstore the grant: raw hours, stamp date, granting member. The deadline is derived on read and never stored, so the granted duration and the window end cannot drift out of lockstep.A new
isSubmissionReopened()predicate gates on that derived deadline plus plan invariants (assigned plan, enabled, submission window already ended). It is folded intoisSubmissionClosed()as an early return, which covers both delete guards with no change at either call site, and it is OR'd into the twoisSubmissionOpen()checks on update and complete. Two admin-only endpoints stamp and clear the grant.The plan invariants are not optional. A deadline-only check would grant edits before the CFP ever opened, and via the
isSubmissionClosed()fold it would open speaker deletes on a plan disabled after the grant, because the delete path never re-checksIsEnabled()itself.Notes for reviewers
Seven things in this diff look like defects but are deliberate. Each cost a review round to establish, so they are recorded here rather than rediscovered.
The new routes carry no
auth.usermiddleware, and the registered endpoints carry no authz groups.UserAuthEndpoint::handle()returns 403 when an endpoint's required group list is empty, soauth.userwould reject every request. The controller performs the summit aware admin check instead, matching the media upload routes.Serialization uses
SerializerType_Private, not_Admin. Presentation has noAdminkey, and an unknown type silently falls back to Public, which would strip every field these endpoints exist to set.Scopes are the OR trio
WriteSummitData,WriteEventData,WritePresentationData. Validation is any of, so the trio admits existing Show Admin tokens. Narrowing toWritePresentationDataalone would 403 every current caller.submission_reopened_byrenders "Full Name (email)" as a plain Admin-only scalar rather than the repo's usual id plus expand idiom. The SDS considered that idiom and rejected it: the expand switch lives in the basePresentationSerializer, so a case added there is reachable from the Public and Submission variants, which is the exact leak the Admin-only design prevents. Show Admin reads the field straight off thegetEventpayload with no expand. The rationale is also in a comment onPresentation::getSubmissionReopenedByNice().The two speaker presentation-list endpoints previously serialized Public and now serialize Submission, which also selects Admin serializers for the nested
created_by,updated_by,moderatorandspeakersrelations. This grants no new access:getPresentationSubmissionalready serializes the same fields for the same audience, gated by the samememberCanEditcheck (creator, moderator, or assigned speaker). The switch itself is what the SDS mandates, sincePagingResponse::toArray()defaults to Public and the CFP portal table would otherwise never receive the new field.The service's
$hours < 1check is unreachable over HTTP, because the endpoint validatessometimes|integer|min:1and refuses first. It is kept deliberately: it is the only guard for a non HTTP caller, and a persisted non positive value would makegetSubmissionReopenedUntil()throw on every read, sincenew \DateInterval('PT-1H')is invalid.A misconfigured default is clamped, while an explicit out of range value is refused. If
default_reopen_hoursis configured abovemax_reopen_hours, the resolved default is clamped down to the ceiling rather than rejected. An explicitly suppliedhoursabove the ceiling still gets a 412. The asymmetry is deliberate: a configuration error the caller can neither see nor fix should not surface to an admin as an unexplainable refusal of a request that supplied nothing, whereas a caller who names an out of range value should be told. Covered bytestMisconfiguredDefaultAboveMaxIsClampedRatherThanRefused.Two further points on the design:
addSql()on purpose. Mixing the DoctrineBuilderschema diff withaddSql()reorders them,addSql()first, which would execute the foreign key before its column exists.Deploy notes
The foreign key addition copies the table. On MySQL 8.0 an in place foreign key addition requires
foreign_key_checksto be disabled; otherwise it rebuilds.Presentationis a large production table, so run this migration in a low traffic window and monitor lock time. The three nullable columns are combined into a singleALTER, and the explicit index on the FK column is the index MySQL would create anyway.Do not blind-rerun the model migration if it fails partway. The three DDL statements commit separately, so a failure at the index or foreign key step leaves the added columns in place while the migration is not recorded as complete, and a naive retry then fails on duplicate schema objects. Check actual schema state before retrying.
Migrations use
--em=configand--em=model_write. There is nomodelentity manager;--em=modelsilently finds zero migrations and reports "already at latest".Safe rollback is application only. The columns are additive and inert when null. Rolling the schema back destroys live grants and the actor and date audit trail, so prefer rolling back the application. If a schema rollback is unavoidable, confirm all serving instances run the old application first.
Endpoint registration evidence
Against the config database, before the migration:
After
php artisan doctrine:migrations:migrate --em=config:Both route strings byte match the Laravel routes as printed by
route:list.Testing
92 new tests across five files, 40 integration and 52 unit:
tests/PresentationReopenModelTest.phpisSubmissionClosed()foldtests/PresentationReopenApiTest.phptests/PresentationReopenAuthzTest.phptests/Unit/Models/PresentationSubmissionReopenTest.phptests/Unit/Services/PresentationSubmissionReopenServiceTest.phpIntegration shard:
OK (54 tests, 351 assertions). Unit files:OK (29 tests, 59 assertions)andOK (23 tests, 77 assertions).All five files are registered in the
push.ymlmatrix. No CI job covers thetests/root, andtests/Unit/Models/was not previously sharded either, so a test in either location runs nowhere unless it is named.The two unit files are genuinely database-free and framework-free: they extend plain
PHPUnit\Framework\TestCase, bind a real config repository into a bare container for the facade, and mockITransactionServiceso the transaction callback still executes. 52 tests run in about 120ms, against roughly 50 seconds for the integration shard. They exist because several cases are impractical or impossible to reach otherwise: the expiry boundary, the half-states where hours is set with a null date or the reverse, and the service's$hours < 1guard, which the endpoint'smin:1rule makes unreachable over HTTP so the unit test is its only coverage.Two notes on test rigour, both of which changed how these were written:
ProtectedApiTestCaseproduces a global admin through two independent levers, the persisted group and the access token stub's default IdP groups, and both delete guards short circuit for a global admin. Both levers are defeated and a canary asserts it, because an admin run 403 suite proves nothing.DoctrineMiddlewarecloses the model entity manager after every request and singleton repositories pin the manager they were first resolved with, so the second write is silently dropped while still returning success. Each affected test asserts its precondition landed in the database before issuing the request under test.The full suite is green in CI, including the
tests/oauth2/shard at 1069 tests. Local full-suite runs need two setup steps that CI does for itself: freshly created test databases (php artisan db:create_initial_dbfor both schemas, aspush.ymldoes), andmax_connectionsabove MySQL's default of 151, which the suite exceeds. Without those, a local run reports failures that are entirely environmental.Not in scope
86bba832v.86bba8388.Three pre-existing defects were found while implementing this and deliberately left untouched, so they are not mistaken for regressions or for oversights:
SelectionPlan::areFieldsEqualcompares its first argument to itself, so the scalar branch always reports equal and the allowed editable question guard never fires for scalar fields.PresentationType::isAreSpeakersMandatory()ignores theare_speakers_mandatorycolumn and returnsmin_speakers > 0, so calling the setter has no effect.getPresentationMediaUploadsis registered twice inroutes/api_v1.php.Scope compatibility with Show Admin, checked
Scope validation is any of, and the endpoints accept
WriteSummitData(summits/write),WriteEventData(summits/write-event) orWritePresentationData(summits/write-presentation). summit-admin's requested scopes, per its.env.example, are:Two of the three, so Show Admin is admitted without any client change.
This is also why the trio matters rather than being belt and braces. summit-admin holds
write-presentation-materials, notwrite-presentation, so registeringWritePresentationDataalone would have returned 403 to every Show Admin caller in production while passing every test here, because the test token does carry it.One residual check for whoever deploys:
.env.exampleis the template, and a deployed.envcan drift from it. Worth confirming the deployed summit-admin.envstill listssummits/writebefore the control in86bba82y3goes live.