Skip to content

feat(marketplace): version-aware browse, duplicate and adopter update - #8525

Open
LWS49 wants to merge 7 commits into
lws49/feat-marketplace-pr7a-versioning-foundationfrom
lws49/feat-marketplace-pr7b-versioning-course-surfaces
Open

feat(marketplace): version-aware browse, duplicate and adopter update#8525
LWS49 wants to merge 7 commits into
lws49/feat-marketplace-pr7a-versioning-foundationfrom
lws49/feat-marketplace-pr7b-versioning-course-surfaces

Conversation

@LWS49

@LWS49 LWS49 commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Summary

Puts the versioned content model from the base PR in front of course managers. Browsing, listing and question preview, and duplication all now read the current version's snapshot instead of the live source assessment, so what a manager sees in the marketplace is exactly what they get when they duplicate. A manager who owns a listing gets a "publish new version" action on the assessment page.

On the other side of the transaction, an adopter whose copy is behind now finds out. The assessment page carries a banner naming when their content was published and when the marketplace's current version was, and offers to replace the copy's content in place. That offer is withheld once a real student has submitted work, because replacing content would discard both their submission and any local edits, and the banner explains that instead of failing.

Finally, a system admin looking at the container course's assessment index gets a version chip per row, since every snapshot there keeps its origin's title verbatim and shares one tab.

Finally, a snapshot's own page now warns that it is frozen. A snapshot in the container is an ordinary assessment with Edit, Delete and the question menus all live, and editing one silently changes what future adopters copy for a version that was never published. Nothing is disabled - the surface is admin-only and the escape hatch for repairing served content is deliberate - so the banner names the risk and links straight at the source assessment to edit instead.

Design decisions

  • 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. Date precision escalates to include a time only when the two vintages would otherwise render identically, which happens when a listing is republished twice in one day.

  • The in-place update replaces questions and materials but deliberately leaves published and the destination tab alone, so updating cannot silently expose, hide or relocate an assessment. Unlock conditions and link-tree membership are also untouched, for a stronger reason: they reference the adopting course's own objects, so the snapshot's would be meaningless there.

  • A colliding title is renamed on every import, not only on re-import of the same listing, and the suffix carries the content's vintage read from the same field the banner uses so the two can never disagree. A copy landing on top of an unrelated assessment of the same name collides just as badly and previously landed silently.

  • Marketplace abilities take the listing as their subject rather than the assessment, because the assessment these actions serve is the container snapshot and never the authoring copy, and because keying on the listing keeps the check working for an orphaned listing whose authoring assessment is gone.

  • The snapshot warning is a soft guard, banner only. A hard guard would need an ability rule in the PR below this one, and it would permanently remove the ability to repair served content without minting a version. The trigger is marketplaceVersion present with a non-null publishedAt, the same discriminator the version chip already uses to label the working copy, so the two cannot disagree.

  • The source-assessment link is absolute and carries the origin instance's own host and port, because a course id only resolves on its own instance's host and a controller otherwise supplies the port the request reached Rails on rather than the public one.

Regression prevention

Covers: browse, listing preview and question preview reading the snapshot rather than the live source, including that a published listing with no snapshot cannot break the browse page; publishing a new version from the assessment page; duplication producing the snapshot's content, the dated rename on collision, repeated re-imports not compounding the suffix, and title truncation against the column limit; the adopter payload emitting the notice only when a newer version exists; the in-place update service preserving publication state, tab, unlock conditions and link-tree membership while deleting test submissions; the gate that withholds the update once a non-phantom student has submitted, enforced at the endpoint and not only in the payload; conditional datetime precision on the banner; and the container index badge, including that it stays off ordinary courses and away from non-admin previewers.

Also covers the snapshot warning: it renders for a published snapshot with a working source link, explains the absence instead of linking when the listing is orphaned, and stays off both the listing's working copy and any assessment the marketplace does not own. The payload side asserts the link resolves on the origin instance's host for a cross-instance source, which is what catches a missing tenant escape or a missing explicit port.

Manually verified: editing a source after publishing leaves the marketplace serving the old snapshot until a new version is cut; duplication matches the served snapshot and a re-import gets a dated title; the banner appears on a copy that is behind and the update preserves deadlines and publication state; a copy with a real student submission shows the explanation and no action; the container index shows dated chips on snapshots and "Source Assessment" on the working copy.

Backward compatible for adopters. Existing copies gain a banner only once their listing publishes a version newer than the one they hold.

@LWS49

LWS49 commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator Author

Follow-up, deliberately not in this PR: host_options and the source-assessment URL build now exist in both Course::Assessment::AssessmentsController and System::Admin::MarketplaceListingsController. They are duplicated because the admin controller lands in #8526, above this PR, so sharing would mean an upward dependency. Extract to a shared concern once the stack merges.

@LWS49
LWS49 force-pushed the lws49/feat-marketplace-pr7a-versioning-foundation branch from b54c334 to b1449fb Compare July 29, 2026 13:32
@LWS49
LWS49 force-pushed the lws49/feat-marketplace-pr7b-versioning-course-surfaces branch from d959743 to 8fe805e Compare July 29, 2026 13:32
LWS49 added 2 commits July 30, 2026 10:02
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.
@LWS49
LWS49 force-pushed the lws49/feat-marketplace-pr7a-versioning-foundation branch from b1449fb to 0bbcfea Compare July 30, 2026 02:02
@LWS49
LWS49 force-pushed the lws49/feat-marketplace-pr7b-versioning-course-surfaces branch 2 times, most recently from c3cedff to 8ff2acd Compare July 30, 2026 02:51
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.
@LWS49
LWS49 force-pushed the lws49/feat-marketplace-pr7a-versioning-foundation branch from 99f8c3c to 213421c Compare July 30, 2026 02:58
LWS49 added 4 commits July 30, 2026 10:58
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.
@LWS49
LWS49 force-pushed the lws49/feat-marketplace-pr7b-versioning-course-surfaces branch from 8ff2acd to 621c34d Compare July 30, 2026 02:58
@LWS49
LWS49 force-pushed the lws49/feat-marketplace-pr7a-versioning-foundation branch from 22e83bd to a9d6967 Compare July 31, 2026 10:39
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