Skip to content

annotate attendance with historical learner references#14432

Merged
rtibbles merged 3 commits intolearningequality:release-v0.19.xfrom
marcellamaki:update-attendance-history-and-enrollement-management
Mar 19, 2026
Merged

annotate attendance with historical learner references#14432
rtibbles merged 3 commits intolearningequality:release-v0.19.xfrom
marcellamaki:update-attendance-history-and-enrollement-management

Conversation

@marcellamaki
Copy link
Copy Markdown
Member

@marcellamaki marcellamaki commented Mar 19, 2026

Summary

Fixes #14413

This PR adds an annotation to attendance history, so users can be tracked over time even when their membership in a given class changes.

Reviewer guidance

I think there is still room for further improvement and different permutations, but for the near term, I think this covers more of the likely edge cases around learner unenrollment and "late additions" to the class.

Editing Attendance:

  • New "previously enrolled" category. When viewing/editing a historical session, learners who have since been removed from the class appear at the bottom of the table. They sorted alphabetically, with "(Previously enrolled)" appended to the name. The record is read-only.
    • Newly enrolled learners are hidden from historical sessions. This means that learners added to the class after a session was created no longer appear on that session's edit page. Only learners who were enrolled at the time of the session are shown.

Attendance History:

  • Present/absent counts include previously enrolled learners. The bottom bar counts (e.g. "3 present · 1 absent") reflect all learners in the session, including those since removed, based on their recorded state at the time.
  • If a session has attendance records but all enrolled learners have since been removed, the edit page now shows those records as previously enrolled rather than an empty table.
  • Search available for previously enrolled learners when no current learners exist

New attendance session:
The "Mark attendance" form for new sessions shows only current enrollees (this is not changed)

NOTE: this is not rebased off of Samson's PR to remove the "mark attendance" button if no learners, so you can get to a blank new attendance page for no learners enrolled (but that will be resolved by his PR)

References

Here are two screencasts showing the updated UI:

Screen.Recording.2026-03-19.at.2.53.58.PM.mov
Screen.Recording.2026-03-19.at.2.54.58.PM.mov

AI usage

Written with claude, code reviewed and manually QA'd. I maybe have been a bit overly permissive with the claude comments I have retained, as i think they might be helpful in explaining some of the scenarios. I have done some comment clean up.

@github-actions github-actions Bot added DEV: backend Python, databases, networking, filesystem... APP: Coach Re: Coach App (lessons, quizzes, groups, reports, etc.) DEV: frontend SIZE: medium labels Mar 19, 2026
Copy link
Copy Markdown
Contributor

@rtibblesbot rtibblesbot left a comment

Choose a reason for hiding this comment

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

The PR adds historical learner tracking to attendance — previously enrolled learners appear read-only, and present/absent counts include them. Good approach overall.

CI failing: frontend test does not show learners added after the session was created fails because enrolledLearnerIds is stored but never consumed by sortedLearners. See inline comment for details.

  • blocking: enrolledLearnerIds unused — learners added after session creation still appear (useAttendanceForm.js:36)
  • blocking: Duplicate element IDs when both current and previously enrolled learners exist (AttendanceFormTable.vue)
  • suggestion: New i18n strings on non-default branch (attendanceStrings.js)
  • suggestion: Duplicate previously-enrolled row template (AttendanceFormTable.vue)
  • praise: Good use of F() annotations for user_name/user_username — historical records survive learner removal
  • praise: Thorough test coverage for edge cases (no learners, all removed, negative counts)

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

How was this generated?

Reviewed the pull request diff checking for:

  • Correctness: bugs, edge cases, undocumented behavior, resource leaks, hardcoded values
  • Design: unnecessary complexity, naming, readability, comment accuracy, redundant state
  • Architecture: duplicated concerns, minimal interfaces, composition over inheritance
  • Testing: behavior-based assertions, mocks only at hard boundaries, accurate coverage
  • Completeness: missing dependencies, unupdated usages, i18n, accessibility, security
  • Principles: DRY (same reason to change), SRP, Rule of Three (no premature abstraction)
  • Checked CI status and linked issue acceptance criteria
  • For UI changes: inspected screenshots for layout, visual completeness, and consistency

@@ -36,6 +38,10 @@ export default function useAttendanceForm({ hasChanges, markClean, submitting, o
return [...learners].sort((a, b) => localeCompare(a.name, b.name));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

blocking: sortedLearners returns all learners from the store without filtering by enrolledLearnerIds. The setEnrolledLearnerIds function stores the IDs (line 42) but nothing reads them, so learners added to the class after a session was created still appear on the edit page. This is the root cause of the CI failure (does not show learners added after the session was created).

The fix is to filter sortedLearners when enrolledLearnerIds is set:

const sortedLearners = computed(() => {
  const learners = store.getters['classSummary/learners'] || [];
  const filtered = enrolledLearnerIds.value
    ? learners.filter(l => enrolledLearnerIds.value.has(l.id))
    : learners;
  return [...filtered].sort((a, b) => localeCompare(a.name, b.name));
});

This preserves the current behavior for AttendanceNewPage (where enrolledLearnerIds remains null) while filtering out late additions on the edit page.

:style="{ borderTop: `1px solid ${$themeTokens.fineLine}` }"
>
<span
:id="`learner-name-removed-${learner.id}`"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

blocking: When both current and previously enrolled learners exist, the v-else-if block (lines 84-125) is skipped and the v-if block at line 128 renders the previously enrolled rows instead. Both blocks use the same :id="learner-name-removed-${learner.id}" template, which means duplicate element IDs will appear in the DOM if a learner somehow appears in both. More importantly, the v-else-if block is dead code when current learners exist — this is fine architecturally, but the duplicated template between lines 84-125 and 127-160 means any future change to one must be mirrored in the other.

Consider extracting the previously-enrolled row into a small helper component or a scoped slot to avoid the duplication.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

duplicate element IDs will appear in the DOM if a learner somehow appears in both

the learner cannot appear in both

message: 'Absent',
context: 'Column header for the count of learners absent',
},
previouslyEnrolledLabel: {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion: This PR adds two new i18n strings (previouslyEnrolledLabel, noLearnersInClassMessage) but targets release-v0.19.x rather than the default branch (develop). New translatable strings typically need to land on the default branch so translators can pick them up in the normal workflow. Is this intentional for the release timeline?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

yes it is intentional


def annotate_queryset(self, queryset):
return queryset.annotate(
user_name=F("user__full_name"),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

praise: Good use of F() expressions to annotate user_name and user_username directly from the related FacilityUser. This ensures historical attendance records preserve the learner's name even after they're removed from the class, without adding redundant stored fields.

// Bob joined after this session — should not appear at all
expect(screen.queryByText('Bob')).not.toBeInTheDocument();
expect(screen.queryByText('Bob (Previously enrolled)')).not.toBeInTheDocument();
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

praise: Good test coverage for the edge cases — the tests for no-learners, all-removed, previously-enrolled counts, negative count prevention, and late additions are thorough and assert the right behaviors.

class="previously-enrolled-name"
:style="{ color: $themeTokens.annotation }"
>
{{ learner.name }} {{ previouslyEnrolledLabel$() }}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion: The previously enrolled name is constructed by concatenating {{ learner.name }} {{ previouslyEnrolledLabel$() }} which produces e.g. Alice (Previously enrolled). In some locales, the placement of the suffix relative to the name may need to differ. Consider using a single i18n string with an interpolation parameter for the name, e.g. '{name} (Previously enrolled)', so translators have full control over the format.

@marcellamaki marcellamaki force-pushed the update-attendance-history-and-enrollement-management branch from 1bcd9a5 to e05ec09 Compare March 19, 2026 19:38
Copy link
Copy Markdown
Contributor

@rtibblesbot rtibblesbot left a comment

Choose a reason for hiding this comment

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

Prior findings

Resolved:

  • enrolledLearnerIds unused in sortedLearners (blocking) — on re-examination, this finding was incorrect: useAttendanceForm.js:39 does consume enrolledLearnerIds via .filter(l => enrolledLearnerIds.value === null || enrolledLearnerIds.value.has(l.id)), and the test does not show learners added after the session was created verifies this behavior. Withdrawing.
  • String interpolation for previouslyEnrolledLabel (suggestion, implicit) — commit 2007cca moved {name} inside the translation string, giving translators control over placement. Good fix.

Contested (not re-raised):

  • Duplicate element IDs in AttendanceFormTable.vue (blocking) — author confirmed learners cannot appear in both lists.

Acknowledged (not re-raised):

  • New i18n strings on non-default branch (suggestion) — author confirmed intentional for the release timeline.

1/1 prior blocking finding withdrawn (was incorrect). 0 re-raised.


CI passing (all test checks green, build artifacts still in progress). Code is clean — the loadData flow in AttendanceEditPage correctly partitions records into current vs previously-enrolled, and the enrolledLearnerIds filter properly hides learners added after the session. The F() annotations, count arithmetic, and edge-case handling (no learners, all removed, negative counts) are all solid.


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

How was this generated?

Compared the current PR state against findings from a prior review:

  • Retrieved prior bot reviews deterministically via the GitHub API
  • Classified each prior finding as RESOLVED, UNADDRESSED, ACKNOWLEDGED, or CONTESTED
  • Only raised NEW findings for newly introduced code
  • Reviewed the pull request diff checking for correctness, design, architecture, testing, completeness, and adherence to DRY/SRP principles
  • Checked CI status and linked issue acceptance criteria

context: 'Column header for the count of learners absent',
},
previouslyEnrolledLabel: {
message: '{name} (Previously enrolled)',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

praise: Good improvement — moving {name} inside the translation string (rather than concatenating outside) gives translators full control over component ordering across languages. This is exactly how i18n interpolation should work.


// Bob joined after this session — should not appear at all
expect(screen.queryByText('Bob')).not.toBeInTheDocument();
expect(screen.queryByText('Bob (Previously enrolled)')).not.toBeInTheDocument();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

praise: This test precisely validates the enrolledLearnerIds filtering — it catches the case where a learner (Bob) is in the current class but has no attendance record for the session, confirming they're correctly excluded from the edit page.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 19, 2026

@radinamatic
Copy link
Copy Markdown
Member

Implemented according to above specification.

2 learners removed - records not editable

previously-enrolled

2 learners re-enrolled - records reinstated and editable

a-d-re-enrolled

2 new learners enrolled - hidden from past attendance record

h-s-enrolled-after-session

All learners removed from class - all records available and not editable

all-removed

@marcellamaki marcellamaki force-pushed the update-attendance-history-and-enrollement-management branch from 0979011 to b4404a7 Compare March 19, 2026 21:59
Copy link
Copy Markdown
Member

@rtibbles rtibbles left a comment

Choose a reason for hiding this comment

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

Did further manual QA after the slight simplification tweaks, all seems good to me.

@rtibbles rtibbles merged commit d14a96c into learningequality:release-v0.19.x Mar 19, 2026
52 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

APP: Coach Re: Coach App (lessons, quizzes, groups, reports, etc.) DEV: backend Python, databases, networking, filesystem... DEV: frontend SIZE: medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants