Skip to content

PR 6: refactor: per-area small refactors (CTS, Scheduler, Students, CMS)#179

Open
rlorenzo wants to merge 6 commits into
mainfrom
refactor/per-area-small
Open

PR 6: refactor: per-area small refactors (CTS, Scheduler, Students, CMS)#179
rlorenzo wants to merge 6 commits into
mainfrom
refactor/per-area-small

Conversation

@rlorenzo

@rlorenzo rlorenzo commented May 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Per-area dedup driven by jscpd findings on the original code-anaylsis branch: 4 self-contained refactors across CTS, Scheduler, Students, and CMS (11 files). The Students refactor also uncovered and fixed a photo-gallery bug (see below), so this PR carries a behavior fix and new regression tests in addition to the dedup.

(This was formerly "Part 6 of 6" of a stack; PRs #174 through #178 are all merged to main, and this branch is rebased off the latest main, so it now stands alone.)

Commits

  • refactor(cts): collapse hardcoded CourseStudents rows into a v-for: replace 3 near-identical <tr> blocks with a single v-for.
  • refactor(scheduler): share schedule filter LINQ via IScheduleEntity: introduce IScheduleEntity + ScheduleQueryExtensions so the by-clinician / by-rotation / by-week scheduling queries share their filter LINQ.
  • refactor(students): consolidate StudentGroupService photo + Ross helpers: extract BuildStudentPhotoListAsync, FormatStudentDisplayName, FormatGroupAssignment, ResolvePhotoUrl, and GetActiveRossIamIdsAsync so the by-class-level / by-group / by-course paths no longer hand-roll the same logic. This commit also includes:
    • Photo gallery bug fix. The gallery was returning zero students for every filter. Root cause: the queries projected into a StudentBaseRecord constructor and then applied OrderBy / Where, which EF Core cannot translate; the resulting runtime InvalidOperationException was swallowed by a catch and surfaced as an empty (but 200 OK) result. Fixed by moving ordering and filtering ahead of the projection and rethrowing with context instead of silently returning empty.
    • EF.Parameter() on large Contains lists. The Ross IAM-id and enrolled-PIDM lookups now wrap their collections in EF.Parameter(...) so SQL Server translates them via OPENJSON (per the >10-item guideline) rather than inlining hundreds of literals.
    • Regression tests (test/Students/StudentGroupServiceQueryTests.cs): SQLite-backed tests that exercise all three query paths end to end, so the projection-translation failure cannot silently regress again.
  • refactor(cms): reduce Codecs to UU-only and cover with tests: collapse the UU encode/decode methods onto shared helpers (DecodeWithMap, EncodeWithMap), delete the unused XX variants and their maps (ColdFusion only ever uses the default UU encoding, so XX was dead in both the legacy app and here), and add CodecsTests. UUEncode has no caller yet but is kept for the CMS migration, where new files must store their AES key UU-encoded so legacy CF can decrypt them during the parallel-run period.

Test plan

  • CI green
  • CTS course students table renders correctly (5+ students)
  • Clinical Scheduler: by-clinician / by-rotation / by-week views still filter the same set
  • Students photo gallery: by-class-level / by-group / by-course flows match prior behavior; Ross students excluded/included as before (verified live: 155 V3 students with Ross excluded, 156 with Ross included; new StudentGroupServiceQueryTests cover all three paths in CI)
  • CMS Codecs: UU round-trip and canonical wire format ("Cat" / "#0V%T") now covered by automated tests (CodecsTests, run in CI); XX path removed

@coderabbitai

coderabbitai Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds IScheduleEntity and ApplyScheduleFilters; refactors Codecs to parameterized EncodeWithMap/DecodeWithMap and adds UU tests; consolidates StudentGroupService photo/Ross IAM logic; converts CourseStudents.vue table to data-driven rendering.

Changes

Schedule Filtering Abstraction

Layer / File(s) Summary
IScheduleEntity contract and model implementations
web/Models/CTS/IScheduleEntity.cs, web/Models/CTS/InstructorSchedule.cs, web/Models/CTS/StudentSchedule.cs
IScheduleEntity interface exposes MothraId, RotationId, ServiceId, WeekId, DateStart/DateEnd, and Week; InstructorSchedule and StudentSchedule implement it.
ScheduleQueryExtensions with ApplyScheduleFilters
web/Areas/ClinicalScheduler/Services/ScheduleQueryExtensions.cs
ApplyScheduleFilters extension conditionally composes LINQ where clauses for rotation, service, week, class year, Mothra ID, and date-range overlap based on nullable inputs.
Service adoption of ApplyScheduleFilters
web/Areas/ClinicalScheduler/Services/InstructorScheduleService.cs, web/Areas/ClinicalScheduler/Services/StudentScheduleService.cs
InstructorScheduleService and StudentScheduleService replace inline per-parameter where chains with single ApplyScheduleFilters calls.

Codec Parameterization and Testing

Layer / File(s) Summary
Codecs.cs parameterized encode/decode
web/Areas/CMS/Data/Codecs.cs
UUDecode and UUEncode public entry points; internal DecodeWithMap/EncodeWithMap accept lookup tables and route character mapping through MapByte with FormatException validation.
CodecsTests comprehensive suite
test/CMS/CodecsTests.cs
xUnit tests validate encode→decode round-trips for multiple lengths (including tails), binary safety across 0–255 bytes, canonical UU line for "Cat", empty-input behavior, null-argument ParamName assertions, and malformed-input FormatException cases.

CourseStudents Table Data-Driven Rendering

Layer / File(s) Summary
Data-driven table and select bindings
VueApp/src/CTS/pages/CourseStudents.vue
loggedInStudents array replaces hardcoded rows; table body uses v-for with dynamic Assess Competency route/aria-label and student name/time; Remove button aria-label made dynamic; select v-model repositioned.

StudentGroupService Consolidation

Layer / File(s) Summary
StudentBaseRecord projection and helper consolidation
web/Areas/Students/Services/StudentGroupService.cs
StudentBaseRecord centralizes row shape; BuildStudentPhotoListAsync batches photo URL resolution and constructs StudentPhoto results; GetActiveRossIamIdsAsync centralizes Ross IAM ID lookup; ResolvePhotoUrl now falls back to defaultPhotoUrl; FormatGroupAssignment rewritten as explicit if/else-if.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • bsedwards
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 34.48% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately summarizes the core change: four focused refactors across CTS, Scheduler, Students, and CMS areas to reduce code duplication driven by jscpd findings.
Description check ✅ Passed The description is detailed and directly related to the changeset, covering the four refactors, a bug fix, optimization, and test additions with clear commit messages and test plan.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/per-area-small

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@rlorenzo rlorenzo force-pushed the refactor/per-area-small branch from 25b942b to d0b2e7f Compare May 5, 2026 07:39
@rlorenzo rlorenzo force-pushed the refactor/dead-code-and-shared-chrome branch from ebe32ec to 75abeb2 Compare May 5, 2026 07:39
@rlorenzo rlorenzo force-pushed the refactor/per-area-small branch from d0b2e7f to 0ea0117 Compare May 5, 2026 14:47
@rlorenzo rlorenzo force-pushed the refactor/dead-code-and-shared-chrome branch 5 times, most recently from d26c5d8 to 8cbfb69 Compare May 7, 2026 15:57
@rlorenzo rlorenzo force-pushed the refactor/per-area-small branch from 0ea0117 to 0fa6909 Compare May 7, 2026 15:57
@rlorenzo rlorenzo force-pushed the refactor/dead-code-and-shared-chrome branch 2 times, most recently from 0d62a4e to a0c55c4 Compare May 9, 2026 05:22
@rlorenzo rlorenzo force-pushed the refactor/per-area-small branch from 0fa6909 to 5cbd5f4 Compare May 9, 2026 05:35
@rlorenzo rlorenzo force-pushed the refactor/dead-code-and-shared-chrome branch from a0c55c4 to d8a3735 Compare May 9, 2026 17:27
@rlorenzo rlorenzo force-pushed the refactor/per-area-small branch from 5cbd5f4 to 8eb247a Compare May 9, 2026 17:27
@rlorenzo rlorenzo force-pushed the refactor/dead-code-and-shared-chrome branch from d8a3735 to ddd64e7 Compare May 11, 2026 16:29
@rlorenzo rlorenzo force-pushed the refactor/per-area-small branch from 8eb247a to a461dcd Compare May 11, 2026 16:30
@rlorenzo rlorenzo force-pushed the refactor/dead-code-and-shared-chrome branch from ddd64e7 to 66fe537 Compare May 12, 2026 02:33
@rlorenzo rlorenzo force-pushed the refactor/per-area-small branch from a461dcd to 6d0722d Compare May 12, 2026 03:18
@rlorenzo rlorenzo force-pushed the refactor/dead-code-and-shared-chrome branch from 66fe537 to d09da71 Compare May 21, 2026 06:36
Base automatically changed from refactor/dead-code-and-shared-chrome to main May 21, 2026 07:19
@rlorenzo rlorenzo force-pushed the refactor/per-area-small branch from 6d0722d to 9c30e16 Compare May 21, 2026 08:02
@codecov-commenter

codecov-commenter commented May 21, 2026

Copy link
Copy Markdown

Bundle Report

Changes will decrease total bundle size by 881 bytes (-0.04%) ⬇️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
viper-frontend-esm 2.13MB -881 bytes (-0.04%) ⬇️

Affected Assets, Files, and Routes:

view changes for bundle: viper-frontend-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/CourseStudents-*.js -881 bytes 1.95kB -31.08%

Files in assets/CourseStudents-*.js:

  • ./src/CTS/pages/CourseStudents.vue → Total Size: 145 bytes

@codecov-commenter

codecov-commenter commented May 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 72.64574% with 61 lines in your changes missing coverage. Please review.
✅ Project coverage is 44.19%. Comparing base (f45f6b7) to head (829c1dd).
⚠️ Report is 62 commits behind head on main.

Files with missing lines Patch % Lines
...nicalScheduler/Services/ScheduleQueryExtensions.cs 0.00% 31 Missing ⚠️
web/Areas/Students/Services/StudentGroupService.cs 81.69% 25 Missing and 3 partials ⚠️
...calScheduler/Services/InstructorScheduleService.cs 0.00% 1 Missing ⚠️
...inicalScheduler/Services/StudentScheduleService.cs 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #179      +/-   ##
==========================================
+ Coverage   43.02%   44.19%   +1.17%     
==========================================
  Files         881      893      +12     
  Lines       51436    51491      +55     
  Branches     4812     4791      -21     
==========================================
+ Hits        22131    22758     +627     
+ Misses      28779    28172     -607     
- Partials      526      561      +35     
Flag Coverage Δ
backend 44.33% <72.64%> (+1.23%) ⬆️
frontend 41.32% <ø> (-0.16%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@rlorenzo rlorenzo force-pushed the refactor/per-area-small branch from 9c30e16 to 743f4e5 Compare May 21, 2026 16:07
@rlorenzo rlorenzo force-pushed the refactor/per-area-small branch from 743f4e5 to b8152a2 Compare June 1, 2026 20:07
@rlorenzo

rlorenzo commented Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Full review triggered.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
web/Areas/Students/Services/StudentGroupService.cs (1)

407-433: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add .AsNoTracking() to read-only query.

Same issue. Insert .AsNoTracking() before .OrderBy().

⚡ Suggested fix
                                sg != null ? sg.StudentgrpV3grp : null);

         // Apply group filtering if specified
         if (!string.IsNullOrEmpty(groupType) && !string.IsNullOrEmpty(groupId))
         {
             query = groupType.ToLower() switch
             {
                 "eighths" => query.Where(s => s.EighthsGroup == groupId),
                 "twentieths" => query.Where(s => s.TwentiethsGroup == groupId),
                 "teams" => query.Where(s => s.TeamNumber == groupId),
                 "v3specialty" => query.Where(s => s.V3SpecialtyGroup == groupId),
                 _ => query
             };
         }

-var students = await query.OrderBy(s => s.LastName).ThenBy(s => s.FirstName).ToListAsync();
+var students = await query.AsNoTracking().OrderBy(s => s.LastName).ThenBy(s => s.FirstName).ToListAsync();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/Areas/Students/Services/StudentGroupService.cs` around lines 407 - 433,
The LINQ query building a sequence of StudentBaseRecord is read-only and should
use AsNoTracking to avoid EF change tracking overhead; update the code that
constructs "query" (the projection creating new StudentBaseRecord in
StudentGroupService) to call .AsNoTracking() on the IQueryable before applying
.OrderBy(...).ThenBy(...)/ToListAsync(), preserving any groupType filtering
logic (the switch) so call .AsNoTracking() on the final query instance just
prior to ordering and materialization.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/CMS/CodecsTests.cs`:
- Line 4: The namespace declared as Test.CMS doesn't match the project's
expected root namespace; update the namespace declaration to Viper.test.CMS
(replace the Test.CMS namespace token) so the class/test types in this file use
the Viper.test.CMS namespace and satisfy the ReSharper/CI checks.

In `@web/Areas/Students/Services/StudentGroupService.cs`:
- Around line 765-772: The query against _sisContext.StudentDesignations in
StudentGroupService should be executed as a read-only query; insert
.AsNoTracking() immediately after _sisContext.StudentDesignations (before the
existing .Where(...) calls) so the EF Core change tracker is not used for this
read-only projection (retain the rest of the chain:
.Where(...).Select(...).Where(...).Distinct().ToListAsync()).
- Line 738: Remove the null-forgiving operator from student.MailId in
StudentGroupService.cs and handle nullable MailId properly: either filter the
students iteration to only include those with MailId != null before mapping, or
assign MailId = student.MailId (no !) and ensure the consumer (e.g., the
StudentPhoto model or downstream code) accepts a nullable MailId; locate the
mapping where MailId = student.MailId! and replace it with a safe nullable-aware
approach consistent with how MailId is handled elsewhere (see other assignments
around lines with StudentPhoto.MailId).
- Around line 113-126: The LINQ query building StudentBaseRecord instances is
read-only but currently tracks entities; update the query used in
StudentGroupService (the variable named query that projects to
StudentBaseRecord) to call .AsNoTracking() before the OrderBy/ThenBy and
ToListAsync so EF Core doesn't enable change tracking for this read-only
projection; locate the projection creating new StudentBaseRecord and insert
.AsNoTracking() on that IQueryable prior to .OrderBy(s => s.LastName).ThenBy(s
=> s.FirstName).ToListAsync().
- Around line 341-354: The query projection creating StudentBaseRecord from
queryBase should be treated as read-only—modify the LINQ chain so that the
IQueryable returned by the Select (the variable named query) uses AsNoTracking()
before applying OrderBy/ThenBy; locate the Select(...) producing
StudentBaseRecord in StudentGroupService (the variable query) and insert
.AsNoTracking() on that query before calling OrderBy(s => s.LastName).ThenBy(s
=> s.FirstName).ToListAsync().

---

Outside diff comments:
In `@web/Areas/Students/Services/StudentGroupService.cs`:
- Around line 407-433: The LINQ query building a sequence of StudentBaseRecord
is read-only and should use AsNoTracking to avoid EF change tracking overhead;
update the code that constructs "query" (the projection creating new
StudentBaseRecord in StudentGroupService) to call .AsNoTracking() on the
IQueryable before applying .OrderBy(...).ThenBy(...)/ToListAsync(), preserving
any groupType filtering logic (the switch) so call .AsNoTracking() on the final
query instance just prior to ordering and materialization.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d79bf74a-c6b5-4d4d-84dc-7f186950c9ab

📥 Commits

Reviewing files that changed from the base of the PR and between ed1f48b and b8152a2.

📒 Files selected for processing (10)
  • VueApp/src/CTS/pages/CourseStudents.vue
  • test/CMS/CodecsTests.cs
  • web/Areas/CMS/Data/Codecs.cs
  • web/Areas/ClinicalScheduler/Services/InstructorScheduleService.cs
  • web/Areas/ClinicalScheduler/Services/ScheduleQueryExtensions.cs
  • web/Areas/ClinicalScheduler/Services/StudentScheduleService.cs
  • web/Areas/Students/Services/StudentGroupService.cs
  • web/Models/CTS/IScheduleEntity.cs
  • web/Models/CTS/InstructorSchedule.cs
  • web/Models/CTS/StudentSchedule.cs

Comment thread test/CMS/CodecsTests.cs Outdated
Comment thread web/Areas/Students/Services/StudentGroupService.cs Outdated
Comment thread web/Areas/Students/Services/StudentGroupService.cs Outdated
Comment thread web/Areas/Students/Services/StudentGroupService.cs Outdated
Comment thread web/Areas/Students/Services/StudentGroupService.cs Outdated
@rlorenzo rlorenzo force-pushed the refactor/per-area-small branch from d1b9d4c to b06de1c Compare June 2, 2026 08:01
@rlorenzo

rlorenzo commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@rlorenzo rlorenzo requested a review from Copilot June 2, 2026 08:23
@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Comment thread web/Areas/CMS/Data/Codecs.cs Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.

@rlorenzo

rlorenzo commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@rlorenzo rlorenzo requested a review from Copilot June 2, 2026 14:05
@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.

@rlorenzo

Copy link
Copy Markdown
Contributor Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor
Action performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Comment thread web/Areas/Students/Services/StudentGroupService.cs Outdated
Comment thread web/Areas/Students/Services/StudentGroupService.cs Outdated
rlorenzo added 4 commits June 10, 2026 09:35
Photo-gallery queries order/filter on the source before projecting so EF Core can translate them, large Contains lists use EF.Parameter (OPENJSON), and the catch blocks rethrow with context instead of masking failures. Adds SQLite-backed regression tests.
@rlorenzo rlorenzo force-pushed the refactor/per-area-small branch from b25beef to 83489ac Compare June 10, 2026 16:45
rlorenzo added 2 commits June 10, 2026 11:26
The course photo gallery threw "Cannot use multiple context instances
within a single query execution" whenever Ross students were requested
and any were active for the term: the Ross-in-course lookup joined the
Courses and AAUD DbContexts in a single query, which EF cannot
translate. Before the gallery fix this surfaced as a silently empty
gallery (the exception was swallowed); after it, as a 500.

Split it into the same two-step pattern the rest of the method uses
(course-roster PIDMs from Courses, then the Ross IAM IDs from AAUD).
Add a SQLite-backed regression test for the include-Ross course path,
which reproduces the failure on the old query and passes on the new one.
@rlorenzo

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
VueApp/src/CTS/pages/CourseStudents.vue (1)

48-84: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Replace native table row/cell markup with Quasar table components in this changed block.

Line 48 introduces a v-for on native <tr>/<td>, which violates the Vue guideline requiring Quasar components instead of HTML elements. Refactor this table rendering to q-table (or Quasar table primitives) so this section is compliant.

As per coding guidelines, "**/*.vue: Use Quasar components (q-btn, q-dialog, q-banner, etc.) instead of HTML elements."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@VueApp/src/CTS/pages/CourseStudents.vue` around lines 48 - 84, The native
<tr>/<td> loop rendering loggedInStudents must be replaced with Quasar table
primitives: use <q-table> with :rows="loggedInStudents" and a body slot (or use
QTr/QTd components) instead of v-for on <tr>; map columns for name/time and
render the action cells using the existing q-btn/q-icon inside QTd, keeping the
:key as row.id and preserving the AssessmentCompetency route construction and
aria-labels (refer to loggedInStudents, AssessmentCompetency route string, and
the q-btn/q-icon instances) so the table uses Quasar components throughout.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/Students/StudentGroupServiceQueryTests.cs`:
- Around line 200-236: Add a negative Ross fixture to the existing test
GetStudentsByCourseAsync_IncludeRoss_ReturnsRossStudentEnrolledInCourse: create
another SeedStudent + StudentDesignation pair (use a distinct pkey/pidm like
"P10"/"PIDM10" and matching IamId) and add a Roster entry for that pidm with
RosterCrn "12345" but set RosterEnrollStatus to a non-"RE" value (e.g. "WD" or
"DR"). Save to _sisContext and _coursesContext like the others, then keep the
existing call to _service.GetStudentsByCourseAsync(Term, "12345",
includeRossStudents: true) and add an assertion that this non-RE Ross student is
not present in result (e.g. assert no s with LastName matching the new seed or
that result.Count remains 2). Ensure you reference the existing SeedStudent,
StudentDesignation, Roster, _sisContext/_coursesContext and
GetStudentsByCourseAsync in the change.

In `@web/Areas/ClinicalScheduler/Services/ScheduleQueryExtensions.cs`:
- Around line 22-25: The current ScheduleQueryExtensions implementation uses
Week.WeekGradYears.Any(...) inside the Where, causing a correlated subquery;
instead preload the matching WeekIds from WeekGradYears (filter by GradYear and
Select WeekId/Distinct) and change the predicate to use
weekIds.Contains(s.WeekId). Add an overload on the
ApplyScheduleFilters/extension in ScheduleQueryExtensions that accepts an
IEnumerable<int> weekIds (or accept a nullable preloaded list) and, when
provided, apply query = query.Where(s => weekIds.Contains(s.WeekId)); keep the
original signature behavior when weekIds is null so callers that preload WeekIds
(from WeekGradYears where GradYear == classYear) can pass them in and avoid the
per-row Any().

In `@web/Areas/Students/Services/StudentGroupService.cs`:
- Around line 741-755: The GroupAssignment is being built from untrimmed
student.EighthsGroup and student.TwentiethsGroup which causes padded values to
appear even though the DTO fields are trimmed; fix by trimming those inputs
before calling FormatGroupAssignment (use student.EighthsGroup?.Trim() and
student.TwentiethsGroup?.Trim() when computing groupAssignment) so that
FormatGroupAssignment receives normalized values; ensure the existing
EighthsGroup and TwentiethsGroup assignments on the StudentPhoto DTO remain
trimmed as they are.
- Around line 447-459: The Ross-only lookup currently re-queries Rosters into
courseRosterPidms without the original RosterEnrollStatus == "RE" filter, which
can reintroduce withdrawn/waitlisted students; change the second query to reuse
the already-filtered enrolledPidms (the variable created for enrolled students)
instead of courseRosterPidms so the EF query in StudentGroupService (the
rossStudentsInCourse AAUD lookup) only considers actively enrolled PIDMs and
avoids re-adding non-enrolled Ross students.

---

Outside diff comments:
In `@VueApp/src/CTS/pages/CourseStudents.vue`:
- Around line 48-84: The native <tr>/<td> loop rendering loggedInStudents must
be replaced with Quasar table primitives: use <q-table> with
:rows="loggedInStudents" and a body slot (or use QTr/QTd components) instead of
v-for on <tr>; map columns for name/time and render the action cells using the
existing q-btn/q-icon inside QTd, keeping the :key as row.id and preserving the
AssessmentCompetency route construction and aria-labels (refer to
loggedInStudents, AssessmentCompetency route string, and the q-btn/q-icon
instances) so the table uses Quasar components throughout.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 52013b18-6c8b-49ac-997d-893b713acd14

📥 Commits

Reviewing files that changed from the base of the PR and between 63047b9 and 829c1dd.

📒 Files selected for processing (11)
  • VueApp/src/CTS/pages/CourseStudents.vue
  • test/CMS/CodecsTests.cs
  • test/Students/StudentGroupServiceQueryTests.cs
  • web/Areas/CMS/Data/Codecs.cs
  • web/Areas/ClinicalScheduler/Services/InstructorScheduleService.cs
  • web/Areas/ClinicalScheduler/Services/ScheduleQueryExtensions.cs
  • web/Areas/ClinicalScheduler/Services/StudentScheduleService.cs
  • web/Areas/Students/Services/StudentGroupService.cs
  • web/Models/CTS/IScheduleEntity.cs
  • web/Models/CTS/InstructorSchedule.cs
  • web/Models/CTS/StudentSchedule.cs

Comment on lines +200 to +236
[Fact]
public async Task GetStudentsByCourseAsync_IncludeRoss_ReturnsRossStudentEnrolledInCourse()
{
// A regular V3 student and a Ross student, both enrolled (RE) in the same course.
SeedStudent("P1", "PIDM1", "Adams", "Bob", "V3");
SeedStudent("P9", "PIDM9", "Reardon", "Chelsea", "V3");
_sisContext.StudentDesignations.Add(new StudentDesignation
{
DesignationType = "Ross",
IamId = "IAMP9", // matches SeedStudent's "IAM" + pKey
StartTerm = int.Parse(Term),
EndTerm = null
});
await _sisContext.SaveChangesAsync(TestContext.Current.CancellationToken);

foreach (var (pkey, pidm) in new[] { ("RST1", "PIDM1"), ("RST9", "PIDM9") })
{
_coursesContext.Rosters.Add(new Roster
{
RosterPkey = pkey,
RosterTermCode = Term,
RosterCrn = "12345",
RosterEnrollStatus = "RE",
RosterPidm = pidm
});
}
await _coursesContext.SaveChangesAsync(TestContext.Current.CancellationToken);

// The Ross-inclusion branch must not join the Courses and AAUD DbContexts in one
// query: EF cannot translate a cross-context join and throws at execution, which the
// service surfaces as a failure (previously swallowed into an empty gallery).
var result = await _service.GetStudentsByCourseAsync(Term, "12345", includeRossStudents: true);

Assert.Equal(2, result.Count);
Assert.Contains(result, s => s.LastName == "Adams" && !s.IsRossStudent);
Assert.Contains(result, s => s.LastName == "Reardon" && s.IsRossStudent);
}

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.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add a non-RE Ross fixture to this regression.

This only proves the happy path. Add one Ross-designated student with a non-enrolled roster status and assert it stays out of the result so the Ross-only course branch cannot drift from the main enrollment filter again.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/Students/StudentGroupServiceQueryTests.cs` around lines 200 - 236, Add a
negative Ross fixture to the existing test
GetStudentsByCourseAsync_IncludeRoss_ReturnsRossStudentEnrolledInCourse: create
another SeedStudent + StudentDesignation pair (use a distinct pkey/pidm like
"P10"/"PIDM10" and matching IamId) and add a Roster entry for that pidm with
RosterCrn "12345" but set RosterEnrollStatus to a non-"RE" value (e.g. "WD" or
"DR"). Save to _sisContext and _coursesContext like the others, then keep the
existing call to _service.GetStudentsByCourseAsync(Term, "12345",
includeRossStudents: true) and add an assertion that this non-RE Ross student is
not present in result (e.g. assert no s with LastName matching the new seed or
that result.Count remains 2). Ensure you reference the existing SeedStudent,
StudentDesignation, Roster, _sisContext/_coursesContext and
GetStudentsByCourseAsync in the change.

Comment on lines +22 to +25
if (classYear.HasValue)
{
query = query.Where(s => s.Week.WeekGradYears.Any(gy => gy.GradYear == classYear));
}

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.

🧹 Nitpick | 🔵 Trivial | 🏗️ Heavy lift

Correlated subquery on schedule filtering.

The .Any() inside .Where() creates a correlated subquery that executes for each schedule row. Per coding guidelines, pre-load WeekIds for the class year and use .Contains() instead.

♻️ Optimization approach

The current extension signature makes this optimization challenging without breaking the composable query pattern. Consider a separate async method or caller-side pre-loading:

// Caller pre-loads before calling extension
var weekIdsForClassYear = classYear.HasValue 
    ? await _context.WeekGradYears
        .Where(gy => gy.GradYear == classYear)
        .Select(gy => gy.WeekId)
        .Distinct()
        .ToListAsync()
    : null;

var query = _context.StudentSchedules
    .Include(s => s.Week)
    .AsNoTracking();

if (weekIdsForClassYear != null)
{
    query = query.Where(s => weekIdsForClassYear.Contains(s.WeekId));
}

query = query.ApplyScheduleFilters(null, mothraId, rotationId, serviceId, weekId, startDate, endDate);

Alternatively, add an overload accepting pre-loaded week IDs to avoid the signature change.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/Areas/ClinicalScheduler/Services/ScheduleQueryExtensions.cs` around lines
22 - 25, The current ScheduleQueryExtensions implementation uses
Week.WeekGradYears.Any(...) inside the Where, causing a correlated subquery;
instead preload the matching WeekIds from WeekGradYears (filter by GradYear and
Select WeekId/Distinct) and change the predicate to use
weekIds.Contains(s.WeekId). Add an overload on the
ApplyScheduleFilters/extension in ScheduleQueryExtensions that accepts an
IEnumerable<int> weekIds (or accept a nullable preloaded list) and, when
provided, apply query = query.Where(s => weekIds.Contains(s.WeekId)); keep the
original signature behavior when weekIds is null so callers that preload WeekIds
(from WeekGradYears where GradYear == classYear) can pass them in and avoid the
per-row Any().

Source: Coding guidelines

Comment on lines +447 to +459
// Get Ross students enrolled in this course. Use a two-step query (like the
// enrollment lookup above) to avoid joining the Courses and AAUD DbContexts
// in a single query, which EF cannot translate.
var courseRosterPidms = await _coursesContext.Rosters
.Where(r => r.RosterTermCode == termCode && r.RosterCrn == crn)
.Select(r => r.RosterPidm)
.Distinct()
.ToListAsync();

var rossStudentsInCourse = await _aaudContext.Ids
.Where(i => i.IdsIamId != null
&& EF.Parameter(courseRosterPidms).Contains(i.IdsPidm)
&& EF.Parameter(rossIamIds).Contains(i.IdsIamId))

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reuse enrolledPidms for the Ross-only lookup.

This second roster query drops the RosterEnrollStatus == "RE" filter from the main path, so includeRossStudents can re-add withdrawn or waitlisted Ross students to the course gallery.

Suggested fix
-                    var courseRosterPidms = await _coursesContext.Rosters
-                        .Where(r => r.RosterTermCode == termCode && r.RosterCrn == crn)
-                        .Select(r => r.RosterPidm)
-                        .Distinct()
-                        .ToListAsync();
+                    var courseRosterPidms = enrolledPidms;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/Areas/Students/Services/StudentGroupService.cs` around lines 447 - 459,
The Ross-only lookup currently re-queries Rosters into courseRosterPidms without
the original RosterEnrollStatus == "RE" filter, which can reintroduce
withdrawn/waitlisted students; change the second query to reuse the
already-filtered enrolledPidms (the variable created for enrolled students)
instead of courseRosterPidms so the EF query in StudentGroupService (the
rossStudentsInCourse AAUD lookup) only considers actively enrolled PIDMs and
avoids re-adding non-enrolled Ross students.

Comment on lines +741 to +755
var displayName = FormatStudentDisplayName(student.LastName, student.FirstName ?? string.Empty, student.MiddleName);
var (photoUrl, hasPhoto) = ResolvePhotoUrl(student.MailId, photoUrls, defaultPhotoUrl);
var groupAssignment = FormatGroupAssignment(student.EighthsGroup, student.TwentiethsGroup);

result.Add(new StudentPhoto
{
MailId = student.MailId ?? string.Empty,
FirstName = student.FirstName ?? string.Empty,
LastName = student.LastName,
DisplayName = displayName,
PhotoUrl = photoUrl,
GroupAssignment = groupAssignment,
EighthsGroup = student.EighthsGroup?.Trim(),
TwentiethsGroup = student.TwentiethsGroup?.Trim(),
TeamNumber = student.ClassLevel == "V3" ? student.TeamNumber?.Trim() : null,

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Trim the group values before formatting GroupAssignment.

EighthsGroup and TwentiethsGroup are normalized for the DTO fields, but GroupAssignment is built from the untrimmed values first. That can leave padded AAUD values in the combined label while the individual properties are clean.

Suggested fix
-                var displayName = FormatStudentDisplayName(student.LastName, student.FirstName ?? string.Empty, student.MiddleName);
-                var (photoUrl, hasPhoto) = ResolvePhotoUrl(student.MailId, photoUrls, defaultPhotoUrl);
-                var groupAssignment = FormatGroupAssignment(student.EighthsGroup, student.TwentiethsGroup);
+                var displayName = FormatStudentDisplayName(student.LastName, student.FirstName ?? string.Empty, student.MiddleName);
+                var (photoUrl, hasPhoto) = ResolvePhotoUrl(student.MailId, photoUrls, defaultPhotoUrl);
+                var eighthsGroup = student.EighthsGroup?.Trim();
+                var twentiethsGroup = student.TwentiethsGroup?.Trim();
+                var groupAssignment = FormatGroupAssignment(eighthsGroup, twentiethsGroup);
@@
-                    EighthsGroup = student.EighthsGroup?.Trim(),
-                    TwentiethsGroup = student.TwentiethsGroup?.Trim(),
+                    EighthsGroup = eighthsGroup,
+                    TwentiethsGroup = twentiethsGroup,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/Areas/Students/Services/StudentGroupService.cs` around lines 741 - 755,
The GroupAssignment is being built from untrimmed student.EighthsGroup and
student.TwentiethsGroup which causes padded values to appear even though the DTO
fields are trimmed; fix by trimming those inputs before calling
FormatGroupAssignment (use student.EighthsGroup?.Trim() and
student.TwentiethsGroup?.Trim() when computing groupAssignment) so that
FormatGroupAssignment receives normalized values; ensure the existing
EighthsGroup and TwentiethsGroup assignments on the StudentPhoto DTO remain
trimmed as they are.

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.

3 participants