feat(marketplace): version-aware browse, duplicate and adopter update - #8525
Open
LWS49 wants to merge 7 commits into
Conversation
Collaborator
Author
|
Follow-up, deliberately not in this PR: |
LWS49
force-pushed
the
lws49/feat-marketplace-pr7a-versioning-foundation
branch
from
July 29, 2026 13:32
b54c334 to
b1449fb
Compare
LWS49
force-pushed
the
lws49/feat-marketplace-pr7b-versioning-course-surfaces
branch
from
July 29, 2026 13:32
d959743 to
8fe805e
Compare
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
force-pushed
the
lws49/feat-marketplace-pr7a-versioning-foundation
branch
from
July 30, 2026 02:02
b1449fb to
0bbcfea
Compare
LWS49
force-pushed
the
lws49/feat-marketplace-pr7b-versioning-course-surfaces
branch
2 times, most recently
from
July 30, 2026 02:51
c3cedff to
8ff2acd
Compare
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
force-pushed
the
lws49/feat-marketplace-pr7a-versioning-foundation
branch
from
July 30, 2026 02:58
99f8c3c to
213421c
Compare
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
force-pushed
the
lws49/feat-marketplace-pr7b-versioning-course-surfaces
branch
from
July 30, 2026 02:58
8ff2acd to
621c34d
Compare
LWS49
force-pushed
the
lws49/feat-marketplace-pr7a-versioning-foundation
branch
from
July 31, 2026 10:39
22e83bd to
a9d6967
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
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
publishedand 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
marketplaceVersionpresent with a non-nullpublishedAt, 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.