Skip to content

Lws49/feat marketplace pr8 singleton attempt - #8521

Draft
LWS49 wants to merge 31 commits into
lws49/feat-marketplace-pr6d-auditfrom
lws49/feat-marketplace-pr8-singleton-attempt
Draft

Lws49/feat marketplace pr8 singleton attempt#8521
LWS49 wants to merge 31 commits into
lws49/feat-marketplace-pr6d-auditfrom
lws49/feat-marketplace-pr8-singleton-attempt

Conversation

@LWS49

@LWS49 LWS49 commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

LWS49 added 30 commits July 27, 2026 22:45
Provisions a hands-on marketplace preview: duplicates the listed
assessment into the singleton container course (linked via
listing.preview_assessment_id, idempotent across re-launches), enrols
the previewer as a course manager, and returns the absolute attempt
URL on the preview host. Runs with system privileges by design — the
Slice 2 content-freeze must not block its own provisioning; the caller
(Slice 3's controller action, not yet added) is responsible for
authorization.
POST /courses/:course_id/marketplace/listings/:id/launch_preview
mirrors #show's authorization (published listing + preview_in_marketplace
gate) and delegates provisioning to PreviewLaunchService, returning the
attempt URL as JSON.
A previewer grades and publishes their own submission in a preview
sandbox course; guard the graded-email chokepoint (the only
notification path that doesn't already self-suppress for non-student
roles) and the Cikgo task-completion push so a rehearsal never emails
the previewer or reports a fake completion to their real LMS.
Aged marketplace preview submissions must be reaped so a previewer can
restart a listing without waiting indefinitely (the unique
assessment/creator index allows only one submission per pair), while
still giving async autograding room to land before deletion. Mirrors
DeletingJob's destroy!-per-row mechanics so the EXP after_destroy hook
and MonitoringService parity aren't skipped by a bulk delete. The
container course, assessment copies, and enrolments are never touched.
The local Postgres dumps this pre-existing index's WHERE clause with a
different array-cast spelling; it rode along with the preview_assessment_id
migration. Semantically identical, so restore master's rendering to keep the
branch's schema delta to this branch's own migrations.
Previewers are enrolled as manager of a container course shared by every
previewer in the instance. Revoke cross-previewer submission access,
view_all_submissions, the gradebook, and the users roster inside a preview
course, carving back exactly the previewer's own submission.
show.json.jbuilder emitted @assessment.id under the `id` key, so
TryItHandsOnButton and DuplicateConfirmation posted back with the wrong id to
endpoints keyed on Listing#id — the lookup missed, raised
CanCan::AccessDenied, and the frontend's 403 handler nuked the whole page.
index.json.jbuilder already used listing.id correctly; this brings show in
line with it.
…test

tab.location was an empty object literal, so TS rejected the later .href
read once the redirect assertion was added. Cast it to { href?: string }.
RECORDER_COMPONENT_UNMOUNT unconditionally called recorderHelper.stopRecord()
whenever the last Voice-question component unmounted, even if it was never
recording. stopRecord() rejects in that case ("Recorder has already
stopped"), and nothing awaited or caught it, so it became an unhandled
rejection — silent in production, but rendered as a full-page dev-overlay
crash under React 18 StrictMode's mount/unmount/remount cycle, which fires on
every page load that renders an unanswered Voice question.

Guard on recorderHelper.isRecording() before calling stopRecord(), plus a
defensive .catch() against any residual race.
Once Fabric.js constructs the canvas, it wraps the raw <canvas> node in its
own container div, re-parenting it out from under this div's direct
children. The loading indicator was rendered as a PRECEDING sibling, so any
later toggle back to "loading" (e.g. a redundant re-initialize) required
React to insertBefore a sibling relative to that no-longer-direct-child
node — which throws (surfaced as "Failed to execute 'insertBefore'... is
not a child of this node").

Moving the indicator after the canvas means React only ever needs to
append a trailing sibling, which needs no reference node and is safe
regardless of what Fabric has done to the canvas by then. Added a
regression test that reproduces the crash with a real Fabric canvas (a
stubbed Image whose src setter fires `load` asynchronously, since jsdom
never does) and confirms a redundant re-initialize no longer throws.
The sidebar already de-links entirely for any preview course; breadcrumbs
did not, so a previewer could still click through the course-title/tab/
assessment-title crumbs to browse the rest of the shared sandbox. Adds a
disableLinks prop to Breadcrumbs (renders every crumb as inert text via the
same Link/Crumb de-linking mechanism already used for the last crumb),
driven off CourseLayoutData.isPreview in CourseContainer.
…e page

The container course's own home page is otherwise blank (no announcements,
todos, or activity), yet it's the first thing a previewer sees when
"Try it hands-on" enrolls them there before the assessment tab opens. Seed a
description on the container course (blank-only, so an admin edit is never
clobbered, and seeded on every provisioning call rather than only at
creation since the container is created once and reused forever) and show
it on CourseShow for previewers, who otherwise see nothing else on the page.
…preview banner

Previewers share one submission per (assessment, previewer) — relaunching
"Try it hands-on" resumes it rather than starting over — but until now the
only way to reset it was a manual Rails console script that wipes the
entire shared container course. Adds a "Reset submission" button to the
preview banner, scoped strictly to the current user's own submission for
the assessment on the current page.

Backend: Submission#reset_preview! resets workflow/grading state back to a
pristine `attempting` (works from any prior state, including `published`)
and gives every question a genuinely blank current answer via
`question.attempt(submission)` with no `last_attempt` — unlike `unsubmit`'s
`recreate_current_answers`, this destroys the old answers outright (and,
via their normal dependent: :destroy associations, any auto-grading and
programming test-case results with them) rather than keeping them as
non-current history, since a preview reset has no audit-trail requirement
to preserve. New PreviewSubmissionsController#update authorizes via the
`reset_own_preview_submission` ability (already scoped to the caller's own
submission) and never accepts a submission id from the client — it's always
looked up by (assessment, creator: current_user).

Frontend: the button stays on the same page after resetting (no navigation
away) and re-dispatches fetchSubmission so the now-blank state renders in
place, since the submission row keeps its id.
…ished

publish() already toasted a generic "update successful" message, but
PublishButton also unmounts immediately on success (it only renders while
workflowState === 'submitted'), so in a solo preview session — no student to
observe the effect — publishing looked like it did nothing. When
CourseLayoutData.isPreview is true, the success toast is replaced with copy
explaining what publishing would do in a real course; the failure path is
unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant