feat(marketplace): hands-on preview in a shared sandbox course - #8529
Open
LWS49 wants to merge 8 commits into
Open
feat(marketplace): hands-on preview in a shared sandbox course#8529LWS49 wants to merge 8 commits into
LWS49 wants to merge 8 commits into
Conversation
LWS49
force-pushed
the
lws49/feat-marketplace-pr7c-versioning-admin
branch
from
August 1, 2026 05:31
7524d9d to
ee1f875
Compare
LWS49
force-pushed
the
lws49/feat-marketplace-pr8a-preview-sandbox
branch
from
August 1, 2026 05:31
96c3763 to
e0c59f1
Compare
LWS49
force-pushed
the
lws49/feat-marketplace-pr7c-versioning-admin
branch
6 times, most recently
from
August 2, 2026 11:36
9676d6b to
95710d0
Compare
Base automatically changed from
lws49/feat-marketplace-pr7c-versioning-admin
to
lws49/feat-marketplace-pr1-foundation
August 2, 2026 14:04
LWS49
force-pushed
the
lws49/feat-marketplace-pr1-foundation
branch
2 times, most recently
from
August 2, 2026 16:44
bbd43f0 to
6eac0c4
Compare
Base automatically changed from
lws49/feat-marketplace-pr1-foundation
to
master
August 2, 2026 17:19
Adds `PreviewLaunchService` and the `launch_preview` endpoint: a previewer is enrolled as a manager of the singleton container course and handed the attempt url for the listing's served snapshot, on the preview host. No copy step — the snapshot the marketplace already serves is what a duplicate would give an adopter, so a preview cannot drift from the adopted copy. Also adds `Listing.serving_assessment?`, which the sandbox lock uses to tell a handed-out snapshot from the rest of the container, and moves the container's description into locales so the sandbox explainer can be written as HTML.
A rehearsal in the sandbox must not reach the outside world. Guarded at the three writers rather than their call sites, so a fourth cannot reintroduce it: - no graded-submission email (the previewer grades and publishes their own work, so it would land in their own inbox for a fake grade), - no task completion pushed to the previewer's real external LMS, - no `pending_staff_reply` flag, which in a container with no staff only ever surfaces as a nag on the Pending tab and the Comments badge.
Both crumb handles on any assessment page fetch `show`, and so does the page itself. One endpoint serving both means a breadcrumb costs the page's ~50 queries to render two strings — and made the marketplace sandbox's crumb allowance a licence to read the whole authoring surface. `show` now renders `crumb` for a `?crumb=true` request, skipping the two `before_action`s the full payload needs. Split on the request rather than on the viewer, so the payload stays the same for everyone.
A previewer is enrolled as a `manager` of the shared container course — the lowest role that can attempt, grade and publish. On the ordinary abilities that is also a licence to read and edit the whole course, and a subtractive allow-list can never be complete: it has to name every component, every custom verb and every collection action, and each new one arrives switched on. `ApplicationPreviewSandboxConcern` inverts it. On the preview instance a non-administrator reaches only what a controller explicitly claims via `preview_sandbox_accessible?`, mirroring how `publicly_accessible?` marks out the unauthenticated surface. A component added tomorrow is denied without anyone remembering to deny it. Alongside it, `restrict_preview_course_reads` revokes the manager grants that would otherwise reach another previewer's submission, the aggregate gradebook and the roster of everyone who has ever previewed here. The lock is per-VIEWER: a system administrator curates the container from inside it, so the sandbox stays navigable for them.
The sandbox exists to rehearse the one assessment the previewer opened, and the lock denies everything else — so leaving live links around only offers them a 403. Sidebar items and breadcrumbs render as inert text instead. Home is withheld separately: it is the one sidebar entry that does not come from the `sidebar` payload, so the jbuilder's de-link never reached it. Gated on `isPreviewRestricted`, not `isPreview`: a system administrator curates the container from inside it and keeps working links. The same flag also keeps them off the sidebar's member identity, which they only hold there because launching a preview enrolled them.
`unique_assessment_id_and_creator_id` allows one submission per (assessment, previewer), and launching a preview resumes it rather than resetting it — so without this, starting over means waiting for the TTL. `Submission#reset_preview!` clears every answer, not just the current ones: a previewer who finalises and unsubmits accumulates Past Answers, which is exactly the trace a reset is meant to erase. It does not reuse `unsubmit`'s `recreate_current_answers`, which preserves in-progress drafts, copies the old content forward as `last_attempt`, and keeps the old rows as history — all correct for a real course, all wrong here. The endpoint never accepts a submission id from the client: the row is always looked up by (assessment, creator), which makes resetting someone else's attempt structurally impossible before the ability check even runs.
Opens the sandbox in a new tab from a listing's preview page, behind a confirmation that says what will happen: a separate sandbox, possibly a sign-in redirect, and no effect on the viewer's own course. `navigateTo` exists as a seam, not an abstraction: jsdom seals the whole navigation surface, so a component that assigns `location.href` inline has no assertable behaviour and its test passes whether or not the branch still exists.
Says what the sandbox is on the page a previewer is actually landed on, and hosts the Reset submission button there. Mounted by `CourseContainer` because the flag lives on the layout payload, the only data available on every course page — but it narrows itself to submission pages: the promise it makes is about work a previewer produces, and the action it hosts has nothing to act on elsewhere. Gated on `isPreview`, not `isPreviewRestricted`: "nothing here is real" is a fact about the course, equally worth saying to an administrator, who also needs the Reset action. Only the LOCK is per-viewer.
LWS49
force-pushed
the
lws49/feat-marketplace-pr8a-preview-sandbox
branch
from
August 3, 2026 05:08
e0c59f1 to
381bf19
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds the hands-on half of marketplace preview. Until now a teacher evaluating a listing could only read its questions; there was no way to answer one, submit it, or grade and publish it before committing to a duplicate. "Try it hands-on" on a listing preview page now opens the listing's served snapshot as an ordinary submission inside a single shared sandbox course on the dedicated preview instance, where the previewer can rehearse the student-and-grader loop end to end.
Everything that makes that safe keys off the
previewflag on the sandbox course, never off an instance id: content is frozen, the sidebar and breadcrumbs are de-linked, a persistent banner says nothing here is real, graded-submission emails and external-LMS task completion are suppressed, and each previewer can reach only their own submission - not the sandbox's other submissions, its gradebook, or its roster. A "Reset submission" button in the banner is the explicit way to start over.This is the first of two PRs split out of #8527. It stops at a working preview flow; #8527's remaining pieces (auto-marking refresh, the preview publish toast, the not-found page, and the attempt TTL reaper) follow in pr8b.
Design decisions
manager- the lowest stock role that can attempt, grade and publish, since there is no grade-only role. Sharing one course means a manager's ordinary reads would otherwise reach every other previewer's submission, so the preview ability revokes submission management, the submissions list, the gradebook and the roster wholesale, then carves back exactly the previewer's own submission bycreator_id.preview_sandbox_accessible?, mirroring howpublicly_accessible?marks out the unauthenticated surface. A subtractive list can never be complete - it has to name every component, every custom verb and every collection action, and each new one arrives switched on.:reset_own_preview_submission, rather than a creator-scoped carve-out of:delete_submission, which CanCan cannot express alongside a blanketcannoton the same action. The endpoint never accepts a submission id from the client - the row is always looked up by (assessment, current user) - and it isupdate, notdestroy, so the submission keeps its id and the previewer watches it come back blank instead of being redirected away.Regression prevention
Backend covers: launch authorization, including a published listing with no snapshot being denied rather than crashing; enrolment and attempt-URL construction on the preview host, and its idempotency per (listing, previewer); the reset endpoint accepting only the caller's own submission, only inside a preview course, and clearing answers back to genuinely blank with no past-answer or test-result residue; cross-previewer denial of another previewer's submission, plus denial of the submissions list, gradebook and roster inside the sandbox; and graded-submission email and external-LMS completion suppression.
Frontend covers: the launch button's new-tab flow, popup-blocked fallback and failure toast; the reset button being absent where the URL carries no assessment id, and re-hydrating the submission page in place when it does; the sandbox banner and the
isPreviewgate that mounts it; and breadcrumbs rendering inert.Manually verified: launch into the sandbox on the preview host and the popup-blocked fallback; banner, inert breadcrumbs and unlinked sidebar; reset clearing answers in place with the button absent off assessment pages; a second previewer blocked from the first's submission, the submissions list, the gradebook and the roster; no graded email or external-LMS push from the sandbox; and a published listing with no snapshot returning 403.
Nothing changes for existing courses: every behaviour here is gated on the sandbox course's
previewflag. The sidebar partial's link-or-not argument now carries that flag alongside the read check.