Skip to content

feat(lessons): add description and resources to lessons - #831

Open
pdmarf wants to merge 5 commits into
codelitdev:mainfrom
pdmarf:feat/media-lesson-description
Open

pdmarf wants to merge 5 commits into
codelitdev:mainfrom
pdmarf:feat/media-lesson-description

Conversation

@pdmarf

@pdmarf pdmarf commented Oct 9, 2026 •

Copy link
Copy Markdown

Summary

Lessons can now carry a rich-text description and a set of downloadable resources (attachments) alongside their main content, so creators can provide notes, instructions and supporting files in place.

Each field is enabled per lesson type from one shared pair of constants in @courselit/common-models:

Lesson type Description Resources
Text – ✓
Video ✓ ✓
Audio ✓ ✓
Embed ✓ ✓
PDF ✓ –
File ✓ –

Changes

Data model & API

  • Add description (ProseMirror doc) and attachments (Media[]) to the lesson schema (apps/web/models/Lesson.ts, orm-models, common-models)
  • Expose both fields on the GraphQL lesson, input and update types; attachments resolve through getMedia
  • Expose both fields on the REST lesson routes and keep openapi.mjs in sync
  • Reject a description or resources sent for a lesson type that doesn't support them, so neither API can store fields the editor won't show and the viewer won't render
  • Validate that a description is a ProseMirror document (a bare string previously made every later read of the lesson fail); over REST, null means no description on create and clears it on update
  • Reject attachments without a media id rather than silently dropping them

Media ownership and lifecycle

  • A new attachment must be media this school uploaded (checked against the media service group) that no lesson already uses, including the lesson's own media. Removing an attachment deletes its file, so this stops an API caller from deleting arbitrary media by attaching and then removing it
  • A lesson's main media can't be one of its attachments, and media any lesson offers as an attachment can't become a lesson's main media
  • Description and attachment media is only deleted when this school owns it and no lesson still uses it as media or an attachment
  • Only embedded media (node src) in a description is treated as the lesson's own; links are never sealed or deleted, so linking to another lesson's file is safe
  • Attachments already on the lesson are kept as stored instead of being re-sealed, so one file the media service has lost can't block edits to the list; repeated attachments are stored once
  • Seal temporary media in descriptions and new attachments on create/update; private attachments drop their signed (expiring) URL and are refetched on read, public ones keep their permanent URL
  • An attachment the media service has lost is returned as stored, so the lesson still loads and the editor can remove it
  • On update, save the lesson first, then delete media for dropped attachments; a failed media deletion no longer fails the update
  • On lesson delete, clean up attachment and description media; a failure cleaning up one media item no longer blocks deleting the lesson, its evaluations and activity
  • packages/scripts domain cleanup also deletes attachment and description media the school owns

UI

  • New LessonAttachments component, and Media / Description / Resources sections in the lesson editor
  • New text and embed lessons reopen for editing after creation, so resources can be added straight away
  • Adding or removing a resource saves immediately and says "Resources saved", since other unsaved edits are not included; resource saves can't overlap
  • Lesson viewer renders the description only when it has content (a cleared editor counts as empty, an image-only description does not) and resource download links (skipping any without a URL)
  • Lesson type support checks (lessonTypeSupportsDescription / lessonTypeSupportsAttachments) and media-source extraction (extractMediaIDsFromNodeSources) live in @courselit/utils
  • Fix two media-selector buttons that submitted their enclosing form

Docs

  • New "Description and resources" page (with the per-type support table) in both apps/docs and apps/docs-new, linked from the add-content guide

Testing

  • pnpm test: 98 suites / 856 tests pass, including new cases in apps/web/graphql/lessons/__tests__/logic.test.ts (ownership, reuse, stale media, links), the REST lesson route test and the lesson editor page test
  • pnpm lint and pnpm prettier clean; tsc --noEmit clean for apps/web and packages/scripts

🤖 Generated with Claude Code

pdmarf and others added 3 commits September 14, 2026 18:22
Media-type lessons (video, audio, PDF and file) can now carry a rich-text
description and a set of downloadable resources alongside their media, so
creators can provide notes, instructions and supporting files in place.

- Add `description` (ProseMirror doc) and `attachments` (Media[]) to the
  lesson schemas in models/Lesson.ts, orm-models and common-models
- Expose both fields on the GraphQL lesson, input and update types, with
  attachments resolved through getMedia
- Seal temporary media in descriptions and attachments on create/update,
  and clean up attachment and description media when a lesson is deleted
- Add a LessonAttachments component and surface Media, Description and
  Resources sections in the lesson editor
- Render the description and resource download links in the lesson viewer
  for media-type lessons
- Expose the new fields through the REST lesson routes and OpenAPI spec
- Fix two media-selector buttons that submitted their enclosing form

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-up fixes from a security and code review of the lesson description
and resources feature.

- Authorise media deletion for lesson attachments, not just lesson media.
  Removing a resource previously failed with a 403 and left the file
  orphaned in the media service
- Render the description only when it has content, so media lessons no
  longer show an empty block under the player
- Delete the media for attachments dropped by an update, which were
  previously left behind in the media service
- Reject a description that is not a document object. Storing a bare
  string made every later GraphQL read of that lesson fail, leaving it
  unopenable in the dashboard and the viewer
- Reject attachments without a media id instead of silently discarding
  them and reporting success
- Persist the attachment list before deleting the underlying media, so a
  failed save can no longer leave a lesson pointing at a deleted file

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Description and resources were offered together on the four media lesson
types. Neither fits every type, so they are now gated independently from
one shared pair of constants in common-models, and documented.

- Offer a description on video, audio, embed, pdf and file lessons, and
  resources on text, video, audio and embed lessons
- Reject a description or resources sent for a lesson type that does not
  support them, so the REST and GraphQL APIs cannot store fields the
  editor will not show and the viewer will not render
- Add a docs page covering both fields, with a per-type support table,
  and link it from the add content guide

Follow-up fixes from a review of the same feature:

- Save the lesson before deleting media dropped by an update, and keep a
  failed deletion from failing the update. A media id the media service
  no longer knows about previously made an attachment impossible to
  remove, since the save never ran
- Keep lesson deletion working when cleanup of one media fails, which
  otherwise left the lesson, its evaluations and its activity undeleted
- Require a description to be a ProseMirror document rather than any
  object, matching what the OpenAPI schema already declares
- Treat a null description over REST as clearing it, instead of storing
  the string "null" and rejecting every later read
- Skip resources that have no URL in the viewer, rather than rendering a
  download button that does nothing
- Narrow lesson media deletion back to the lesson's own media now that
  removing a resource no longer deletes through that route

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@pdmarf
pdmarf marked this pull request as draft October 9, 2026 21:21
pdmarf and others added 2 commits October 9, 2026 21:34
Follow-up fixes from a code review of the description and resources
feature.

- Only accept a new resource that this school uploaded and that no
  lesson already uses, including the lesson's own media. Removing a
  resource deletes its file, so any media id sent through the API could
  previously be deleted that way, including another school's
- Only delete description and resource media that this school owns
- Keep resources already on the lesson as stored rather than sealing
  them again, so a file the media service has lost no longer blocks
  every edit to the list, and store a repeated resource once
- Treat only embedded media (image sources) in a description as the
  lesson's own. Links are no longer sealed on save or deleted when
  removed, so linking to another lesson's file is safe
- Delete resource and description media in the domain cleanup script
- Reopen a new text or embed lesson for editing after it is created, so
  resources can be added straight away
- Treat a null description on REST create as none, matching update
- Say "Resources saved" rather than "Lesson updated" when a resource is
  added or removed, since other edits are not saved with it
- Hide a description that was cleared, while still showing one that
  holds only an image
- Share lesson type support checks and media input shaping as helpers
  instead of repeating them, and move inline strings into strings.ts

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Reject a lesson's media as one of its resources on create, and media
  that any lesson already offers as a resource.
- Only delete removed resource and description media that no lesson
  still uses as its media or a resource.
- Return a resource the media service has lost as stored, so the
  lesson still loads and the editor can remove it.
- Keep the file url on public resources, which are not refetched.
- Reject non-list attachments.
- Serialize resource saves in the editor.
- Domain cleanup only deletes resource and description media the
  school owns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@pdmarf
pdmarf marked this pull request as ready for review October 9, 2026 22:09
@rajat1saxena

Copy link
Copy Markdown
Member

Thanks for the PR. The review will take some time.

This branch has not been deployed

No deployments
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.

2 participants