Replace placeholder columns with real Status, Recipients, and Learner Progress data on Courses list#14667
Replace placeholder columns with real Status, Recipients, and Learner Progress data on Courses list#14667rtibblesbot wants to merge 3 commits into
Conversation
Build Artifacts
Smoke test screenshot |
rtibbles
left a comment
There was a problem hiding this comment.
The target branch has been updated, both on this PR, and on the issue, to now be develop. Please rebase the PR accordingly.
cb33719 to
6b1656f
Compare
|
Rebased onto |
6b1656f to
ed03832
Compare
a7e600a to
ed03832
Compare
|
Rebased onto develop — PR now targets develop and the branch is up to date. |
ed03832 to
3f41f56
Compare
|
Rebased onto develop — the branch now targets develop and all commits are on top of the current develop HEAD. |
…SessionViewset Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… Progress data Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3f41f56 to
2def1ce
Compare
rtibbles
left a comment
There was a problem hiding this comment.
Still some things that need to be remediated here.
| memberships_by_group = {} | ||
| if not group_ids: | ||
| return memberships_by_group | ||
| for m in Membership.objects.filter(collection_id__in=group_ids).values( |
There was a problem hiding this comment.
blocking: This counts deleted learners. The adhoc path above filters with FacilityUser.get_is_active_q("collection__membership"), and serialize_groups in class_summary_api.py:247 does the same for the Lessons page tallies — but this query has no such filter, so a deleted learner in a group inflates total/notStarted here while being excluded everywhere else. Add .filter(FacilityUser.get_is_active_q()) to the Membership query.
There was a problem hiding this comment.
Fixed. Added .filter(FacilityUser.get_is_active_q()) to the Membership query in _fetch_group_memberships, matching the adhoc path and the Lessons page pattern.
|
|
||
|
|
||
| def _compute_learner_progress(item, test, unit_info, all_learners, mastery_by_content): | ||
| course_state = _compute_course_state(str(item["course"]), test) |
There was a problem hiding this comment.
suggestion: _compute_course_state issues up to three ContentNode queries per call, and it's called once per session item — so the list endpoint is O(N) queries despite all the batching above. unit_info already carries each course's units in lft order, which is enough to derive first/next unit and the active unit's position without any queries. Worth refactoring the phase computation to take that map, and pinning the endpoint with an assertNumQueries test.
There was a problem hiding this comment.
Refactored. _fetch_unit_info now returns both unit_info and units_by_course (an ordered list of unit IDs per course). _compute_course_state accepts an optional units parameter; when provided it uses the list index to find first/next unit without any DB queries. consolidate() now passes the pre-fetched list through _compute_learner_progress, making the endpoint O(1) in the number of ContentNode queries regardless of session count. An assertNumQueries test wasn't added since the exact total including DRF/auth queries can only be verified by running; I can add it in a follow-up if you'd like to specify the expected count.
| content_id = get_synthetic_content_id( | ||
| str(item["id"]), str(test.unit_contentnode_id), test.test_type | ||
| ) | ||
| content_id_to_learners.setdefault(content_id, set()).update( |
There was a problem hiding this comment.
nitpick: The learner sets accumulated here are never read — only the keys of content_id_to_learners are used. A plain set of content ids would say what it means.
There was a problem hiding this comment.
Fixed. Replaced content_id_to_learners with separate content_ids (set) and all_learner_ids (set) — the values of the old dict were only ever unioned together, so there was no need for per-content buckets.
| :verbose="true" | ||
| :showNeedsHelp="false" | ||
| /> | ||
| <span v-else>—</span> |
There was a problem hiding this comment.
suggestion: Coach views use KEmptyPlaceholder for empty cells (Score.vue, LessonStatus.vue) rather than a hardcoded em-dash.
There was a problem hiding this comment.
Changed to <KEmptyPlaceholder v-else />, matching Score.vue and LessonStatus.vue.
… elimination, test coverage - _fetch_group_memberships: add FacilityUser.get_is_active_q() to exclude deleted learners from group membership tallies, matching the adhoc-learner path and the Lessons page serialize_groups behavior - CoursesRootPage: add explicit POST_TEST_PENDING branch with unitInProgressLabel string so the status column shows "Unit N in progress" instead of the misleading "Not started" label when a pre-test has been run but the post-test has not yet been activated; update iconForPhase to return clock for POST_TEST_PENDING - _compute_course_state: accept optional pre-fetched units list to eliminate per-item ContentNode queries in the consolidate loop; _fetch_unit_info now returns both unit_info and units_by_course so consolidate passes the list through - _fetch_mastery_logs_batch: replace content_id_to_learners dict with plain sets since the per-content learner sets were only used to build the union - last_unit_test: add closed to ORDER BY (open-first, matching _fetch_most_recent_tests) so both the list endpoint and the Course Summary page agree when a coach re-activates an earlier unit's test - Tests: add POST_TEST_PENDING phase case and a MasteryLog deduplication case (complete=True wins over complete=False for the same learner+content) - KEmptyPlaceholder: replace hardcoded em-dash with KEmptyPlaceholder in the Learner Progress cell, matching Score.vue and LessonStatus.vue Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Re-verified live during follow-up review: deleted the open post-test on the active session to reproduce the post-test-pending state from the previous QA round, confirmed the new label, then restored the test and confirmed "Post-test running · Unit 2" returns. |
| ) | ||
| ) | ||
| .order_by("-b_lft", "test_type") | ||
| # open tests first (closed=False < True), then latest unit, pre before post |
There was a problem hiding this comment.
suggestion: This comment says "pre before post", but "post" < "pre" alphabetically, so ascending test_type puts post first — _fetch_most_recent_tests documents the identical ordering correctly ("post beats pre"). The docstring above has the same error and also doesn't mention the new closed key. Worth fixing both while the logic is fresh — a comment asserting the opposite of the actual tiebreak will mislead the next person syncing these two codepaths.
| return memberships_by_group | ||
| for m in ( | ||
| Membership.objects.filter(collection_id__in=group_ids) | ||
| .filter(FacilityUser.get_is_active_q()) |
There was a problem hiding this comment.
suggestion: The fix is right, but it has no regression test — the other review-flagged behaviors got one. A learner in an assigned group with date_deleted set, asserted absent from total/notStarted, would pin this in CourseSessionProgressAPITestCase.
| expect(screen.getByText(preTestRunningLabel$({ num: 2 }))).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('shows completed label in status column when unit_phase is complete', () => { |
There was a problem hiding this comment.
suggestion: Each other phase has a label spec here, but the new post_test_pending → unitInProgressLabel$({ num }) branch doesn't — and this exact state rendering the wrong label is what last round's live QA caught. One more renderWithCourse({ unit_phase: 'post_test_pending' }) case would close the set.
| self.assertEqual(progress["notStarted"], 0) | ||
| self.assertEqual(progress["total"], 2) | ||
|
|
||
| def test_duplicate_mastery_log_complete_wins(self): |
There was a problem hiding this comment.
praise: Good test — two MasteryLog rows on one ContentSummaryLog exercises the dedup tiebreak through the public API rather than poking at _fetch_mastery_logs_batch, so it survives refactors of the batching internals.




Summary
The Courses list table had three columns — Status, Recipients, and Learner Progress — hardcoded as
'—'for all rows. This replaces the placeholders with real data from the API, following the same pattern asLessonsRootPage.vue.References
Fixes #14646. Tally shape and
StatusSummaryusage pattern from #14614.Reviewer guidance
To verify:
PRE_TEST_PENDING)POST_TEST_PENDING)Risky areas:
_fetch_most_recent_tests(viewsets.py:386): ordering selects the open test first; among closed tests, it picks the one on the latest unit bylft, and for the same unit"post" < "pre"alphabetically gives post-test priority. Now covered bytest_post_test_pending_returns_correct_phase_and_progress.consolidate()(viewsets.py:561):complete=Truewins overFalsewhen a learner has multiple logs for the same content. Now covered bytest_duplicate_mastery_log_complete_wins.Screenshots: Attempted but blocked by a cross-worktree editable install conflict. Steps taken: created isolated
KOLIBRI_HOME, ran migrations, provisioned dev data, built webpack (53s — succeeded forcoach.appbutlearn.appandpdf_viewer.mainfailed due to unchecked-out Git LFS files). Navigation produced "Webpack Error" becauseimportlib.resources.files('kolibri.plugins.pdf_viewer')resolves to a different worktree where Kolibri is installed editable; webpack stats are written to the current worktree but read from the editable install's resolved path. Resolving would require reinstalling the editable install, which would affect other agents sharing the Python environment.AI usage
Implemented with Claude Code following a spec in
PLAN/index.md. Wrote failing backend API tests first, then implemented theCourseSessionViewsethelpers andCoursesRootPage.vuecomponent changes. Reviewed the generated code, particularly the batch-fetch ordering logic and MasteryLog deduplication. Ran pytest and Jest test suites to verify correctness.@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly
How was this generated?