Lws49/feat marketplace pr8 singleton attempt - #8521
Draft
LWS49 wants to merge 38 commits into
Draft
Conversation
A version IS its publication datetime, and the snapshot it serves is duplicated into a `preview` container course so a published version can never change under the courses that adopted it. Non-admins cannot edit container content.
Deleting the source assessment now orphans the listing instead of destroying it: the marketplace keeps serving the snapshot, and the authoring copy is rebuilt in the container automatically so a new version can still be published.
Nothing rolls back in this suite, so two unscoped `delete_all`s leaked into every later spec file and failed them under some seeds: `User::Email.delete_all` stripped the address off every earlier file's users (surfacing as `SMTP To address may not be blank` in the mailer specs and a nil `user.email` in the external-assessment import specs), and allow-list rows left behind granted `:access_marketplace` to users the ability spec asserts cannot have it. Scope the email cleanup to the domains that file hardcodes, and clear the allow-list tables in the ability spec the way access_list_query_spec.rb already does.
Browse, listing/question preview and duplication all read the current version's snapshot rather than the live source assessment, and a manager can cut a new version from the assessment page.
The banner dates both content vintages rather than numbering them, and cannot be dismissed or muted: it is a statement of fact about the copy, so it stands until the copy is updated or deleted. It offers the in-place update only while no student has submitted; once one has, it explains why no action is possible.
Inside the preview container every listing's snapshots share one title, so the index chip dates each one and links it to the listing it belongs to.
A snapshot in the container course is an ordinary assessment with every management affordance live, and editing one is silently destructive: it changes what future adopters copy for a version that was never published, and stops the version's publication date describing its content. A soft guard only. Nothing is disabled, because the surface is admin-only and the escape hatch for repairing served content without minting a version is deliberate. The banner names the risk and links straight at the source assessment to edit instead, on that assessment's own host since it may live on another instance.
Every listing across instances, with its version history, adoption list and source provenance, plus the actions only an admin has: unlist, re-list, rebuild the authoring copy, and permanently delete a listing that is off the marketplace. The whole read path runs tenant-free — listings span instances while their snapshots live in the preview container — and links to a source assessment are absolute, since a course id only resolves on its own instance's host.
The listings table, its per-listing page, and the two maintenance buttons. Actions sit in one fixed order so a given action always occupies the same slot, and delete is present on every row — disabled until the listing is unlisted — so its tooltip can state the rule rather than leaving the admin to infer it from a missing icon.
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.
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.
LWS49
force-pushed
the
lws49/feat-marketplace-pr8-singleton-attempt
branch
from
July 30, 2026 04:57
9d6c66a to
49f0ffb
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.
No description provided.