Skip to content

Migrate the learn plugin onto the new resource methods - #15155

Merged
AlexVelezLl merged 5 commits into
learningequality:developfrom
rtibblesbot:issue-15059-203220
Aug 11, 2026
Merged

Migrate the learn plugin onto the new resource methods#15155
AlexVelezLl merged 5 commits into
learningequality:developfrom
rtibblesbot:issue-15059-203220

Conversation

@rtibblesbot

@rtibblesbot rtibblesbot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Every resource call in kolibri/plugins/learn now uses the new Resource methods. Shared kolibri-common custom methods gained _v2 variants; the clearCache() calls are gone. Reads that the retired cache served now hit the network — request coalesces only concurrent identical GETs.

References

Fixes #15059. Recipe: #15057. New methods: #15056. Evidence per criterion.

  • No old base methods or cache helpers in learn — two greps return nothing.
  • Custom methods per the recipe — five _v2 additions, originals untouched; learn-only getResumeData rewritten in place.
  • useRetrieve/useList where possible — nowhere. The three useFetch sites in CourseUnitView/index.vue compose or wrap custom endpoints, and useList drives only the default list endpoint. Every other read is one-shot.
  • Tests updated, pnpm test-jest green for learn — 35 suites, 930 tests.

PATCH /issues/15059 returns 403 Must have admin rights to Repository, so a maintainer has to tick the issue body.

Code review

  • useLearnerResources.js:300force gone from the four public fetchers; the cache they bypassed is gone.
  • ContentNodeResource.retrieve's cache hit ignores params, baseurl included — the same node on two devices resolves to whichever was fetched first. Unchanged from fetchModel, and why the two-device QA below.
  • CustomContentRenderer.vue:359 reads .results off a bare array — undefined before and after. Pre-existing.

Manual QA

Nothing in Learn should look or behave differently — this is a plumbing change with no new UI. Every screen in the plugin was touched, so it all needs a pass. These five carry the most risk:

  • Progress after an assessment. As a learner, submit a quiz, an exam, a course quiz and a pre/post test. After each, return to the class page — the new progress must be there without reloading.
  • A coach's change while a learner is watching. Leave a learner sitting on a class page; as a coach, assign or unassign a lesson or quiz. It must appear for the learner within about 30 seconds, no reload.
  • "Disconnected" marker. Browse the Kolibri Library (needs internet) or a pinned peer device, then cut that connection. The "Disconnected" label and icon must appear within a few seconds, and clear when the connection returns.
  • Channels with a custom look. On such a channel, use its search filters and any shuffle / "random resources" section. Nothing that used to show content may come up empty.
  • Two devices at once. Pin another Kolibri device, open a resource there, then open one on your own device. Each must show its own device's copy — right title, files and metadata.

Normal depth everywhere else: library and topic browsing including "show more", bookmarks, the resource-info side panel, recommendations after finishing a resource, downloads, pinning and unpinning devices, lesson playlists, the quiz and exam players, and course units.

Deviations from the issue spec

  • ContentNodeResource.fetchResume and fetchLessonResources — called from learn, left on the raw client. They hit kolibri:core:usercontentnode_list, a resource name this.request cannot resolve, so neither recipe branch applies.

AI usage

Used Claude Code to apply the recipe call site by call site. Verified with the Jest suites and greps for every retired name.


@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly

How was this generated?
  • Ran pre-flight CI checks (lint, format, tests) and verified all pass
  • Rebased onto the target branch and resolved any conflicts
  • Reorganized commit history into clean, logical commits
  • Audited the diff to ensure only issue-relevant files are changed
  • Built PR body from the repository's PR template with evidence blocks
@rtibblesbot

🟡 Waiting for feedback

Last updated: 2026-08-11 14:22 UTC

@rtibblesbot

Copy link
Copy Markdown
Contributor Author

Acceptance criteria from #15059, with the evidence for each. Posted here because this account cannot edit an issue body (UpdateIssue is denied), so the boxes there stay unticked.

  • No code in learn calls the old base methods or cache-management helpers.
$ grep -rnE '\.(fetchModel|fetchCollection|saveModel|deleteModel|saveCollection|deleteCollection|fetchDetailModel|fetchDetailCollection|fetchListCollection|getListEndpoint|postListEndpoint|postDetailEndpoint|accessListEndpoint|accessDetailEndpoint|clearCache|getModel|getCollection|addModel|findModel|removeModel|removeCollection|unCacheModel|unCacheCollection)\b' kolibri/plugins/learn
$ echo $?
1

Anchored on \b rather than ( so .fetchTree.mockResolvedValue in specs is caught too, and so is the stale JSDoc example in apiResources.js, which was updated for that reason.

  • Custom methods handled per the recipe. Six _v2 methods added to shared kolibri-common resources, each a pure addition with the original left intact for the plugins not yet migrated: ContentNodeResource.fetchTree_v2, fetchBookmarks_v2, fetchRecommendationsFor_v2, fetchRandomCollection_v2, ContentNodeProgressResource.fetchTree_v2, ChannelResource.fetchFilterOptions_v2, RemoteChannelResource.getKolibriStudioStatus_v2. learn's own LearnerCourseResource.getResumeData took the in-place branch — it is defined and used only within learn — with an unchanged signature and return.

No learn call site still names an original:

$ grep -rnE '\.(fetchTree|fetchBookmarks|fetchRecommendationsFor|fetchRandomCollection|fetchFilterOptions|getKolibriStudioStatus)\b' kolibri/plugins/learn
$ echo $?
1
  • Reactive reads use useRetrieve/useList where possible — which, in learn, is nowhere. Scoped to reactive reads, per Migrate all plugins and shared packages to the new Resource-layer methods #15057 step 4 ("if it will become a large refactor, then omit it"). Four groups of call site, none convertible:

    • learn's only three useFetch sites are all in CourseUnitView/index.vue: one composes two independent requests into a single object, the other two wrap the fetchTree_v2 and getResumeData custom endpoints. Resource.useList's fetchMethod is () => this.list(toValue(params)) — it can only drive the default list endpoint.
    • usePinnedDevices.fetchPinsForUser, useBookmarks.fetchBookmarks and useContentNodeProgress.* each reshape the response into a module-level shared map that is the composable's public surface. useList's onSuccess is a side-effect hook, not a mapper, so its data ref would hold the unshaped response and the map would still need maintaining alongside it.
    • LibraryPage/index.vue's _showChannels is the closest call — it does write to rootNodes/rootNodesLoading refs. It still does not convert: it is invoked per (channels, baseurl) pair rather than from reactive params, it re-orders the response against its channels argument, and it drops the result behind a samePageCheckGenerator() guard useList has no hook for.
    • Everything else is a one-shot imperative call — Options-API methods and .then() chains, route handlers, and DeviceConnectionStatus.vue's 5s useTimeoutPoll.
  • Tests that call/mock replaced methods are updated; pnpm test-jest passes for learn. Eight specs updated. Most use bare automocks, so only the names change; CourseUnitView.spec.js and LearnIndex.spec.js use jest.mock factories, where a missing key is undefined rather than an auto-stub, so their keys were renamed explicitly.

$ pnpm test-jest -- --testPathPatterns 'plugins/learn|apiResources'
Test Suites: 36 passed, 36 total
Tests:       933 passed, 933 total

New spec packages/kolibri-common/apiResources/__tests__/ContentNodeResource.spec.js covers the two overrides that carry real behaviour — the read-through cache, the defensive copy on a cache hit, and list populating the cache. The six _v2 methods are one-line request wrappers and get none.


Two things found on the way, both left alone:

  • ContentNodeResource.fetchResume, fetchPopular, fetchNextSteps and fetchLessonResources are unmigrated. They target kolibri:core:usercontentnode_list, a different resource name than this.request can resolve, and they already bypass the Model/Collection layer.
  • CustomContentRenderer.vue's sendRandomCollection reads .results off what the random endpoint returns as a bare array, so the sandbox custom-channel API has always received undefined there. Pre-existing; worth its own issue.

@rtibblesbot
rtibblesbot force-pushed the issue-15059-203220 branch 4 times, most recently from 45ab7a0 to 083a490 Compare August 6, 2026 22:30
@rtibblesbot
rtibblesbot marked this pull request as ready for review August 7, 2026 01:36

@AlexVelezLl AlexVelezLl left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Just a couple of nitpicks. Also smoke tested the Learn pluggin, and didn't find any regression. So, after the fixes this should be ready for QA! Please add instructions for QA in the PR description. We will need QA pretty much on the entire plugin, but specially mention areas that need more attention.

Comment on lines +44 to +47
const studio = await RemoteChannelResource.getKolibriStudioStatus_v2();
set(allDevices, [
{
...response.data,
...studio,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we rename this to studioStatus or something more meaningful?

@rtibblesbot rtibblesbot Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to studioStatus.

Comment on lines 140 to +160
},
// Overrides `Resource.retrieve` to read through this resource's `cache`, which any of its
// fetch methods may have populated. As in `fetchModel` above, a cache hit ignores `params`.
async retrieve(id, { params } = {}) {
if (!id) {
throw TypeError('An id must be specified');
}
if (this.cache[id]) {
return cloneDeep(this.cache[id]);
}
return this._requestAndCache({ action: 'detail', routeParams: id, params });
},
// Overrides `Resource.list` to populate this resource's `cache` with every node it returns.
async list(params = {}) {
return this._requestAndCache({ params });
},
async _requestAndCache(options) {
const { data } = await this.request(options);
this.cacheData(data);
return data;
},

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the _requestAndCache is a good idea, but I think the way this PR handles this is sligthly better, because there we actually call the underlying method, which, for now its equivalent to the code change here, but if in the future we change something in the base methods, we will need to recall to update it here too.

@rtibblesbot rtibblesbot Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — retrieve and list now call Resource.prototype.retrieve/list and cache what comes back, so a later change to the base methods reaches this resource without an edit here. _requestAndCache survives for fetchTree_v2 and fetchBookmarks_v2, whose custom endpoints have no base method to delegate to.

@github-actions github-actions Bot added APP: Learn Re: Learn App (content, quizzes, lessons, etc.) DEV: frontend SIZE: medium labels Aug 7, 2026
@rtibblesbot

rtibblesbot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Both nitpicks addressed on the current head, and a QA section is now in the description. Thanks for the smoke test!

(No commit SHA cited on purpose — this branch is periodically folded and force-pushed, which kills any SHA link. Read the current diff instead.)

@AlexVelezLl
AlexVelezLl requested a review from radinamatic August 7, 2026 14:16
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

@rtibblesbot

Copy link
Copy Markdown
Contributor Author

The red Build Pi image check is not from this branch. It fails in the image's apt-get update, before any Kolibri code runs:

E: The repository 'https://apt.learningequality.org stable Release' does not have a Release file.
### Error: "apt-get" returned 100, cleaning up...

https://apt.learningequality.org/dists/stable/Release still returns 404. Tracked in #15149.

@rtibblesbot
rtibblesbot force-pushed the issue-15059-203220 branch 2 times, most recently from 177f25b to c8e86f3 Compare August 7, 2026 23:13
@rtibblesbot

Copy link
Copy Markdown
Contributor Author

Update: https://apt.learningequality.org/dists/stable/Release now returns 200 (InRelease last-modified 2026-08-08 01:20 UTC). This run failed at 23:23 UTC on 08-07, ~2h before the repo started publishing — the red check is stale, not live. It will clear on the next push.

@radinamatic

Copy link
Copy Markdown
Member

@rtibblesbot Reiterating here the need for a concise reviewer guidance specific for Manual QA in the PR body.

It might be the best to, if the PR requires manual QA, always specify testing workflows separately (under their own headings): one for the code review, and another suitable for manual QA reviewers, not engineers.

Code review

  • ...
  • ...

Manual QA

  • ...
  • ...

@rtibblesbot

Copy link
Copy Markdown
Contributor Author

Split into ## Code review and ## Manual QA in the description.

Manual QA is now written for a non-engineer: no code identifiers, five highest-risk workflows spelled out as steps, then the sweep list for the rest of the plugin. Behaviour is unchanged everywhere, so the whole plugin does need a pass — the five are where a regression would hide.

rtibblesbot and others added 5 commits August 10, 2026 08:23
The shared resources get _v2 variants over request, with the originals
left intact for consumers not yet migrated. Those built on
getListEndpoint resolve with response.data instead of the whole axios
response, so their call sites unwrap.

LearnerCourseResource is learn-only, so getResumeData is rewritten in
place onto request.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
force is dropped from useLearnerResources' four public fetchers. Without
the Model/Collection cache it bypassed nothing, so advertising it would
promise behaviour the code no longer has. ClassAssignmentsPage's 30s
poll is unaffected: every tick hits the network either way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fetchLessonResources stays: it targets usercontentnode_list, a different
resource name than request can resolve, and already bypasses the
Model/Collection layer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CustomContentRenderer's sendRandomCollection still reads .results off
what the random endpoint returns as a bare array. That expression was
already undefined before this change, and stays undefined after; it is a
pre-existing bug in the sandbox custom-channel API, left for its own
issue.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
With no Model/Collection cache there is nothing stale for the quiz
renderers' LearnerClassroomResource.clearCache() to clear: the next
list() always hits the network.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@pcenov

pcenov commented Aug 11, 2026

Copy link
Copy Markdown
Member

Thanks @AlexVelezLl - no regressions observed while manually testing the Learn plugin - good to go!

@radinamatic radinamatic left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Manual QA passes, good to go! :shipit:

@AlexVelezLl AlexVelezLl left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks all, lgtm!

@AlexVelezLl
AlexVelezLl merged commit 60d1ba5 into learningequality:develop Aug 11, 2026
92 checks passed
@rtibblesbot
rtibblesbot deleted the issue-15059-203220 branch August 11, 2026 15:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

APP: Learn Re: Learn App (content, quizzes, lessons, etc.) DEV: frontend SIZE: medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Migrate learn to the new Resource-layer methods

4 participants