<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://wiki.expertiza.ncsu.edu/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Admin</id>
	<title>Expertiza_Wiki - User contributions [en]</title>
	<link rel="self" type="application/atom+xml" href="https://wiki.expertiza.ncsu.edu/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Admin"/>
	<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Special:Contributions/Admin"/>
	<updated>2026-08-14T17:30:19Z</updated>
	<subtitle>User contributions</subtitle>
	<generator>MediaWiki 1.41.0</generator>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Score_Calculation&amp;diff=168259</id>
		<title>Score Calculation</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Score_Calculation&amp;diff=168259"/>
		<updated>2026-07-07T00:36:24Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Layer 2: ResponseMap (Round Weights) */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Score Calculation =&lt;br /&gt;
&lt;br /&gt;
Peer review grades in Expertiza are computed in three models.&lt;br /&gt;
&lt;br /&gt;
== Layer 1: Response (Item Weights) ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;Response#aggregate_questionnaire_score&amp;lt;/code&amp;gt; sums &amp;lt;code&amp;gt;answer * item.weight&amp;lt;/code&amp;gt; for all answered, scored items in a single submitted review. Section-header items, text-area items, etc., are excluded (they carry no weight).&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;Response#maximum_score&amp;lt;/code&amp;gt; computes the maximum possible score for the same set of answered items: &amp;lt;code&amp;gt;sum(item.weight) * questionnaire.max_question_score&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
== Layer 2: ResponseMap (Round Weights) ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; normalizes each round's score (&amp;lt;code&amp;gt;aggregate_questionnaire_score / maximum_score&amp;lt;/code&amp;gt;) and computes a weighted average across rounds using &amp;lt;code&amp;gt;AssignmentQuestionnaire#questionnaire_weight&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Only the latest submitted response per round is used. Rounds with no submitted response are excluded from both the numerator and denominator.&lt;br /&gt;
&lt;br /&gt;
== Layer 3: AssignmentTeam (Reviewer Reputation) ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; averages &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; across all reviewers of the team, weighted by each reviewer's reputation score (currently defaulting to 1.0 — placeholder for future Uchswas integration).&lt;br /&gt;
&lt;br /&gt;
The same computation is used by &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt; for teammate reviews.&lt;br /&gt;
&lt;br /&gt;
== Validation ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; validates that &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt; is 0 when the linked rubric contains no scored items (only &amp;lt;code&amp;gt;SectionHeader&amp;lt;/code&amp;gt; items), since a non-zero weight on an unscored rubric would produce meaningless grades.&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Score_Calculation&amp;diff=168258</id>
		<title>Score Calculation</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Score_Calculation&amp;diff=168258"/>
		<updated>2026-07-07T00:35:22Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Layer 1: Response (Item Weights) */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Score Calculation =&lt;br /&gt;
&lt;br /&gt;
Peer review grades in Expertiza are computed in three models.&lt;br /&gt;
&lt;br /&gt;
== Layer 1: Response (Item Weights) ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;Response#aggregate_questionnaire_score&amp;lt;/code&amp;gt; sums &amp;lt;code&amp;gt;answer * item.weight&amp;lt;/code&amp;gt; for all answered, scored items in a single submitted review. Section-header items, text-area items, etc., are excluded (they carry no weight).&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;Response#maximum_score&amp;lt;/code&amp;gt; computes the maximum possible score for the same set of answered items: &amp;lt;code&amp;gt;sum(item.weight) * questionnaire.max_question_score&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
== Layer 2: ResponseMap (Round Weights) ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; normalises each round's score (&amp;lt;code&amp;gt;aggregate_questionnaire_score / maximum_score&amp;lt;/code&amp;gt;) and computes a weighted average across rounds using &amp;lt;code&amp;gt;AssignmentQuestionnaire#questionnaire_weight&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Only the latest submitted response per round is used. Rounds with no submitted response are excluded from both the numerator and denominator.&lt;br /&gt;
&lt;br /&gt;
== Layer 3: AssignmentTeam (Reviewer Reputation) ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; averages &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; across all reviewers of the team, weighted by each reviewer's reputation score (currently defaulting to 1.0 — placeholder for future Uchswas integration).&lt;br /&gt;
&lt;br /&gt;
The same computation is used by &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt; for teammate reviews.&lt;br /&gt;
&lt;br /&gt;
== Validation ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; validates that &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt; is 0 when the linked rubric contains no scored items (only &amp;lt;code&amp;gt;SectionHeader&amp;lt;/code&amp;gt; items), since a non-zero weight on an unscored rubric would produce meaningless grades.&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2602._Reimplement_student_task_view&amp;diff=168255</id>
		<title>CSC/ECE 517 Spring 2026 - E2602. Reimplement student task view</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2602._Reimplement_student_task_view&amp;diff=168255"/>
		<updated>2026-07-07T00:27:59Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Reviewer reputation-weighted grading */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= CSC/ECE 517 Spring 2026 - E2602. Reimplement Student Task View =&lt;br /&gt;
&lt;br /&gt;
== About Expertiza ==&lt;br /&gt;
Expertiza is a robust, open-source web platform designed to empower educational environments that emphasize project-based learning. It streamlines collaborative activities including peer reviews, surveys, and team assignments — giving instructors tools to manage assignments and track performance, and giving students a single place to submit work, review peers, and track feedback.&lt;br /&gt;
&lt;br /&gt;
== Introduction ==&lt;br /&gt;
''This page documents the reimplementation of the Student Task view. It supersedes the earlier [[E2602]] team's version. All searches for E2602 will hit here.''&lt;br /&gt;
This iteration of E2602 hardens and extends the Student Task View: consolidating the student dashboard and detail pages into single-call REST endpoints, eliminating N+1 queries in the peer-review score heatgrid, and fixing several display/permission bugs surfaced by manual QA against real assignment data.&lt;br /&gt;
&lt;br /&gt;
=== Tech Stack ===&lt;br /&gt;
* '''Backend:''' Ruby on Rails (API-only)&lt;br /&gt;
* '''Frontend:''' React + TypeScript&lt;br /&gt;
* '''Database:''' MySQL&lt;br /&gt;
* '''Backend Testing:''' RSpec&lt;br /&gt;
* '''Frontend Testing:''' Jest, React Testing Library&lt;br /&gt;
&lt;br /&gt;
=== Key Improvements ===&lt;br /&gt;
* Eliminated N+1 queries in the score heatgrid (&amp;lt;code&amp;gt;get_team_scores&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_my_scores_data&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_answer&amp;lt;/code&amp;gt;) via &amp;lt;code&amp;gt;.includes(responses: { scores: :item })&amp;lt;/code&amp;gt; and in-memory filtering instead of re-querying preloaded associations&lt;br /&gt;
* Converted &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt;'s stylesheet to CSS Modules, fixing a Bootstrap class collision and scoping color classes (&amp;lt;code&amp;gt;c1&amp;lt;/code&amp;gt;–&amp;lt;code&amp;gt;c5&amp;lt;/code&amp;gt;) correctly&lt;br /&gt;
* Redesigned the score/feedback heatgrid with sticky Question column during horizontal scroll&lt;br /&gt;
* Added hover tooltips on score cells showing the reviewer's written feedback&lt;br /&gt;
* Rewrote &amp;lt;code&amp;gt;StudentTasks&amp;lt;/code&amp;gt; from a (mis-implemented) topic sign-up sheet into a proper course-grouped assignment dashboard&lt;br /&gt;
* Added participant-level &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; permission checks, hiding &amp;quot;Your work&amp;quot; / &amp;quot;Others' work&amp;quot; task links accordingly&lt;br /&gt;
* Refactored review grade calculation: &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; now weights each round by its &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;; &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; centralizes the weighted-average logic previously duplicated across &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Problem Statement ==&lt;br /&gt;
The previous Student Task View had several concrete defects:&lt;br /&gt;
* The score heatgrid fired one SQL query per response/score across all reviewers and rounds, producing hundreds of queries for a single page load&lt;br /&gt;
* SectionHeader rubric rows (non-scorable dividers) were incorrectly counted in the &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; column and lost their fixed position during horizontal scroll&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTasks.tsx&amp;lt;/code&amp;gt; was previously implemented as a topic sign-up sheet, not a task dashboard, and required a full rewrite&lt;br /&gt;
* The page did not respect participant-level &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; flags — all task links were always shown regardless of permission&lt;br /&gt;
* Review grade calculation treated every reviewer and every round equally, ignoring each rubric's configured &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== User Stories ==&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want to see all my assignments in one place ===&lt;br /&gt;
&lt;br /&gt;
This is the home view for a student. It shows all of the student's assignments, arranged by the course they pertain to.&lt;br /&gt;
&lt;br /&gt;
[[File:student_view_assignments_page.png|800px|thumb|center|Student View Assignments Page]]&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; returns all tasks for the logged-in student in one call&lt;br /&gt;
* Tasks are grouped by course on the dashboard&lt;br /&gt;
* Each task shows current stage and days remaining, if applicable&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want to view a single assignment's deadlines and details ===&lt;br /&gt;
&lt;br /&gt;
A student can perform several tasks for each assignment, depending upon the role they play (e.g., whether they are a submitter, a reviewer, or both.&lt;br /&gt;
&lt;br /&gt;
[[File:student_view_assignment_detail_page.png|800px|thumb|center|Student View Assignment Detail Page]]&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt; returns the task plus a unified &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt; timeline&lt;br /&gt;
* The timeline visually distinguishes completed, current, and pending stages&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/team&amp;lt;/code&amp;gt; returns teammates grouped by course&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want my team's score heatgrid to load fast and stay readable while scrolling ===&lt;br /&gt;
&lt;br /&gt;
The heatgrid shows all of the reviews that have been written of the team's submission (or, the student's submission, if this is not a team assignment).  The user can hover over any cell with a score that is underlined, meaning that the reviewer has made a comment to go along with the score.  Hovering over a cell will show the comments it contains.&lt;br /&gt;
&lt;br /&gt;
[[File:student_view_assignment_grades_page.png|800px|thumb|left|Student View Assignment View Team Grades]]&lt;br /&gt;
&amp;lt;br clear=&amp;quot;all&amp;quot; /&amp;gt;&lt;br /&gt;
Clicking on &amp;quot;Feedback&amp;quot; will show all of the feedback along with the scores.&lt;br /&gt;
[[File:student_view_assignment_feedback_page.png|800px|thumb|left|Student View Assignment View Feedbacks]]&lt;br /&gt;
&amp;lt;br clear=&amp;quot;all&amp;quot; /&amp;gt;&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* The heatgrid loads from a single &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt; call&lt;br /&gt;
* The first two columns (item number &amp;quot;&amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;&amp;quot; and Item) remain fixed during horizontal scroll, along with section headers&lt;br /&gt;
* Hovering over a score with a comment shows the corresponding reviewer's feedback as a tooltip&lt;br /&gt;
&lt;br /&gt;
=== A student who is not permitted to submit or review will not see those task links ===&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;quot;Your work&amp;quot; is hidden when &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;quot;Others' work&amp;quot; is hidden when &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== As an instructor, I want calculated review grades to reflect each rubric's configured weight ===&lt;br /&gt;
&lt;br /&gt;
Grades are calculated in &amp;lt;code&amp;gt;Response.rb&amp;lt;/code&amp;gt; (score for a particular review by a specific reviewer), &amp;lt;code&amp;gt;ResponseMap.rb&amp;lt;/code&amp;gt; (composite score for reviews in all rounds by the same reviewer) and &amp;lt;code&amp;gt;AssignmentTeam.rb&amp;lt;/code&amp;gt; (combined score for all reviews by all reviewers).&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; weights each round's normalized score by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt; both compute their averages through the same shared &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
=== Student Task Overview ===&lt;br /&gt;
A StudentTask is a task that a student performs in the course of doing an assignment, e.g., forming a team, choosing a topic, submitting work, or reviewing work. No dedicated &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; database table exists. A &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; is a plain Ruby object composed from &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; and its associated &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignedUpTeam&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;ProjectTopic&amp;lt;/code&amp;gt;, and permission fields (&amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;permission_granted&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Request / Response Flow ====&lt;br /&gt;
* Student navigates to &amp;lt;code&amp;gt;/student_tasks&amp;lt;/code&amp;gt; → frontend calls &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTask.tasks(user)&amp;lt;/code&amp;gt; preloads &amp;lt;code&amp;gt;assignment: :course&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;:user&amp;lt;/code&amp;gt; associations, then maps each &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; through &amp;lt;code&amp;gt;StudentTask.create_from_participant&amp;lt;/code&amp;gt;&lt;br /&gt;
* Selecting an assignment navigates to the detail route → frontend calls &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTask.timeline_events&amp;lt;/code&amp;gt; merges the assignment's due dates with timestamps of reviews done of the logged-in user's work into one sorted timeline&lt;br /&gt;
* For the heatgrid: frontend calls &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt; → &amp;lt;code&amp;gt;GradesController#get_team_scores&amp;lt;/code&amp;gt; eager-loads &amp;lt;code&amp;gt;responses: { scores: :item }&amp;lt;/code&amp;gt; and calls &amp;lt;code&amp;gt;insert_section_headers&amp;lt;/code&amp;gt; to splice section-header marker objects into the response before it is returned to the frontend&lt;br /&gt;
&lt;br /&gt;
=== Aggregation of Scores Assigned by a Reviewer ===&lt;br /&gt;
* &amp;lt;code&amp;gt;Response#aggregate_questionnaire_score&amp;lt;/code&amp;gt; - item-weighted score for one submitted response&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; — normalizes a map's score per round and weights it by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score(maps)&amp;lt;/code&amp;gt; — weighted average across multiple maps, the single implementation shared by &amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Backend Implementation ==&lt;br /&gt;
&lt;br /&gt;
=== StudentTasksController ===&lt;br /&gt;
* &amp;lt;code&amp;gt;list&amp;lt;/code&amp;gt; — all &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; objects for the logged-in user&lt;br /&gt;
* &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt; — single task by participant ID with the unified timeline&lt;br /&gt;
* &amp;lt;code&amp;gt;team&amp;lt;/code&amp;gt; — teammates grouped by course name (&amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
=== GradesController — N+1 elimination ===&lt;br /&gt;
&amp;lt;code&amp;gt;get_team_scores&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;get_my_scores_data&amp;lt;/code&amp;gt; eager-load &amp;lt;code&amp;gt;responses: { scores: :item }&amp;lt;/code&amp;gt; on the &amp;lt;code&amp;gt;ReviewResponseMap&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;TeammateReviewResponseMap&amp;lt;/code&amp;gt; queries. &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt; filters SectionHeader scores via Ruby &amp;lt;code&amp;gt;.reject&amp;lt;/code&amp;gt; instead of &amp;lt;code&amp;gt;.joins&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;.where&amp;lt;/code&amp;gt; — calling &amp;lt;code&amp;gt;.where&amp;lt;/code&amp;gt; on an already-preloaded association silently bypasses the cache and re-queries the database. &amp;lt;code&amp;gt;get_answer&amp;lt;/code&amp;gt; now accepts the parent &amp;lt;code&amp;gt;response&amp;lt;/code&amp;gt; directly instead of re-fetching it via &amp;lt;code&amp;gt;score.response&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== insert_section_headers ===&lt;br /&gt;
Inserts &amp;lt;code&amp;gt;{ type: &amp;quot;header&amp;quot;, txt: &amp;quot;...&amp;quot; }&amp;lt;/code&amp;gt; marker objects into the scores array at positions derived from each &amp;lt;code&amp;gt;SectionHeader&amp;lt;/code&amp;gt; item's sequence order, so the frontend renders section heading rows between score rows without those rows affecting item numbering.&lt;br /&gt;
&lt;br /&gt;
=== Review Grade Calculation ===&lt;br /&gt;
For details on how peer review grades are computed, see [[Score Calculation]].&lt;br /&gt;
&lt;br /&gt;
=== Comprehensive Testing ===&lt;br /&gt;
* Model specs for &amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;create_from_participant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_events_for_assignment&amp;lt;/code&amp;gt;&lt;br /&gt;
* Request specs for &amp;lt;code&amp;gt;student_tasks/list&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;student_tasks/show/:id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;student_tasks/team&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;grades/:id/view_our_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
* Routing specs verifying the above routes resolve correctly&lt;br /&gt;
&lt;br /&gt;
== Frontend Implementation ==&lt;br /&gt;
=== New/Updated Files ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! File !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTasks.tsx&amp;lt;/code&amp;gt; || Course-grouped assignment dashboard (rewritten from a topic sign-up sheet)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTasksList.tsx&amp;lt;/code&amp;gt; || Sidebar — not-started tasks, revisions, teammates by course&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTaskDetail.tsx&amp;lt;/code&amp;gt; || Per-assignment detail page — timeline, task links, conditional &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; rendering&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ViewTeamGrades.module.scss&amp;lt;/code&amp;gt; || CSS Modules conversion; scoped &amp;lt;code&amp;gt;c1&amp;lt;/code&amp;gt;–&amp;lt;code&amp;gt;c5&amp;lt;/code&amp;gt; color classes, sticky-column and tooltip styles&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewTable.tsx&amp;lt;/code&amp;gt; || Single-call heatgrid; Scores/Feedback toggle; sticky SectionHeader rows&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewTableRow.tsx&amp;lt;/code&amp;gt; || Sticky &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;/Question columns; reduced row height; hover tooltip via &amp;lt;code&amp;gt;data-question&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;FeedbackTable.tsx&amp;lt;/code&amp;gt; || Feedback-mode table mirroring &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt;'s sticky-column layout&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;heatgridUtils.ts&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;RoundRow&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;isHeader()&amp;lt;/code&amp;gt; types; fixed &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; numbering to skip SectionHeader sentinels&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== StudentTasks (List View) ===&lt;br /&gt;
* Single &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; call, grouped by course&lt;br /&gt;
* Fixed: &amp;quot;Assignments&amp;quot; heading was rendering as body text because its class was referenced as a plain string instead of the CSS Modules export (&amp;lt;code&amp;gt;styles['assignments-title']&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
=== StudentTaskDetail (Detail View) ===&lt;br /&gt;
* Visual timeline with progress line and stage nodes (completed/current/pending)&lt;br /&gt;
* Task links list — &amp;quot;Your work&amp;quot; and &amp;quot;Others' work&amp;quot; conditionally rendered based on &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;quot;Send Email To Reviewers&amp;quot; button repositioned to align with the task list rather than floating disconnected at the page top&lt;br /&gt;
&lt;br /&gt;
=== ViewTeamGrades (Heatgrid) ===&lt;br /&gt;
* Consolidated ~6 sequential API calls into one &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
* SectionHeader rows split into a sticky label cell + scrolling spacer cell so they stay fixed during horizontal scroll&lt;br /&gt;
* Score cells show reviewer feedback as a hover tooltip&lt;br /&gt;
* Round headings simplified from &amp;quot;Review (Round: 1 of 2)&amp;quot; to &amp;quot;Round 1&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Design Principles ==&lt;br /&gt;
&lt;br /&gt;
=== Single Responsibility ===&lt;br /&gt;
Score aggregation is layered: &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; owns item-level scoring, &amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt; owns per-map/per-round weighting, &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; owns cross-map averaging — each level only knows about the one below it.&lt;br /&gt;
&lt;br /&gt;
=== Don't Repeat Yourself ===&lt;br /&gt;
The weighted-average logic that was duplicated between &amp;lt;code&amp;gt;AssignmentTeam#aggregate_review_grade&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;ReviewAggregator#compute_average_review_score&amp;lt;/code&amp;gt; was consolidated into a single &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt;, and the now-dead &amp;lt;code&amp;gt;ReviewAggregator&amp;lt;/code&amp;gt; concern was deleted.&lt;br /&gt;
&lt;br /&gt;
=== Performance by Design ===&lt;br /&gt;
Eager loading (&amp;lt;code&amp;gt;.includes&amp;lt;/code&amp;gt;) and in-memory filtering (&amp;lt;code&amp;gt;.reject&amp;lt;/code&amp;gt;) were chosen deliberately over additional database round-trips after profiling showed the heatgrid endpoint firing hundreds of queries under the old implementation.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
&lt;br /&gt;
=== Backend Tests ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! # !! Test !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| 1 || &amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt; groups teammates by course || Returns &amp;lt;code&amp;gt;{course_name =&amp;gt; [fullnames]}&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 2 || &amp;lt;code&amp;gt;all_teammates&amp;lt;/code&amp;gt; excludes calibrated assignments || Calibrated-assignment teammates excluded&lt;br /&gt;
|-&lt;br /&gt;
| 3 || &amp;lt;code&amp;gt;all_teammates&amp;lt;/code&amp;gt; excludes the user themself || Solo team returns empty hash&lt;br /&gt;
|-&lt;br /&gt;
| 4 || &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; returns all tasks for user || 200 OK with task array&lt;br /&gt;
|-&lt;br /&gt;
| 5 || &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt; returns due_dates timeline || 200 OK with &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt; array&lt;br /&gt;
|-&lt;br /&gt;
| 6 || &amp;lt;code&amp;gt;GET /student_tasks/team&amp;lt;/code&amp;gt; groups teammates by course || 200 OK with grouped hash&lt;br /&gt;
|-&lt;br /&gt;
| 7 || Heatgrid &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; column excludes SectionHeaders from numbering || Sequential numbering skips header rows&lt;br /&gt;
|-&lt;br /&gt;
| 8 || &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt; filters SectionHeaders without re-querying || No additional SQL fired against preloaded association&lt;br /&gt;
|-&lt;br /&gt;
| 9 || &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; weight validation || &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt; must be 0 when rubric has no scored questions&lt;br /&gt;
|-&lt;br /&gt;
| 10 || &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; || Weighted average matches Σ(grade × weight) / Σ(weight)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Frontend Tests ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! # !! Test !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| 1 || &amp;lt;code&amp;gt;StudentTasks&amp;lt;/code&amp;gt; renders &amp;quot;Assignments&amp;quot; heading with correct styling || Heading uses module class, renders bold and large&lt;br /&gt;
|-&lt;br /&gt;
| 2 || &amp;lt;code&amp;gt;StudentTaskDetail&amp;lt;/code&amp;gt; hides &amp;quot;Your work&amp;quot; when &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; is false || List item not rendered&lt;br /&gt;
|-&lt;br /&gt;
| 3 || &amp;lt;code&amp;gt;StudentTaskDetail&amp;lt;/code&amp;gt; hides &amp;quot;Others' work&amp;quot; when &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; is false || List item not rendered&lt;br /&gt;
|-&lt;br /&gt;
| 4 || &amp;lt;code&amp;gt;heatgridUtils&amp;lt;/code&amp;gt; itemNumber skips SectionHeader rows || Numbering matches scored items only&lt;br /&gt;
|-&lt;br /&gt;
| 5 || &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt; SectionHeader row stays fixed during horizontal scroll || Header cell &amp;lt;code&amp;gt;position: sticky&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 6 || &amp;lt;code&amp;gt;ReviewTableRow&amp;lt;/code&amp;gt; shows tooltip only when a comment exists || No tooltip box when &amp;lt;code&amp;gt;data-question&amp;lt;/code&amp;gt; is undefined&lt;br /&gt;
|-&lt;br /&gt;
| 7 || Heatgrid column borders render as &amp;lt;code&amp;gt;#ddd&amp;lt;/code&amp;gt; || No black borders after &amp;lt;code&amp;gt;tbl_heat&amp;lt;/code&amp;gt; class applied&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Relevant Links ==&lt;br /&gt;
&lt;br /&gt;
=== Repos ===&lt;br /&gt;
* https://github.com/expertiza/reimplementation-back-end&lt;br /&gt;
* https://github.com/expertiza/reimplementation-front-end&lt;br /&gt;
&lt;br /&gt;
=== Pull Requests ===&lt;br /&gt;
* Backend: https://github.com/expertiza/reimplementation-back-end/pull/352&lt;br /&gt;
* Frontend: https://github.com/expertiza/reimplementation-front-end/pull/182&lt;br /&gt;
&lt;br /&gt;
=== Demo Video ===&lt;br /&gt;
* (add link once recorded)&lt;br /&gt;
&lt;br /&gt;
== Developer Guidance ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Setup &amp;amp; Run (via Docker) ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
docker compose up&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Within the container ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
bundle exec rspec spec/models/student_task_spec.rb&lt;br /&gt;
bundle exec rspec spec/requests/api/v1/student_tasks_controller_spec.rb&lt;br /&gt;
bundle exec rspec spec/requests/api/v1/grades_controller_spec.rb&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
==== Setup &amp;amp; Running ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm install&lt;br /&gt;
npm start&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Running Frontend Tests ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm test -- StudentTaskDetail   # run a single suite&lt;br /&gt;
npm test                        # run full suite&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Future Scope ==&lt;br /&gt;
&lt;br /&gt;
=== Reviewer reputation-weighted grading ===&lt;br /&gt;
Integrate an external review-quality grader (e.g. an LLM-based reviewer grader) so &amp;lt;code&amp;gt;ResponseMap.reviewer_reputation_for&amp;lt;/code&amp;gt; reflects actual review quality instead of the current &amp;lt;code&amp;gt;1.0&amp;lt;/code&amp;gt; placeholder weight  for every reviewer.&lt;br /&gt;
&lt;br /&gt;
=== Email-the-authors integration ===&lt;br /&gt;
Wire &amp;quot;Send Email To Reviewers&amp;quot; to a real backend endpoint instead of the current placeholder route.&lt;br /&gt;
&lt;br /&gt;
=== Dedicated StudentTask abstraction ===&lt;br /&gt;
&amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; remains a composed, non-persisted object. A future dedicated table could centralize derived state and simplify queries if the feature's scope grows further.&lt;br /&gt;
&lt;br /&gt;
== Mentor ==&lt;br /&gt;
[mailto:kgudipe@ncsu.edu Koushik Gudipelly]&lt;br /&gt;
&lt;br /&gt;
== Members ==&lt;br /&gt;
[mailto:rsgopara@ncsu.edu Ravi Goparaju]&lt;br /&gt;
[mailto:xmi@ncsu.edu Xiangjun Mi]&lt;br /&gt;
&lt;br /&gt;
== Completed by ==&lt;br /&gt;
[mailto:blalu@ncsu.edu Bestin Lalu]&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2602._Reimplement_student_task_view&amp;diff=168254</id>
		<title>CSC/ECE 517 Spring 2026 - E2602. Reimplement student task view</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2602._Reimplement_student_task_view&amp;diff=168254"/>
		<updated>2026-07-07T00:26:19Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Setup &amp;amp; Run */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= CSC/ECE 517 Spring 2026 - E2602. Reimplement Student Task View =&lt;br /&gt;
&lt;br /&gt;
== About Expertiza ==&lt;br /&gt;
Expertiza is a robust, open-source web platform designed to empower educational environments that emphasize project-based learning. It streamlines collaborative activities including peer reviews, surveys, and team assignments — giving instructors tools to manage assignments and track performance, and giving students a single place to submit work, review peers, and track feedback.&lt;br /&gt;
&lt;br /&gt;
== Introduction ==&lt;br /&gt;
''This page documents the reimplementation of the Student Task view. It supersedes the earlier [[E2602]] team's version. All searches for E2602 will hit here.''&lt;br /&gt;
This iteration of E2602 hardens and extends the Student Task View: consolidating the student dashboard and detail pages into single-call REST endpoints, eliminating N+1 queries in the peer-review score heatgrid, and fixing several display/permission bugs surfaced by manual QA against real assignment data.&lt;br /&gt;
&lt;br /&gt;
=== Tech Stack ===&lt;br /&gt;
* '''Backend:''' Ruby on Rails (API-only)&lt;br /&gt;
* '''Frontend:''' React + TypeScript&lt;br /&gt;
* '''Database:''' MySQL&lt;br /&gt;
* '''Backend Testing:''' RSpec&lt;br /&gt;
* '''Frontend Testing:''' Jest, React Testing Library&lt;br /&gt;
&lt;br /&gt;
=== Key Improvements ===&lt;br /&gt;
* Eliminated N+1 queries in the score heatgrid (&amp;lt;code&amp;gt;get_team_scores&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_my_scores_data&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_answer&amp;lt;/code&amp;gt;) via &amp;lt;code&amp;gt;.includes(responses: { scores: :item })&amp;lt;/code&amp;gt; and in-memory filtering instead of re-querying preloaded associations&lt;br /&gt;
* Converted &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt;'s stylesheet to CSS Modules, fixing a Bootstrap class collision and scoping color classes (&amp;lt;code&amp;gt;c1&amp;lt;/code&amp;gt;–&amp;lt;code&amp;gt;c5&amp;lt;/code&amp;gt;) correctly&lt;br /&gt;
* Redesigned the score/feedback heatgrid with sticky Question column during horizontal scroll&lt;br /&gt;
* Added hover tooltips on score cells showing the reviewer's written feedback&lt;br /&gt;
* Rewrote &amp;lt;code&amp;gt;StudentTasks&amp;lt;/code&amp;gt; from a (mis-implemented) topic sign-up sheet into a proper course-grouped assignment dashboard&lt;br /&gt;
* Added participant-level &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; permission checks, hiding &amp;quot;Your work&amp;quot; / &amp;quot;Others' work&amp;quot; task links accordingly&lt;br /&gt;
* Refactored review grade calculation: &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; now weights each round by its &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;; &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; centralizes the weighted-average logic previously duplicated across &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Problem Statement ==&lt;br /&gt;
The previous Student Task View had several concrete defects:&lt;br /&gt;
* The score heatgrid fired one SQL query per response/score across all reviewers and rounds, producing hundreds of queries for a single page load&lt;br /&gt;
* SectionHeader rubric rows (non-scorable dividers) were incorrectly counted in the &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; column and lost their fixed position during horizontal scroll&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTasks.tsx&amp;lt;/code&amp;gt; was previously implemented as a topic sign-up sheet, not a task dashboard, and required a full rewrite&lt;br /&gt;
* The page did not respect participant-level &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; flags — all task links were always shown regardless of permission&lt;br /&gt;
* Review grade calculation treated every reviewer and every round equally, ignoring each rubric's configured &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== User Stories ==&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want to see all my assignments in one place ===&lt;br /&gt;
&lt;br /&gt;
This is the home view for a student. It shows all of the student's assignments, arranged by the course they pertain to.&lt;br /&gt;
&lt;br /&gt;
[[File:student_view_assignments_page.png|800px|thumb|center|Student View Assignments Page]]&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; returns all tasks for the logged-in student in one call&lt;br /&gt;
* Tasks are grouped by course on the dashboard&lt;br /&gt;
* Each task shows current stage and days remaining, if applicable&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want to view a single assignment's deadlines and details ===&lt;br /&gt;
&lt;br /&gt;
A student can perform several tasks for each assignment, depending upon the role they play (e.g., whether they are a submitter, a reviewer, or both.&lt;br /&gt;
&lt;br /&gt;
[[File:student_view_assignment_detail_page.png|800px|thumb|center|Student View Assignment Detail Page]]&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt; returns the task plus a unified &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt; timeline&lt;br /&gt;
* The timeline visually distinguishes completed, current, and pending stages&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/team&amp;lt;/code&amp;gt; returns teammates grouped by course&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want my team's score heatgrid to load fast and stay readable while scrolling ===&lt;br /&gt;
&lt;br /&gt;
The heatgrid shows all of the reviews that have been written of the team's submission (or, the student's submission, if this is not a team assignment).  The user can hover over any cell with a score that is underlined, meaning that the reviewer has made a comment to go along with the score.  Hovering over a cell will show the comments it contains.&lt;br /&gt;
&lt;br /&gt;
[[File:student_view_assignment_grades_page.png|800px|thumb|left|Student View Assignment View Team Grades]]&lt;br /&gt;
&amp;lt;br clear=&amp;quot;all&amp;quot; /&amp;gt;&lt;br /&gt;
Clicking on &amp;quot;Feedback&amp;quot; will show all of the feedback along with the scores.&lt;br /&gt;
[[File:student_view_assignment_feedback_page.png|800px|thumb|left|Student View Assignment View Feedbacks]]&lt;br /&gt;
&amp;lt;br clear=&amp;quot;all&amp;quot; /&amp;gt;&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* The heatgrid loads from a single &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt; call&lt;br /&gt;
* The first two columns (item number &amp;quot;&amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;&amp;quot; and Item) remain fixed during horizontal scroll, along with section headers&lt;br /&gt;
* Hovering over a score with a comment shows the corresponding reviewer's feedback as a tooltip&lt;br /&gt;
&lt;br /&gt;
=== A student who is not permitted to submit or review will not see those task links ===&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;quot;Your work&amp;quot; is hidden when &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;quot;Others' work&amp;quot; is hidden when &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== As an instructor, I want calculated review grades to reflect each rubric's configured weight ===&lt;br /&gt;
&lt;br /&gt;
Grades are calculated in &amp;lt;code&amp;gt;Response.rb&amp;lt;/code&amp;gt; (score for a particular review by a specific reviewer), &amp;lt;code&amp;gt;ResponseMap.rb&amp;lt;/code&amp;gt; (composite score for reviews in all rounds by the same reviewer) and &amp;lt;code&amp;gt;AssignmentTeam.rb&amp;lt;/code&amp;gt; (combined score for all reviews by all reviewers).&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; weights each round's normalized score by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt; both compute their averages through the same shared &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
=== Student Task Overview ===&lt;br /&gt;
A StudentTask is a task that a student performs in the course of doing an assignment, e.g., forming a team, choosing a topic, submitting work, or reviewing work. No dedicated &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; database table exists. A &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; is a plain Ruby object composed from &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; and its associated &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignedUpTeam&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;ProjectTopic&amp;lt;/code&amp;gt;, and permission fields (&amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;permission_granted&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Request / Response Flow ====&lt;br /&gt;
* Student navigates to &amp;lt;code&amp;gt;/student_tasks&amp;lt;/code&amp;gt; → frontend calls &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTask.tasks(user)&amp;lt;/code&amp;gt; preloads &amp;lt;code&amp;gt;assignment: :course&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;:user&amp;lt;/code&amp;gt; associations, then maps each &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; through &amp;lt;code&amp;gt;StudentTask.create_from_participant&amp;lt;/code&amp;gt;&lt;br /&gt;
* Selecting an assignment navigates to the detail route → frontend calls &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTask.timeline_events&amp;lt;/code&amp;gt; merges the assignment's due dates with timestamps of reviews done of the logged-in user's work into one sorted timeline&lt;br /&gt;
* For the heatgrid: frontend calls &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt; → &amp;lt;code&amp;gt;GradesController#get_team_scores&amp;lt;/code&amp;gt; eager-loads &amp;lt;code&amp;gt;responses: { scores: :item }&amp;lt;/code&amp;gt; and calls &amp;lt;code&amp;gt;insert_section_headers&amp;lt;/code&amp;gt; to splice section-header marker objects into the response before it is returned to the frontend&lt;br /&gt;
&lt;br /&gt;
=== Aggregation of Scores Assigned by a Reviewer ===&lt;br /&gt;
* &amp;lt;code&amp;gt;Response#aggregate_questionnaire_score&amp;lt;/code&amp;gt; - item-weighted score for one submitted response&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; — normalizes a map's score per round and weights it by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score(maps)&amp;lt;/code&amp;gt; — weighted average across multiple maps, the single implementation shared by &amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Backend Implementation ==&lt;br /&gt;
&lt;br /&gt;
=== StudentTasksController ===&lt;br /&gt;
* &amp;lt;code&amp;gt;list&amp;lt;/code&amp;gt; — all &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; objects for the logged-in user&lt;br /&gt;
* &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt; — single task by participant ID with the unified timeline&lt;br /&gt;
* &amp;lt;code&amp;gt;team&amp;lt;/code&amp;gt; — teammates grouped by course name (&amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
=== GradesController — N+1 elimination ===&lt;br /&gt;
&amp;lt;code&amp;gt;get_team_scores&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;get_my_scores_data&amp;lt;/code&amp;gt; eager-load &amp;lt;code&amp;gt;responses: { scores: :item }&amp;lt;/code&amp;gt; on the &amp;lt;code&amp;gt;ReviewResponseMap&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;TeammateReviewResponseMap&amp;lt;/code&amp;gt; queries. &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt; filters SectionHeader scores via Ruby &amp;lt;code&amp;gt;.reject&amp;lt;/code&amp;gt; instead of &amp;lt;code&amp;gt;.joins&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;.where&amp;lt;/code&amp;gt; — calling &amp;lt;code&amp;gt;.where&amp;lt;/code&amp;gt; on an already-preloaded association silently bypasses the cache and re-queries the database. &amp;lt;code&amp;gt;get_answer&amp;lt;/code&amp;gt; now accepts the parent &amp;lt;code&amp;gt;response&amp;lt;/code&amp;gt; directly instead of re-fetching it via &amp;lt;code&amp;gt;score.response&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== insert_section_headers ===&lt;br /&gt;
Inserts &amp;lt;code&amp;gt;{ type: &amp;quot;header&amp;quot;, txt: &amp;quot;...&amp;quot; }&amp;lt;/code&amp;gt; marker objects into the scores array at positions derived from each &amp;lt;code&amp;gt;SectionHeader&amp;lt;/code&amp;gt; item's sequence order, so the frontend renders section heading rows between score rows without those rows affecting item numbering.&lt;br /&gt;
&lt;br /&gt;
=== Review Grade Calculation ===&lt;br /&gt;
For details on how peer review grades are computed, see [[Score Calculation]].&lt;br /&gt;
&lt;br /&gt;
=== Comprehensive Testing ===&lt;br /&gt;
* Model specs for &amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;create_from_participant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_events_for_assignment&amp;lt;/code&amp;gt;&lt;br /&gt;
* Request specs for &amp;lt;code&amp;gt;student_tasks/list&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;student_tasks/show/:id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;student_tasks/team&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;grades/:id/view_our_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
* Routing specs verifying the above routes resolve correctly&lt;br /&gt;
&lt;br /&gt;
== Frontend Implementation ==&lt;br /&gt;
=== New/Updated Files ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! File !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTasks.tsx&amp;lt;/code&amp;gt; || Course-grouped assignment dashboard (rewritten from a topic sign-up sheet)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTasksList.tsx&amp;lt;/code&amp;gt; || Sidebar — not-started tasks, revisions, teammates by course&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTaskDetail.tsx&amp;lt;/code&amp;gt; || Per-assignment detail page — timeline, task links, conditional &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; rendering&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ViewTeamGrades.module.scss&amp;lt;/code&amp;gt; || CSS Modules conversion; scoped &amp;lt;code&amp;gt;c1&amp;lt;/code&amp;gt;–&amp;lt;code&amp;gt;c5&amp;lt;/code&amp;gt; color classes, sticky-column and tooltip styles&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewTable.tsx&amp;lt;/code&amp;gt; || Single-call heatgrid; Scores/Feedback toggle; sticky SectionHeader rows&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewTableRow.tsx&amp;lt;/code&amp;gt; || Sticky &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;/Question columns; reduced row height; hover tooltip via &amp;lt;code&amp;gt;data-question&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;FeedbackTable.tsx&amp;lt;/code&amp;gt; || Feedback-mode table mirroring &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt;'s sticky-column layout&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;heatgridUtils.ts&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;RoundRow&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;isHeader()&amp;lt;/code&amp;gt; types; fixed &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; numbering to skip SectionHeader sentinels&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== StudentTasks (List View) ===&lt;br /&gt;
* Single &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; call, grouped by course&lt;br /&gt;
* Fixed: &amp;quot;Assignments&amp;quot; heading was rendering as body text because its class was referenced as a plain string instead of the CSS Modules export (&amp;lt;code&amp;gt;styles['assignments-title']&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
=== StudentTaskDetail (Detail View) ===&lt;br /&gt;
* Visual timeline with progress line and stage nodes (completed/current/pending)&lt;br /&gt;
* Task links list — &amp;quot;Your work&amp;quot; and &amp;quot;Others' work&amp;quot; conditionally rendered based on &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;quot;Send Email To Reviewers&amp;quot; button repositioned to align with the task list rather than floating disconnected at the page top&lt;br /&gt;
&lt;br /&gt;
=== ViewTeamGrades (Heatgrid) ===&lt;br /&gt;
* Consolidated ~6 sequential API calls into one &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
* SectionHeader rows split into a sticky label cell + scrolling spacer cell so they stay fixed during horizontal scroll&lt;br /&gt;
* Score cells show reviewer feedback as a hover tooltip&lt;br /&gt;
* Round headings simplified from &amp;quot;Review (Round: 1 of 2)&amp;quot; to &amp;quot;Round 1&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Design Principles ==&lt;br /&gt;
&lt;br /&gt;
=== Single Responsibility ===&lt;br /&gt;
Score aggregation is layered: &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; owns item-level scoring, &amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt; owns per-map/per-round weighting, &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; owns cross-map averaging — each level only knows about the one below it.&lt;br /&gt;
&lt;br /&gt;
=== Don't Repeat Yourself ===&lt;br /&gt;
The weighted-average logic that was duplicated between &amp;lt;code&amp;gt;AssignmentTeam#aggregate_review_grade&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;ReviewAggregator#compute_average_review_score&amp;lt;/code&amp;gt; was consolidated into a single &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt;, and the now-dead &amp;lt;code&amp;gt;ReviewAggregator&amp;lt;/code&amp;gt; concern was deleted.&lt;br /&gt;
&lt;br /&gt;
=== Performance by Design ===&lt;br /&gt;
Eager loading (&amp;lt;code&amp;gt;.includes&amp;lt;/code&amp;gt;) and in-memory filtering (&amp;lt;code&amp;gt;.reject&amp;lt;/code&amp;gt;) were chosen deliberately over additional database round-trips after profiling showed the heatgrid endpoint firing hundreds of queries under the old implementation.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
&lt;br /&gt;
=== Backend Tests ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! # !! Test !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| 1 || &amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt; groups teammates by course || Returns &amp;lt;code&amp;gt;{course_name =&amp;gt; [fullnames]}&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 2 || &amp;lt;code&amp;gt;all_teammates&amp;lt;/code&amp;gt; excludes calibrated assignments || Calibrated-assignment teammates excluded&lt;br /&gt;
|-&lt;br /&gt;
| 3 || &amp;lt;code&amp;gt;all_teammates&amp;lt;/code&amp;gt; excludes the user themself || Solo team returns empty hash&lt;br /&gt;
|-&lt;br /&gt;
| 4 || &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; returns all tasks for user || 200 OK with task array&lt;br /&gt;
|-&lt;br /&gt;
| 5 || &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt; returns due_dates timeline || 200 OK with &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt; array&lt;br /&gt;
|-&lt;br /&gt;
| 6 || &amp;lt;code&amp;gt;GET /student_tasks/team&amp;lt;/code&amp;gt; groups teammates by course || 200 OK with grouped hash&lt;br /&gt;
|-&lt;br /&gt;
| 7 || Heatgrid &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; column excludes SectionHeaders from numbering || Sequential numbering skips header rows&lt;br /&gt;
|-&lt;br /&gt;
| 8 || &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt; filters SectionHeaders without re-querying || No additional SQL fired against preloaded association&lt;br /&gt;
|-&lt;br /&gt;
| 9 || &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; weight validation || &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt; must be 0 when rubric has no scored questions&lt;br /&gt;
|-&lt;br /&gt;
| 10 || &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; || Weighted average matches Σ(grade × weight) / Σ(weight)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Frontend Tests ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! # !! Test !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| 1 || &amp;lt;code&amp;gt;StudentTasks&amp;lt;/code&amp;gt; renders &amp;quot;Assignments&amp;quot; heading with correct styling || Heading uses module class, renders bold and large&lt;br /&gt;
|-&lt;br /&gt;
| 2 || &amp;lt;code&amp;gt;StudentTaskDetail&amp;lt;/code&amp;gt; hides &amp;quot;Your work&amp;quot; when &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; is false || List item not rendered&lt;br /&gt;
|-&lt;br /&gt;
| 3 || &amp;lt;code&amp;gt;StudentTaskDetail&amp;lt;/code&amp;gt; hides &amp;quot;Others' work&amp;quot; when &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; is false || List item not rendered&lt;br /&gt;
|-&lt;br /&gt;
| 4 || &amp;lt;code&amp;gt;heatgridUtils&amp;lt;/code&amp;gt; itemNumber skips SectionHeader rows || Numbering matches scored items only&lt;br /&gt;
|-&lt;br /&gt;
| 5 || &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt; SectionHeader row stays fixed during horizontal scroll || Header cell &amp;lt;code&amp;gt;position: sticky&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 6 || &amp;lt;code&amp;gt;ReviewTableRow&amp;lt;/code&amp;gt; shows tooltip only when a comment exists || No tooltip box when &amp;lt;code&amp;gt;data-question&amp;lt;/code&amp;gt; is undefined&lt;br /&gt;
|-&lt;br /&gt;
| 7 || Heatgrid column borders render as &amp;lt;code&amp;gt;#ddd&amp;lt;/code&amp;gt; || No black borders after &amp;lt;code&amp;gt;tbl_heat&amp;lt;/code&amp;gt; class applied&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Relevant Links ==&lt;br /&gt;
&lt;br /&gt;
=== Repos ===&lt;br /&gt;
* https://github.com/expertiza/reimplementation-back-end&lt;br /&gt;
* https://github.com/expertiza/reimplementation-front-end&lt;br /&gt;
&lt;br /&gt;
=== Pull Requests ===&lt;br /&gt;
* Backend: https://github.com/expertiza/reimplementation-back-end/pull/352&lt;br /&gt;
* Frontend: https://github.com/expertiza/reimplementation-front-end/pull/182&lt;br /&gt;
&lt;br /&gt;
=== Demo Video ===&lt;br /&gt;
* (add link once recorded)&lt;br /&gt;
&lt;br /&gt;
== Developer Guidance ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Setup &amp;amp; Run (via Docker) ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
docker compose up&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Within the container ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
bundle exec rspec spec/models/student_task_spec.rb&lt;br /&gt;
bundle exec rspec spec/requests/api/v1/student_tasks_controller_spec.rb&lt;br /&gt;
bundle exec rspec spec/requests/api/v1/grades_controller_spec.rb&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
==== Setup &amp;amp; Running ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm install&lt;br /&gt;
npm start&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Running Frontend Tests ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm test -- StudentTaskDetail   # run a single suite&lt;br /&gt;
npm test                        # run full suite&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Future Scope ==&lt;br /&gt;
&lt;br /&gt;
=== Reviewer reputation-weighted grading ===&lt;br /&gt;
Integrate an external review-quality grader (e.g. an LLM-based reviewer grader) so &amp;lt;code&amp;gt;ResponseMap.reviewer_reputation_for&amp;lt;/code&amp;gt; reflects actual review quality instead of the current &amp;lt;code&amp;gt;1.0&amp;lt;/code&amp;gt; placeholder for every reviewer.&lt;br /&gt;
&lt;br /&gt;
=== Email-the-authors integration ===&lt;br /&gt;
Wire &amp;quot;Send Email To Reviewers&amp;quot; to a real backend endpoint instead of the current placeholder route.&lt;br /&gt;
&lt;br /&gt;
=== Dedicated StudentTask abstraction ===&lt;br /&gt;
&amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; remains a composed, non-persisted object. A future dedicated table could centralize derived state and simplify queries if the feature's scope grows further.&lt;br /&gt;
&lt;br /&gt;
== Mentor ==&lt;br /&gt;
[mailto:kgudipe@ncsu.edu Koushik Gudipelly]&lt;br /&gt;
&lt;br /&gt;
== Members ==&lt;br /&gt;
[mailto:rsgopara@ncsu.edu Ravi Goparaju]&lt;br /&gt;
[mailto:xmi@ncsu.edu Xiangjun Mi]&lt;br /&gt;
&lt;br /&gt;
== Completed by ==&lt;br /&gt;
[mailto:blalu@ncsu.edu Bestin Lalu]&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2602._Reimplement_student_task_view&amp;diff=168253</id>
		<title>CSC/ECE 517 Spring 2026 - E2602. Reimplement student task view</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2602._Reimplement_student_task_view&amp;diff=168253"/>
		<updated>2026-07-07T00:23:00Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* As an instructor, I want calculated review grades to reflect each rubric's configured weight */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= CSC/ECE 517 Spring 2026 - E2602. Reimplement Student Task View =&lt;br /&gt;
&lt;br /&gt;
== About Expertiza ==&lt;br /&gt;
Expertiza is a robust, open-source web platform designed to empower educational environments that emphasize project-based learning. It streamlines collaborative activities including peer reviews, surveys, and team assignments — giving instructors tools to manage assignments and track performance, and giving students a single place to submit work, review peers, and track feedback.&lt;br /&gt;
&lt;br /&gt;
== Introduction ==&lt;br /&gt;
''This page documents the reimplementation of the Student Task view. It supersedes the earlier [[E2602]] team's version. All searches for E2602 will hit here.''&lt;br /&gt;
This iteration of E2602 hardens and extends the Student Task View: consolidating the student dashboard and detail pages into single-call REST endpoints, eliminating N+1 queries in the peer-review score heatgrid, and fixing several display/permission bugs surfaced by manual QA against real assignment data.&lt;br /&gt;
&lt;br /&gt;
=== Tech Stack ===&lt;br /&gt;
* '''Backend:''' Ruby on Rails (API-only)&lt;br /&gt;
* '''Frontend:''' React + TypeScript&lt;br /&gt;
* '''Database:''' MySQL&lt;br /&gt;
* '''Backend Testing:''' RSpec&lt;br /&gt;
* '''Frontend Testing:''' Jest, React Testing Library&lt;br /&gt;
&lt;br /&gt;
=== Key Improvements ===&lt;br /&gt;
* Eliminated N+1 queries in the score heatgrid (&amp;lt;code&amp;gt;get_team_scores&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_my_scores_data&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_answer&amp;lt;/code&amp;gt;) via &amp;lt;code&amp;gt;.includes(responses: { scores: :item })&amp;lt;/code&amp;gt; and in-memory filtering instead of re-querying preloaded associations&lt;br /&gt;
* Converted &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt;'s stylesheet to CSS Modules, fixing a Bootstrap class collision and scoping color classes (&amp;lt;code&amp;gt;c1&amp;lt;/code&amp;gt;–&amp;lt;code&amp;gt;c5&amp;lt;/code&amp;gt;) correctly&lt;br /&gt;
* Redesigned the score/feedback heatgrid with sticky Question column during horizontal scroll&lt;br /&gt;
* Added hover tooltips on score cells showing the reviewer's written feedback&lt;br /&gt;
* Rewrote &amp;lt;code&amp;gt;StudentTasks&amp;lt;/code&amp;gt; from a (mis-implemented) topic sign-up sheet into a proper course-grouped assignment dashboard&lt;br /&gt;
* Added participant-level &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; permission checks, hiding &amp;quot;Your work&amp;quot; / &amp;quot;Others' work&amp;quot; task links accordingly&lt;br /&gt;
* Refactored review grade calculation: &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; now weights each round by its &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;; &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; centralizes the weighted-average logic previously duplicated across &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Problem Statement ==&lt;br /&gt;
The previous Student Task View had several concrete defects:&lt;br /&gt;
* The score heatgrid fired one SQL query per response/score across all reviewers and rounds, producing hundreds of queries for a single page load&lt;br /&gt;
* SectionHeader rubric rows (non-scorable dividers) were incorrectly counted in the &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; column and lost their fixed position during horizontal scroll&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTasks.tsx&amp;lt;/code&amp;gt; was previously implemented as a topic sign-up sheet, not a task dashboard, and required a full rewrite&lt;br /&gt;
* The page did not respect participant-level &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; flags — all task links were always shown regardless of permission&lt;br /&gt;
* Review grade calculation treated every reviewer and every round equally, ignoring each rubric's configured &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== User Stories ==&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want to see all my assignments in one place ===&lt;br /&gt;
&lt;br /&gt;
This is the home view for a student. It shows all of the student's assignments, arranged by the course they pertain to.&lt;br /&gt;
&lt;br /&gt;
[[File:student_view_assignments_page.png|800px|thumb|center|Student View Assignments Page]]&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; returns all tasks for the logged-in student in one call&lt;br /&gt;
* Tasks are grouped by course on the dashboard&lt;br /&gt;
* Each task shows current stage and days remaining, if applicable&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want to view a single assignment's deadlines and details ===&lt;br /&gt;
&lt;br /&gt;
A student can perform several tasks for each assignment, depending upon the role they play (e.g., whether they are a submitter, a reviewer, or both.&lt;br /&gt;
&lt;br /&gt;
[[File:student_view_assignment_detail_page.png|800px|thumb|center|Student View Assignment Detail Page]]&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt; returns the task plus a unified &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt; timeline&lt;br /&gt;
* The timeline visually distinguishes completed, current, and pending stages&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/team&amp;lt;/code&amp;gt; returns teammates grouped by course&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want my team's score heatgrid to load fast and stay readable while scrolling ===&lt;br /&gt;
&lt;br /&gt;
The heatgrid shows all of the reviews that have been written of the team's submission (or, the student's submission, if this is not a team assignment).  The user can hover over any cell with a score that is underlined, meaning that the reviewer has made a comment to go along with the score.  Hovering over a cell will show the comments it contains.&lt;br /&gt;
&lt;br /&gt;
[[File:student_view_assignment_grades_page.png|800px|thumb|left|Student View Assignment View Team Grades]]&lt;br /&gt;
&amp;lt;br clear=&amp;quot;all&amp;quot; /&amp;gt;&lt;br /&gt;
Clicking on &amp;quot;Feedback&amp;quot; will show all of the feedback along with the scores.&lt;br /&gt;
[[File:student_view_assignment_feedback_page.png|800px|thumb|left|Student View Assignment View Feedbacks]]&lt;br /&gt;
&amp;lt;br clear=&amp;quot;all&amp;quot; /&amp;gt;&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* The heatgrid loads from a single &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt; call&lt;br /&gt;
* The first two columns (item number &amp;quot;&amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;&amp;quot; and Item) remain fixed during horizontal scroll, along with section headers&lt;br /&gt;
* Hovering over a score with a comment shows the corresponding reviewer's feedback as a tooltip&lt;br /&gt;
&lt;br /&gt;
=== A student who is not permitted to submit or review will not see those task links ===&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;quot;Your work&amp;quot; is hidden when &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;quot;Others' work&amp;quot; is hidden when &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== As an instructor, I want calculated review grades to reflect each rubric's configured weight ===&lt;br /&gt;
&lt;br /&gt;
Grades are calculated in &amp;lt;code&amp;gt;Response.rb&amp;lt;/code&amp;gt; (score for a particular review by a specific reviewer), &amp;lt;code&amp;gt;ResponseMap.rb&amp;lt;/code&amp;gt; (composite score for reviews in all rounds by the same reviewer) and &amp;lt;code&amp;gt;AssignmentTeam.rb&amp;lt;/code&amp;gt; (combined score for all reviews by all reviewers).&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; weights each round's normalized score by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt; both compute their averages through the same shared &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
=== Student Task Overview ===&lt;br /&gt;
A StudentTask is a task that a student performs in the course of doing an assignment, e.g., forming a team, choosing a topic, submitting work, or reviewing work. No dedicated &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; database table exists. A &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; is a plain Ruby object composed from &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; and its associated &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignedUpTeam&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;ProjectTopic&amp;lt;/code&amp;gt;, and permission fields (&amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;permission_granted&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Request / Response Flow ====&lt;br /&gt;
* Student navigates to &amp;lt;code&amp;gt;/student_tasks&amp;lt;/code&amp;gt; → frontend calls &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTask.tasks(user)&amp;lt;/code&amp;gt; preloads &amp;lt;code&amp;gt;assignment: :course&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;:user&amp;lt;/code&amp;gt; associations, then maps each &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; through &amp;lt;code&amp;gt;StudentTask.create_from_participant&amp;lt;/code&amp;gt;&lt;br /&gt;
* Selecting an assignment navigates to the detail route → frontend calls &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTask.timeline_events&amp;lt;/code&amp;gt; merges the assignment's due dates with timestamps of reviews done of the logged-in user's work into one sorted timeline&lt;br /&gt;
* For the heatgrid: frontend calls &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt; → &amp;lt;code&amp;gt;GradesController#get_team_scores&amp;lt;/code&amp;gt; eager-loads &amp;lt;code&amp;gt;responses: { scores: :item }&amp;lt;/code&amp;gt; and calls &amp;lt;code&amp;gt;insert_section_headers&amp;lt;/code&amp;gt; to splice section-header marker objects into the response before it is returned to the frontend&lt;br /&gt;
&lt;br /&gt;
=== Aggregation of Scores Assigned by a Reviewer ===&lt;br /&gt;
* &amp;lt;code&amp;gt;Response#aggregate_questionnaire_score&amp;lt;/code&amp;gt; - item-weighted score for one submitted response&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; — normalizes a map's score per round and weights it by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score(maps)&amp;lt;/code&amp;gt; — weighted average across multiple maps, the single implementation shared by &amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Backend Implementation ==&lt;br /&gt;
&lt;br /&gt;
=== StudentTasksController ===&lt;br /&gt;
* &amp;lt;code&amp;gt;list&amp;lt;/code&amp;gt; — all &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; objects for the logged-in user&lt;br /&gt;
* &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt; — single task by participant ID with the unified timeline&lt;br /&gt;
* &amp;lt;code&amp;gt;team&amp;lt;/code&amp;gt; — teammates grouped by course name (&amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
=== GradesController — N+1 elimination ===&lt;br /&gt;
&amp;lt;code&amp;gt;get_team_scores&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;get_my_scores_data&amp;lt;/code&amp;gt; eager-load &amp;lt;code&amp;gt;responses: { scores: :item }&amp;lt;/code&amp;gt; on the &amp;lt;code&amp;gt;ReviewResponseMap&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;TeammateReviewResponseMap&amp;lt;/code&amp;gt; queries. &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt; filters SectionHeader scores via Ruby &amp;lt;code&amp;gt;.reject&amp;lt;/code&amp;gt; instead of &amp;lt;code&amp;gt;.joins&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;.where&amp;lt;/code&amp;gt; — calling &amp;lt;code&amp;gt;.where&amp;lt;/code&amp;gt; on an already-preloaded association silently bypasses the cache and re-queries the database. &amp;lt;code&amp;gt;get_answer&amp;lt;/code&amp;gt; now accepts the parent &amp;lt;code&amp;gt;response&amp;lt;/code&amp;gt; directly instead of re-fetching it via &amp;lt;code&amp;gt;score.response&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== insert_section_headers ===&lt;br /&gt;
Inserts &amp;lt;code&amp;gt;{ type: &amp;quot;header&amp;quot;, txt: &amp;quot;...&amp;quot; }&amp;lt;/code&amp;gt; marker objects into the scores array at positions derived from each &amp;lt;code&amp;gt;SectionHeader&amp;lt;/code&amp;gt; item's sequence order, so the frontend renders section heading rows between score rows without those rows affecting item numbering.&lt;br /&gt;
&lt;br /&gt;
=== Review Grade Calculation ===&lt;br /&gt;
For details on how peer review grades are computed, see [[Score Calculation]].&lt;br /&gt;
&lt;br /&gt;
=== Comprehensive Testing ===&lt;br /&gt;
* Model specs for &amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;create_from_participant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_events_for_assignment&amp;lt;/code&amp;gt;&lt;br /&gt;
* Request specs for &amp;lt;code&amp;gt;student_tasks/list&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;student_tasks/show/:id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;student_tasks/team&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;grades/:id/view_our_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
* Routing specs verifying the above routes resolve correctly&lt;br /&gt;
&lt;br /&gt;
== Frontend Implementation ==&lt;br /&gt;
=== New/Updated Files ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! File !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTasks.tsx&amp;lt;/code&amp;gt; || Course-grouped assignment dashboard (rewritten from a topic sign-up sheet)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTasksList.tsx&amp;lt;/code&amp;gt; || Sidebar — not-started tasks, revisions, teammates by course&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTaskDetail.tsx&amp;lt;/code&amp;gt; || Per-assignment detail page — timeline, task links, conditional &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; rendering&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ViewTeamGrades.module.scss&amp;lt;/code&amp;gt; || CSS Modules conversion; scoped &amp;lt;code&amp;gt;c1&amp;lt;/code&amp;gt;–&amp;lt;code&amp;gt;c5&amp;lt;/code&amp;gt; color classes, sticky-column and tooltip styles&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewTable.tsx&amp;lt;/code&amp;gt; || Single-call heatgrid; Scores/Feedback toggle; sticky SectionHeader rows&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewTableRow.tsx&amp;lt;/code&amp;gt; || Sticky &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;/Question columns; reduced row height; hover tooltip via &amp;lt;code&amp;gt;data-question&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;FeedbackTable.tsx&amp;lt;/code&amp;gt; || Feedback-mode table mirroring &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt;'s sticky-column layout&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;heatgridUtils.ts&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;RoundRow&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;isHeader()&amp;lt;/code&amp;gt; types; fixed &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; numbering to skip SectionHeader sentinels&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== StudentTasks (List View) ===&lt;br /&gt;
* Single &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; call, grouped by course&lt;br /&gt;
* Fixed: &amp;quot;Assignments&amp;quot; heading was rendering as body text because its class was referenced as a plain string instead of the CSS Modules export (&amp;lt;code&amp;gt;styles['assignments-title']&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
=== StudentTaskDetail (Detail View) ===&lt;br /&gt;
* Visual timeline with progress line and stage nodes (completed/current/pending)&lt;br /&gt;
* Task links list — &amp;quot;Your work&amp;quot; and &amp;quot;Others' work&amp;quot; conditionally rendered based on &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;quot;Send Email To Reviewers&amp;quot; button repositioned to align with the task list rather than floating disconnected at the page top&lt;br /&gt;
&lt;br /&gt;
=== ViewTeamGrades (Heatgrid) ===&lt;br /&gt;
* Consolidated ~6 sequential API calls into one &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
* SectionHeader rows split into a sticky label cell + scrolling spacer cell so they stay fixed during horizontal scroll&lt;br /&gt;
* Score cells show reviewer feedback as a hover tooltip&lt;br /&gt;
* Round headings simplified from &amp;quot;Review (Round: 1 of 2)&amp;quot; to &amp;quot;Round 1&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Design Principles ==&lt;br /&gt;
&lt;br /&gt;
=== Single Responsibility ===&lt;br /&gt;
Score aggregation is layered: &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; owns item-level scoring, &amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt; owns per-map/per-round weighting, &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; owns cross-map averaging — each level only knows about the one below it.&lt;br /&gt;
&lt;br /&gt;
=== Don't Repeat Yourself ===&lt;br /&gt;
The weighted-average logic that was duplicated between &amp;lt;code&amp;gt;AssignmentTeam#aggregate_review_grade&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;ReviewAggregator#compute_average_review_score&amp;lt;/code&amp;gt; was consolidated into a single &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt;, and the now-dead &amp;lt;code&amp;gt;ReviewAggregator&amp;lt;/code&amp;gt; concern was deleted.&lt;br /&gt;
&lt;br /&gt;
=== Performance by Design ===&lt;br /&gt;
Eager loading (&amp;lt;code&amp;gt;.includes&amp;lt;/code&amp;gt;) and in-memory filtering (&amp;lt;code&amp;gt;.reject&amp;lt;/code&amp;gt;) were chosen deliberately over additional database round-trips after profiling showed the heatgrid endpoint firing hundreds of queries under the old implementation.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
&lt;br /&gt;
=== Backend Tests ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! # !! Test !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| 1 || &amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt; groups teammates by course || Returns &amp;lt;code&amp;gt;{course_name =&amp;gt; [fullnames]}&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 2 || &amp;lt;code&amp;gt;all_teammates&amp;lt;/code&amp;gt; excludes calibrated assignments || Calibrated-assignment teammates excluded&lt;br /&gt;
|-&lt;br /&gt;
| 3 || &amp;lt;code&amp;gt;all_teammates&amp;lt;/code&amp;gt; excludes the user themself || Solo team returns empty hash&lt;br /&gt;
|-&lt;br /&gt;
| 4 || &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; returns all tasks for user || 200 OK with task array&lt;br /&gt;
|-&lt;br /&gt;
| 5 || &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt; returns due_dates timeline || 200 OK with &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt; array&lt;br /&gt;
|-&lt;br /&gt;
| 6 || &amp;lt;code&amp;gt;GET /student_tasks/team&amp;lt;/code&amp;gt; groups teammates by course || 200 OK with grouped hash&lt;br /&gt;
|-&lt;br /&gt;
| 7 || Heatgrid &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; column excludes SectionHeaders from numbering || Sequential numbering skips header rows&lt;br /&gt;
|-&lt;br /&gt;
| 8 || &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt; filters SectionHeaders without re-querying || No additional SQL fired against preloaded association&lt;br /&gt;
|-&lt;br /&gt;
| 9 || &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; weight validation || &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt; must be 0 when rubric has no scored questions&lt;br /&gt;
|-&lt;br /&gt;
| 10 || &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; || Weighted average matches Σ(grade × weight) / Σ(weight)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Frontend Tests ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! # !! Test !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| 1 || &amp;lt;code&amp;gt;StudentTasks&amp;lt;/code&amp;gt; renders &amp;quot;Assignments&amp;quot; heading with correct styling || Heading uses module class, renders bold and large&lt;br /&gt;
|-&lt;br /&gt;
| 2 || &amp;lt;code&amp;gt;StudentTaskDetail&amp;lt;/code&amp;gt; hides &amp;quot;Your work&amp;quot; when &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; is false || List item not rendered&lt;br /&gt;
|-&lt;br /&gt;
| 3 || &amp;lt;code&amp;gt;StudentTaskDetail&amp;lt;/code&amp;gt; hides &amp;quot;Others' work&amp;quot; when &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; is false || List item not rendered&lt;br /&gt;
|-&lt;br /&gt;
| 4 || &amp;lt;code&amp;gt;heatgridUtils&amp;lt;/code&amp;gt; itemNumber skips SectionHeader rows || Numbering matches scored items only&lt;br /&gt;
|-&lt;br /&gt;
| 5 || &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt; SectionHeader row stays fixed during horizontal scroll || Header cell &amp;lt;code&amp;gt;position: sticky&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 6 || &amp;lt;code&amp;gt;ReviewTableRow&amp;lt;/code&amp;gt; shows tooltip only when a comment exists || No tooltip box when &amp;lt;code&amp;gt;data-question&amp;lt;/code&amp;gt; is undefined&lt;br /&gt;
|-&lt;br /&gt;
| 7 || Heatgrid column borders render as &amp;lt;code&amp;gt;#ddd&amp;lt;/code&amp;gt; || No black borders after &amp;lt;code&amp;gt;tbl_heat&amp;lt;/code&amp;gt; class applied&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Relevant Links ==&lt;br /&gt;
&lt;br /&gt;
=== Repos ===&lt;br /&gt;
* https://github.com/expertiza/reimplementation-back-end&lt;br /&gt;
* https://github.com/expertiza/reimplementation-front-end&lt;br /&gt;
&lt;br /&gt;
=== Pull Requests ===&lt;br /&gt;
* Backend: https://github.com/expertiza/reimplementation-back-end/pull/352&lt;br /&gt;
* Frontend: https://github.com/expertiza/reimplementation-front-end/pull/182&lt;br /&gt;
&lt;br /&gt;
=== Demo Video ===&lt;br /&gt;
* (add link once recorded)&lt;br /&gt;
&lt;br /&gt;
== Developer Guidance ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Setup &amp;amp; Run (via Docker) ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
docker compose up&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Within the container ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
bundle exec rspec spec/models/student_task_spec.rb&lt;br /&gt;
bundle exec rspec spec/requests/api/v1/student_tasks_controller_spec.rb&lt;br /&gt;
bundle exec rspec spec/requests/api/v1/grades_controller_spec.rb&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
==== Setup &amp;amp; Run ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm install&lt;br /&gt;
npm start&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Running Frontend Tests ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm test -- StudentTaskDetail   # run a single suite&lt;br /&gt;
npm test                        # run full suite&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Future Scope ==&lt;br /&gt;
&lt;br /&gt;
=== Reviewer reputation-weighted grading ===&lt;br /&gt;
Integrate an external review-quality grader (e.g. an LLM-based reviewer grader) so &amp;lt;code&amp;gt;ResponseMap.reviewer_reputation_for&amp;lt;/code&amp;gt; reflects actual review quality instead of the current &amp;lt;code&amp;gt;1.0&amp;lt;/code&amp;gt; placeholder for every reviewer.&lt;br /&gt;
&lt;br /&gt;
=== Email-the-authors integration ===&lt;br /&gt;
Wire &amp;quot;Send Email To Reviewers&amp;quot; to a real backend endpoint instead of the current placeholder route.&lt;br /&gt;
&lt;br /&gt;
=== Dedicated StudentTask abstraction ===&lt;br /&gt;
&amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; remains a composed, non-persisted object. A future dedicated table could centralize derived state and simplify queries if the feature's scope grows further.&lt;br /&gt;
&lt;br /&gt;
== Mentor ==&lt;br /&gt;
[mailto:kgudipe@ncsu.edu Koushik Gudipelly]&lt;br /&gt;
&lt;br /&gt;
== Members ==&lt;br /&gt;
[mailto:rsgopara@ncsu.edu Ravi Goparaju]&lt;br /&gt;
[mailto:xmi@ncsu.edu Xiangjun Mi]&lt;br /&gt;
&lt;br /&gt;
== Completed by ==&lt;br /&gt;
[mailto:blalu@ncsu.edu Bestin Lalu]&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2602._Reimplement_student_task_view&amp;diff=168252</id>
		<title>CSC/ECE 517 Spring 2026 - E2602. Reimplement student task view</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2602._Reimplement_student_task_view&amp;diff=168252"/>
		<updated>2026-07-07T00:22:03Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* As an instructor, I want calculated review grades to reflect each rubric's configured weight */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= CSC/ECE 517 Spring 2026 - E2602. Reimplement Student Task View =&lt;br /&gt;
&lt;br /&gt;
== About Expertiza ==&lt;br /&gt;
Expertiza is a robust, open-source web platform designed to empower educational environments that emphasize project-based learning. It streamlines collaborative activities including peer reviews, surveys, and team assignments — giving instructors tools to manage assignments and track performance, and giving students a single place to submit work, review peers, and track feedback.&lt;br /&gt;
&lt;br /&gt;
== Introduction ==&lt;br /&gt;
''This page documents the reimplementation of the Student Task view. It supersedes the earlier [[E2602]] team's version. All searches for E2602 will hit here.''&lt;br /&gt;
This iteration of E2602 hardens and extends the Student Task View: consolidating the student dashboard and detail pages into single-call REST endpoints, eliminating N+1 queries in the peer-review score heatgrid, and fixing several display/permission bugs surfaced by manual QA against real assignment data.&lt;br /&gt;
&lt;br /&gt;
=== Tech Stack ===&lt;br /&gt;
* '''Backend:''' Ruby on Rails (API-only)&lt;br /&gt;
* '''Frontend:''' React + TypeScript&lt;br /&gt;
* '''Database:''' MySQL&lt;br /&gt;
* '''Backend Testing:''' RSpec&lt;br /&gt;
* '''Frontend Testing:''' Jest, React Testing Library&lt;br /&gt;
&lt;br /&gt;
=== Key Improvements ===&lt;br /&gt;
* Eliminated N+1 queries in the score heatgrid (&amp;lt;code&amp;gt;get_team_scores&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_my_scores_data&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_answer&amp;lt;/code&amp;gt;) via &amp;lt;code&amp;gt;.includes(responses: { scores: :item })&amp;lt;/code&amp;gt; and in-memory filtering instead of re-querying preloaded associations&lt;br /&gt;
* Converted &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt;'s stylesheet to CSS Modules, fixing a Bootstrap class collision and scoping color classes (&amp;lt;code&amp;gt;c1&amp;lt;/code&amp;gt;–&amp;lt;code&amp;gt;c5&amp;lt;/code&amp;gt;) correctly&lt;br /&gt;
* Redesigned the score/feedback heatgrid with sticky Question column during horizontal scroll&lt;br /&gt;
* Added hover tooltips on score cells showing the reviewer's written feedback&lt;br /&gt;
* Rewrote &amp;lt;code&amp;gt;StudentTasks&amp;lt;/code&amp;gt; from a (mis-implemented) topic sign-up sheet into a proper course-grouped assignment dashboard&lt;br /&gt;
* Added participant-level &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; permission checks, hiding &amp;quot;Your work&amp;quot; / &amp;quot;Others' work&amp;quot; task links accordingly&lt;br /&gt;
* Refactored review grade calculation: &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; now weights each round by its &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;; &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; centralizes the weighted-average logic previously duplicated across &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Problem Statement ==&lt;br /&gt;
The previous Student Task View had several concrete defects:&lt;br /&gt;
* The score heatgrid fired one SQL query per response/score across all reviewers and rounds, producing hundreds of queries for a single page load&lt;br /&gt;
* SectionHeader rubric rows (non-scorable dividers) were incorrectly counted in the &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; column and lost their fixed position during horizontal scroll&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTasks.tsx&amp;lt;/code&amp;gt; was previously implemented as a topic sign-up sheet, not a task dashboard, and required a full rewrite&lt;br /&gt;
* The page did not respect participant-level &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; flags — all task links were always shown regardless of permission&lt;br /&gt;
* Review grade calculation treated every reviewer and every round equally, ignoring each rubric's configured &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== User Stories ==&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want to see all my assignments in one place ===&lt;br /&gt;
&lt;br /&gt;
This is the home view for a student. It shows all of the student's assignments, arranged by the course they pertain to.&lt;br /&gt;
&lt;br /&gt;
[[File:student_view_assignments_page.png|800px|thumb|center|Student View Assignments Page]]&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; returns all tasks for the logged-in student in one call&lt;br /&gt;
* Tasks are grouped by course on the dashboard&lt;br /&gt;
* Each task shows current stage and days remaining, if applicable&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want to view a single assignment's deadlines and details ===&lt;br /&gt;
&lt;br /&gt;
A student can perform several tasks for each assignment, depending upon the role they play (e.g., whether they are a submitter, a reviewer, or both.&lt;br /&gt;
&lt;br /&gt;
[[File:student_view_assignment_detail_page.png|800px|thumb|center|Student View Assignment Detail Page]]&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt; returns the task plus a unified &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt; timeline&lt;br /&gt;
* The timeline visually distinguishes completed, current, and pending stages&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/team&amp;lt;/code&amp;gt; returns teammates grouped by course&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want my team's score heatgrid to load fast and stay readable while scrolling ===&lt;br /&gt;
&lt;br /&gt;
The heatgrid shows all of the reviews that have been written of the team's submission (or, the student's submission, if this is not a team assignment).  The user can hover over any cell with a score that is underlined, meaning that the reviewer has made a comment to go along with the score.  Hovering over a cell will show the comments it contains.&lt;br /&gt;
&lt;br /&gt;
[[File:student_view_assignment_grades_page.png|800px|thumb|left|Student View Assignment View Team Grades]]&lt;br /&gt;
&amp;lt;br clear=&amp;quot;all&amp;quot; /&amp;gt;&lt;br /&gt;
Clicking on &amp;quot;Feedback&amp;quot; will show all of the feedback along with the scores.&lt;br /&gt;
[[File:student_view_assignment_feedback_page.png|800px|thumb|left|Student View Assignment View Feedbacks]]&lt;br /&gt;
&amp;lt;br clear=&amp;quot;all&amp;quot; /&amp;gt;&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* The heatgrid loads from a single &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt; call&lt;br /&gt;
* The first two columns (item number &amp;quot;&amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;&amp;quot; and Item) remain fixed during horizontal scroll, along with section headers&lt;br /&gt;
* Hovering over a score with a comment shows the corresponding reviewer's feedback as a tooltip&lt;br /&gt;
&lt;br /&gt;
=== A student who is not permitted to submit or review will not see those task links ===&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;quot;Your work&amp;quot; is hidden when &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;quot;Others' work&amp;quot; is hidden when &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== As an instructor, I want calculated review grades to reflect each rubric's configured weight ===&lt;br /&gt;
&lt;br /&gt;
Grades are calculated in &amp;lt;code&amp;gt;Response.rb&amp;lt;/code&amp;gt; (score for a particular review by a specific reviewer), &amp;lt;code&amp;gt;ResponseMap.rb&amp;lt;/code&amp;gt; (composite score for reviews in all rounds by the same reviewer) and &amp;lt;code&amp;gt;AssignmentTeam.rb&amp;lt;/code&amp;gt; (combined score for all reviews by all reviewers).&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; weights each round's normalized score by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt; both compute their averages through the same shared &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
=== Student Task Overview ===&lt;br /&gt;
A StudentTask is a task that a student performs in the course of doing an assignment, e.g., forming a team, choosing a topic, submitting work, or reviewing work. No dedicated &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; database table exists. A &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; is a plain Ruby object composed from &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; and its associated &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignedUpTeam&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;ProjectTopic&amp;lt;/code&amp;gt;, and permission fields (&amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;permission_granted&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Request / Response Flow ====&lt;br /&gt;
* Student navigates to &amp;lt;code&amp;gt;/student_tasks&amp;lt;/code&amp;gt; → frontend calls &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTask.tasks(user)&amp;lt;/code&amp;gt; preloads &amp;lt;code&amp;gt;assignment: :course&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;:user&amp;lt;/code&amp;gt; associations, then maps each &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; through &amp;lt;code&amp;gt;StudentTask.create_from_participant&amp;lt;/code&amp;gt;&lt;br /&gt;
* Selecting an assignment navigates to the detail route → frontend calls &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTask.timeline_events&amp;lt;/code&amp;gt; merges the assignment's due dates with timestamps of reviews done of the logged-in user's work into one sorted timeline&lt;br /&gt;
* For the heatgrid: frontend calls &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt; → &amp;lt;code&amp;gt;GradesController#get_team_scores&amp;lt;/code&amp;gt; eager-loads &amp;lt;code&amp;gt;responses: { scores: :item }&amp;lt;/code&amp;gt; and calls &amp;lt;code&amp;gt;insert_section_headers&amp;lt;/code&amp;gt; to splice section-header marker objects into the response before it is returned to the frontend&lt;br /&gt;
&lt;br /&gt;
=== Aggregation of Scores Assigned by a Reviewer ===&lt;br /&gt;
* &amp;lt;code&amp;gt;Response#aggregate_questionnaire_score&amp;lt;/code&amp;gt; - item-weighted score for one submitted response&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; — normalizes a map's score per round and weights it by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score(maps)&amp;lt;/code&amp;gt; — weighted average across multiple maps, the single implementation shared by &amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Backend Implementation ==&lt;br /&gt;
&lt;br /&gt;
=== StudentTasksController ===&lt;br /&gt;
* &amp;lt;code&amp;gt;list&amp;lt;/code&amp;gt; — all &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; objects for the logged-in user&lt;br /&gt;
* &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt; — single task by participant ID with the unified timeline&lt;br /&gt;
* &amp;lt;code&amp;gt;team&amp;lt;/code&amp;gt; — teammates grouped by course name (&amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
=== GradesController — N+1 elimination ===&lt;br /&gt;
&amp;lt;code&amp;gt;get_team_scores&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;get_my_scores_data&amp;lt;/code&amp;gt; eager-load &amp;lt;code&amp;gt;responses: { scores: :item }&amp;lt;/code&amp;gt; on the &amp;lt;code&amp;gt;ReviewResponseMap&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;TeammateReviewResponseMap&amp;lt;/code&amp;gt; queries. &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt; filters SectionHeader scores via Ruby &amp;lt;code&amp;gt;.reject&amp;lt;/code&amp;gt; instead of &amp;lt;code&amp;gt;.joins&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;.where&amp;lt;/code&amp;gt; — calling &amp;lt;code&amp;gt;.where&amp;lt;/code&amp;gt; on an already-preloaded association silently bypasses the cache and re-queries the database. &amp;lt;code&amp;gt;get_answer&amp;lt;/code&amp;gt; now accepts the parent &amp;lt;code&amp;gt;response&amp;lt;/code&amp;gt; directly instead of re-fetching it via &amp;lt;code&amp;gt;score.response&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== insert_section_headers ===&lt;br /&gt;
Inserts &amp;lt;code&amp;gt;{ type: &amp;quot;header&amp;quot;, txt: &amp;quot;...&amp;quot; }&amp;lt;/code&amp;gt; marker objects into the scores array at positions derived from each &amp;lt;code&amp;gt;SectionHeader&amp;lt;/code&amp;gt; item's sequence order, so the frontend renders section heading rows between score rows without those rows affecting item numbering.&lt;br /&gt;
&lt;br /&gt;
=== Review Grade Calculation ===&lt;br /&gt;
For details on how peer review grades are computed, see [[Score Calculation]].&lt;br /&gt;
&lt;br /&gt;
=== Comprehensive Testing ===&lt;br /&gt;
* Model specs for &amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;create_from_participant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_events_for_assignment&amp;lt;/code&amp;gt;&lt;br /&gt;
* Request specs for &amp;lt;code&amp;gt;student_tasks/list&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;student_tasks/show/:id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;student_tasks/team&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;grades/:id/view_our_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
* Routing specs verifying the above routes resolve correctly&lt;br /&gt;
&lt;br /&gt;
== Frontend Implementation ==&lt;br /&gt;
=== New/Updated Files ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! File !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTasks.tsx&amp;lt;/code&amp;gt; || Course-grouped assignment dashboard (rewritten from a topic sign-up sheet)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTasksList.tsx&amp;lt;/code&amp;gt; || Sidebar — not-started tasks, revisions, teammates by course&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTaskDetail.tsx&amp;lt;/code&amp;gt; || Per-assignment detail page — timeline, task links, conditional &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; rendering&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ViewTeamGrades.module.scss&amp;lt;/code&amp;gt; || CSS Modules conversion; scoped &amp;lt;code&amp;gt;c1&amp;lt;/code&amp;gt;–&amp;lt;code&amp;gt;c5&amp;lt;/code&amp;gt; color classes, sticky-column and tooltip styles&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewTable.tsx&amp;lt;/code&amp;gt; || Single-call heatgrid; Scores/Feedback toggle; sticky SectionHeader rows&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewTableRow.tsx&amp;lt;/code&amp;gt; || Sticky &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;/Question columns; reduced row height; hover tooltip via &amp;lt;code&amp;gt;data-question&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;FeedbackTable.tsx&amp;lt;/code&amp;gt; || Feedback-mode table mirroring &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt;'s sticky-column layout&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;heatgridUtils.ts&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;RoundRow&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;isHeader()&amp;lt;/code&amp;gt; types; fixed &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; numbering to skip SectionHeader sentinels&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== StudentTasks (List View) ===&lt;br /&gt;
* Single &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; call, grouped by course&lt;br /&gt;
* Fixed: &amp;quot;Assignments&amp;quot; heading was rendering as body text because its class was referenced as a plain string instead of the CSS Modules export (&amp;lt;code&amp;gt;styles['assignments-title']&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
=== StudentTaskDetail (Detail View) ===&lt;br /&gt;
* Visual timeline with progress line and stage nodes (completed/current/pending)&lt;br /&gt;
* Task links list — &amp;quot;Your work&amp;quot; and &amp;quot;Others' work&amp;quot; conditionally rendered based on &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;quot;Send Email To Reviewers&amp;quot; button repositioned to align with the task list rather than floating disconnected at the page top&lt;br /&gt;
&lt;br /&gt;
=== ViewTeamGrades (Heatgrid) ===&lt;br /&gt;
* Consolidated ~6 sequential API calls into one &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
* SectionHeader rows split into a sticky label cell + scrolling spacer cell so they stay fixed during horizontal scroll&lt;br /&gt;
* Score cells show reviewer feedback as a hover tooltip&lt;br /&gt;
* Round headings simplified from &amp;quot;Review (Round: 1 of 2)&amp;quot; to &amp;quot;Round 1&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Design Principles ==&lt;br /&gt;
&lt;br /&gt;
=== Single Responsibility ===&lt;br /&gt;
Score aggregation is layered: &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; owns item-level scoring, &amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt; owns per-map/per-round weighting, &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; owns cross-map averaging — each level only knows about the one below it.&lt;br /&gt;
&lt;br /&gt;
=== Don't Repeat Yourself ===&lt;br /&gt;
The weighted-average logic that was duplicated between &amp;lt;code&amp;gt;AssignmentTeam#aggregate_review_grade&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;ReviewAggregator#compute_average_review_score&amp;lt;/code&amp;gt; was consolidated into a single &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt;, and the now-dead &amp;lt;code&amp;gt;ReviewAggregator&amp;lt;/code&amp;gt; concern was deleted.&lt;br /&gt;
&lt;br /&gt;
=== Performance by Design ===&lt;br /&gt;
Eager loading (&amp;lt;code&amp;gt;.includes&amp;lt;/code&amp;gt;) and in-memory filtering (&amp;lt;code&amp;gt;.reject&amp;lt;/code&amp;gt;) were chosen deliberately over additional database round-trips after profiling showed the heatgrid endpoint firing hundreds of queries under the old implementation.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
&lt;br /&gt;
=== Backend Tests ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! # !! Test !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| 1 || &amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt; groups teammates by course || Returns &amp;lt;code&amp;gt;{course_name =&amp;gt; [fullnames]}&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 2 || &amp;lt;code&amp;gt;all_teammates&amp;lt;/code&amp;gt; excludes calibrated assignments || Calibrated-assignment teammates excluded&lt;br /&gt;
|-&lt;br /&gt;
| 3 || &amp;lt;code&amp;gt;all_teammates&amp;lt;/code&amp;gt; excludes the user themself || Solo team returns empty hash&lt;br /&gt;
|-&lt;br /&gt;
| 4 || &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; returns all tasks for user || 200 OK with task array&lt;br /&gt;
|-&lt;br /&gt;
| 5 || &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt; returns due_dates timeline || 200 OK with &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt; array&lt;br /&gt;
|-&lt;br /&gt;
| 6 || &amp;lt;code&amp;gt;GET /student_tasks/team&amp;lt;/code&amp;gt; groups teammates by course || 200 OK with grouped hash&lt;br /&gt;
|-&lt;br /&gt;
| 7 || Heatgrid &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; column excludes SectionHeaders from numbering || Sequential numbering skips header rows&lt;br /&gt;
|-&lt;br /&gt;
| 8 || &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt; filters SectionHeaders without re-querying || No additional SQL fired against preloaded association&lt;br /&gt;
|-&lt;br /&gt;
| 9 || &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; weight validation || &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt; must be 0 when rubric has no scored questions&lt;br /&gt;
|-&lt;br /&gt;
| 10 || &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; || Weighted average matches Σ(grade × weight) / Σ(weight)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Frontend Tests ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! # !! Test !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| 1 || &amp;lt;code&amp;gt;StudentTasks&amp;lt;/code&amp;gt; renders &amp;quot;Assignments&amp;quot; heading with correct styling || Heading uses module class, renders bold and large&lt;br /&gt;
|-&lt;br /&gt;
| 2 || &amp;lt;code&amp;gt;StudentTaskDetail&amp;lt;/code&amp;gt; hides &amp;quot;Your work&amp;quot; when &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; is false || List item not rendered&lt;br /&gt;
|-&lt;br /&gt;
| 3 || &amp;lt;code&amp;gt;StudentTaskDetail&amp;lt;/code&amp;gt; hides &amp;quot;Others' work&amp;quot; when &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; is false || List item not rendered&lt;br /&gt;
|-&lt;br /&gt;
| 4 || &amp;lt;code&amp;gt;heatgridUtils&amp;lt;/code&amp;gt; itemNumber skips SectionHeader rows || Numbering matches scored items only&lt;br /&gt;
|-&lt;br /&gt;
| 5 || &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt; SectionHeader row stays fixed during horizontal scroll || Header cell &amp;lt;code&amp;gt;position: sticky&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 6 || &amp;lt;code&amp;gt;ReviewTableRow&amp;lt;/code&amp;gt; shows tooltip only when a comment exists || No tooltip box when &amp;lt;code&amp;gt;data-question&amp;lt;/code&amp;gt; is undefined&lt;br /&gt;
|-&lt;br /&gt;
| 7 || Heatgrid column borders render as &amp;lt;code&amp;gt;#ddd&amp;lt;/code&amp;gt; || No black borders after &amp;lt;code&amp;gt;tbl_heat&amp;lt;/code&amp;gt; class applied&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Relevant Links ==&lt;br /&gt;
&lt;br /&gt;
=== Repos ===&lt;br /&gt;
* https://github.com/expertiza/reimplementation-back-end&lt;br /&gt;
* https://github.com/expertiza/reimplementation-front-end&lt;br /&gt;
&lt;br /&gt;
=== Pull Requests ===&lt;br /&gt;
* Backend: https://github.com/expertiza/reimplementation-back-end/pull/352&lt;br /&gt;
* Frontend: https://github.com/expertiza/reimplementation-front-end/pull/182&lt;br /&gt;
&lt;br /&gt;
=== Demo Video ===&lt;br /&gt;
* (add link once recorded)&lt;br /&gt;
&lt;br /&gt;
== Developer Guidance ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Setup &amp;amp; Run (via Docker) ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
docker compose up&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Within the container ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
bundle exec rspec spec/models/student_task_spec.rb&lt;br /&gt;
bundle exec rspec spec/requests/api/v1/student_tasks_controller_spec.rb&lt;br /&gt;
bundle exec rspec spec/requests/api/v1/grades_controller_spec.rb&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
==== Setup &amp;amp; Run ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm install&lt;br /&gt;
npm start&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Running Frontend Tests ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm test -- StudentTaskDetail   # run a single suite&lt;br /&gt;
npm test                        # run full suite&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Future Scope ==&lt;br /&gt;
&lt;br /&gt;
=== Reviewer reputation-weighted grading ===&lt;br /&gt;
Integrate an external review-quality grader (e.g. an LLM-based reviewer grader) so &amp;lt;code&amp;gt;ResponseMap.reviewer_reputation_for&amp;lt;/code&amp;gt; reflects actual review quality instead of the current &amp;lt;code&amp;gt;1.0&amp;lt;/code&amp;gt; placeholder for every reviewer.&lt;br /&gt;
&lt;br /&gt;
=== Email-the-authors integration ===&lt;br /&gt;
Wire &amp;quot;Send Email To Reviewers&amp;quot; to a real backend endpoint instead of the current placeholder route.&lt;br /&gt;
&lt;br /&gt;
=== Dedicated StudentTask abstraction ===&lt;br /&gt;
&amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; remains a composed, non-persisted object. A future dedicated table could centralize derived state and simplify queries if the feature's scope grows further.&lt;br /&gt;
&lt;br /&gt;
== Mentor ==&lt;br /&gt;
[mailto:kgudipe@ncsu.edu Koushik Gudipelly]&lt;br /&gt;
&lt;br /&gt;
== Members ==&lt;br /&gt;
[mailto:rsgopara@ncsu.edu Ravi Goparaju]&lt;br /&gt;
[mailto:xmi@ncsu.edu Xiangjun Mi]&lt;br /&gt;
&lt;br /&gt;
== Completed by ==&lt;br /&gt;
[mailto:blalu@ncsu.edu Bestin Lalu]&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2602._Reimplement_student_task_view&amp;diff=168249</id>
		<title>CSC/ECE 517 Spring 2026 - E2602. Reimplement student task view</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2602._Reimplement_student_task_view&amp;diff=168249"/>
		<updated>2026-07-07T00:17:45Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* As a student, I want my team's score heatgrid to load fast and stay readable while scrolling */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= CSC/ECE 517 Spring 2026 - E2602. Reimplement Student Task View =&lt;br /&gt;
&lt;br /&gt;
== About Expertiza ==&lt;br /&gt;
Expertiza is a robust, open-source web platform designed to empower educational environments that emphasize project-based learning. It streamlines collaborative activities including peer reviews, surveys, and team assignments — giving instructors tools to manage assignments and track performance, and giving students a single place to submit work, review peers, and track feedback.&lt;br /&gt;
&lt;br /&gt;
== Introduction ==&lt;br /&gt;
''This page documents the reimplementation of the Student Task view. It supersedes the earlier [[E2602]] team's version. All searches for E2602 will hit here.''&lt;br /&gt;
This iteration of E2602 hardens and extends the Student Task View: consolidating the student dashboard and detail pages into single-call REST endpoints, eliminating N+1 queries in the peer-review score heatgrid, and fixing several display/permission bugs surfaced by manual QA against real assignment data.&lt;br /&gt;
&lt;br /&gt;
=== Tech Stack ===&lt;br /&gt;
* '''Backend:''' Ruby on Rails (API-only)&lt;br /&gt;
* '''Frontend:''' React + TypeScript&lt;br /&gt;
* '''Database:''' MySQL&lt;br /&gt;
* '''Backend Testing:''' RSpec&lt;br /&gt;
* '''Frontend Testing:''' Jest, React Testing Library&lt;br /&gt;
&lt;br /&gt;
=== Key Improvements ===&lt;br /&gt;
* Eliminated N+1 queries in the score heatgrid (&amp;lt;code&amp;gt;get_team_scores&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_my_scores_data&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_answer&amp;lt;/code&amp;gt;) via &amp;lt;code&amp;gt;.includes(responses: { scores: :item })&amp;lt;/code&amp;gt; and in-memory filtering instead of re-querying preloaded associations&lt;br /&gt;
* Converted &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt;'s stylesheet to CSS Modules, fixing a Bootstrap class collision and scoping color classes (&amp;lt;code&amp;gt;c1&amp;lt;/code&amp;gt;–&amp;lt;code&amp;gt;c5&amp;lt;/code&amp;gt;) correctly&lt;br /&gt;
* Redesigned the score/feedback heatgrid with sticky Question column during horizontal scroll&lt;br /&gt;
* Added hover tooltips on score cells showing the reviewer's written feedback&lt;br /&gt;
* Rewrote &amp;lt;code&amp;gt;StudentTasks&amp;lt;/code&amp;gt; from a (mis-implemented) topic sign-up sheet into a proper course-grouped assignment dashboard&lt;br /&gt;
* Added participant-level &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; permission checks, hiding &amp;quot;Your work&amp;quot; / &amp;quot;Others' work&amp;quot; task links accordingly&lt;br /&gt;
* Refactored review grade calculation: &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; now weights each round by its &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;; &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; centralizes the weighted-average logic previously duplicated across &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Problem Statement ==&lt;br /&gt;
The previous Student Task View had several concrete defects:&lt;br /&gt;
* The score heatgrid fired one SQL query per response/score across all reviewers and rounds, producing hundreds of queries for a single page load&lt;br /&gt;
* SectionHeader rubric rows (non-scorable dividers) were incorrectly counted in the &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; column and lost their fixed position during horizontal scroll&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTasks.tsx&amp;lt;/code&amp;gt; was previously implemented as a topic sign-up sheet, not a task dashboard, and required a full rewrite&lt;br /&gt;
* The page did not respect participant-level &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; flags — all task links were always shown regardless of permission&lt;br /&gt;
* Review grade calculation treated every reviewer and every round equally, ignoring each rubric's configured &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== User Stories ==&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want to see all my assignments in one place ===&lt;br /&gt;
&lt;br /&gt;
This is the home view for a student. It shows all of the student's assignments, arranged by the course they pertain to.&lt;br /&gt;
&lt;br /&gt;
[[File:student_view_assignments_page.png|800px|thumb|center|Student View Assignments Page]]&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; returns all tasks for the logged-in student in one call&lt;br /&gt;
* Tasks are grouped by course on the dashboard&lt;br /&gt;
* Each task shows current stage and days remaining, if applicable&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want to view a single assignment's deadlines and details ===&lt;br /&gt;
&lt;br /&gt;
A student can perform several tasks for each assignment, depending upon the role they play (e.g., whether they are a submitter, a reviewer, or both.&lt;br /&gt;
&lt;br /&gt;
[[File:student_view_assignment_detail_page.png|800px|thumb|center|Student View Assignment Detail Page]]&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt; returns the task plus a unified &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt; timeline&lt;br /&gt;
* The timeline visually distinguishes completed, current, and pending stages&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/team&amp;lt;/code&amp;gt; returns teammates grouped by course&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want my team's score heatgrid to load fast and stay readable while scrolling ===&lt;br /&gt;
&lt;br /&gt;
The heatgrid shows all of the reviews that have been written of the team's submission (or, the student's submission, if this is not a team assignment).  The user can hover over any cell with a score that is underlined, meaning that the reviewer has made a comment to go along with the score.  Hovering over a cell will show the comments it contains.&lt;br /&gt;
&lt;br /&gt;
[[File:student_view_assignment_grades_page.png|800px|thumb|left|Student View Assignment View Team Grades]]&lt;br /&gt;
&lt;br /&gt;
Clicking on &amp;quot;Feedback&amp;quot; will show all of the feedback along with the scores.&lt;br /&gt;
[[File:student_view_assignment_feedback_page.png|800px|thumb|right|Student View Assignment View Feedbacks]]&lt;br /&gt;
&amp;lt;br clear=&amp;quot;all&amp;quot; /&amp;gt;&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* The heatgrid loads from a single &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt; call&lt;br /&gt;
* The first two columns (item number &amp;quot;&amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;&amp;quot; and Item) remain fixed during horizontal scroll, along with section headers&lt;br /&gt;
* Hovering over a score with a comment shows the corresponding reviewer's feedback as a tooltip&lt;br /&gt;
&lt;br /&gt;
=== A student who is not permitted to submit or review will not see those task links ===&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;quot;Your work&amp;quot; is hidden when &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;quot;Others' work&amp;quot; is hidden when &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== As an instructor, I want calculated review grades to reflect each rubric's configured weight ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; weights each round's normalized score by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt; both compute their averages through the same shared &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
=== Student Task Overview ===&lt;br /&gt;
A StudentTask is a task that a student performs in the course of doing an assignment, e.g., forming a team, choosing a topic, submitting work, or reviewing work. No dedicated &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; database table exists. A &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; is a plain Ruby object composed from &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; and its associated &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignedUpTeam&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;ProjectTopic&amp;lt;/code&amp;gt;, and permission fields (&amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;permission_granted&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Request / Response Flow ====&lt;br /&gt;
* Student navigates to &amp;lt;code&amp;gt;/student_tasks&amp;lt;/code&amp;gt; → frontend calls &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTask.tasks(user)&amp;lt;/code&amp;gt; preloads &amp;lt;code&amp;gt;assignment: :course&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;:user&amp;lt;/code&amp;gt; associations, then maps each &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; through &amp;lt;code&amp;gt;StudentTask.create_from_participant&amp;lt;/code&amp;gt;&lt;br /&gt;
* Selecting an assignment navigates to the detail route → frontend calls &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTask.timeline_events&amp;lt;/code&amp;gt; merges the assignment's due dates with timestamps of reviews done of the logged-in user's work into one sorted timeline&lt;br /&gt;
* For the heatgrid: frontend calls &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt; → &amp;lt;code&amp;gt;GradesController#get_team_scores&amp;lt;/code&amp;gt; eager-loads &amp;lt;code&amp;gt;responses: { scores: :item }&amp;lt;/code&amp;gt; and calls &amp;lt;code&amp;gt;insert_section_headers&amp;lt;/code&amp;gt; to splice section-header marker objects into the response before it is returned to the frontend&lt;br /&gt;
&lt;br /&gt;
=== Aggregation of Scores Assigned by a Reviewer ===&lt;br /&gt;
* &amp;lt;code&amp;gt;Response#aggregate_questionnaire_score&amp;lt;/code&amp;gt; - item-weighted score for one submitted response&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; — normalizes a map's score per round and weights it by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score(maps)&amp;lt;/code&amp;gt; — weighted average across multiple maps, the single implementation shared by &amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Backend Implementation ==&lt;br /&gt;
&lt;br /&gt;
=== StudentTasksController ===&lt;br /&gt;
* &amp;lt;code&amp;gt;list&amp;lt;/code&amp;gt; — all &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; objects for the logged-in user&lt;br /&gt;
* &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt; — single task by participant ID with the unified timeline&lt;br /&gt;
* &amp;lt;code&amp;gt;team&amp;lt;/code&amp;gt; — teammates grouped by course name (&amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
=== GradesController — N+1 elimination ===&lt;br /&gt;
&amp;lt;code&amp;gt;get_team_scores&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;get_my_scores_data&amp;lt;/code&amp;gt; eager-load &amp;lt;code&amp;gt;responses: { scores: :item }&amp;lt;/code&amp;gt; on the &amp;lt;code&amp;gt;ReviewResponseMap&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;TeammateReviewResponseMap&amp;lt;/code&amp;gt; queries. &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt; filters SectionHeader scores via Ruby &amp;lt;code&amp;gt;.reject&amp;lt;/code&amp;gt; instead of &amp;lt;code&amp;gt;.joins&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;.where&amp;lt;/code&amp;gt; — calling &amp;lt;code&amp;gt;.where&amp;lt;/code&amp;gt; on an already-preloaded association silently bypasses the cache and re-queries the database. &amp;lt;code&amp;gt;get_answer&amp;lt;/code&amp;gt; now accepts the parent &amp;lt;code&amp;gt;response&amp;lt;/code&amp;gt; directly instead of re-fetching it via &amp;lt;code&amp;gt;score.response&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== insert_section_headers ===&lt;br /&gt;
Inserts &amp;lt;code&amp;gt;{ type: &amp;quot;header&amp;quot;, txt: &amp;quot;...&amp;quot; }&amp;lt;/code&amp;gt; marker objects into the scores array at positions derived from each &amp;lt;code&amp;gt;SectionHeader&amp;lt;/code&amp;gt; item's sequence order, so the frontend renders section heading rows between score rows without those rows affecting item numbering.&lt;br /&gt;
&lt;br /&gt;
=== Review Grade Calculation ===&lt;br /&gt;
For details on how peer review grades are computed, see [[Score Calculation]].&lt;br /&gt;
&lt;br /&gt;
=== Comprehensive Testing ===&lt;br /&gt;
* Model specs for &amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;create_from_participant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_events_for_assignment&amp;lt;/code&amp;gt;&lt;br /&gt;
* Request specs for &amp;lt;code&amp;gt;student_tasks/list&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;student_tasks/show/:id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;student_tasks/team&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;grades/:id/view_our_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
* Routing specs verifying the above routes resolve correctly&lt;br /&gt;
&lt;br /&gt;
== Frontend Implementation ==&lt;br /&gt;
=== New/Updated Files ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! File !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTasks.tsx&amp;lt;/code&amp;gt; || Course-grouped assignment dashboard (rewritten from a topic sign-up sheet)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTasksList.tsx&amp;lt;/code&amp;gt; || Sidebar — not-started tasks, revisions, teammates by course&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTaskDetail.tsx&amp;lt;/code&amp;gt; || Per-assignment detail page — timeline, task links, conditional &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; rendering&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ViewTeamGrades.module.scss&amp;lt;/code&amp;gt; || CSS Modules conversion; scoped &amp;lt;code&amp;gt;c1&amp;lt;/code&amp;gt;–&amp;lt;code&amp;gt;c5&amp;lt;/code&amp;gt; color classes, sticky-column and tooltip styles&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewTable.tsx&amp;lt;/code&amp;gt; || Single-call heatgrid; Scores/Feedback toggle; sticky SectionHeader rows&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewTableRow.tsx&amp;lt;/code&amp;gt; || Sticky &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;/Question columns; reduced row height; hover tooltip via &amp;lt;code&amp;gt;data-question&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;FeedbackTable.tsx&amp;lt;/code&amp;gt; || Feedback-mode table mirroring &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt;'s sticky-column layout&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;heatgridUtils.ts&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;RoundRow&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;isHeader()&amp;lt;/code&amp;gt; types; fixed &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; numbering to skip SectionHeader sentinels&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== StudentTasks (List View) ===&lt;br /&gt;
* Single &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; call, grouped by course&lt;br /&gt;
* Fixed: &amp;quot;Assignments&amp;quot; heading was rendering as body text because its class was referenced as a plain string instead of the CSS Modules export (&amp;lt;code&amp;gt;styles['assignments-title']&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
=== StudentTaskDetail (Detail View) ===&lt;br /&gt;
* Visual timeline with progress line and stage nodes (completed/current/pending)&lt;br /&gt;
* Task links list — &amp;quot;Your work&amp;quot; and &amp;quot;Others' work&amp;quot; conditionally rendered based on &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;quot;Send Email To Reviewers&amp;quot; button repositioned to align with the task list rather than floating disconnected at the page top&lt;br /&gt;
&lt;br /&gt;
=== ViewTeamGrades (Heatgrid) ===&lt;br /&gt;
* Consolidated ~6 sequential API calls into one &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
* SectionHeader rows split into a sticky label cell + scrolling spacer cell so they stay fixed during horizontal scroll&lt;br /&gt;
* Score cells show reviewer feedback as a hover tooltip&lt;br /&gt;
* Round headings simplified from &amp;quot;Review (Round: 1 of 2)&amp;quot; to &amp;quot;Round 1&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Design Principles ==&lt;br /&gt;
&lt;br /&gt;
=== Single Responsibility ===&lt;br /&gt;
Score aggregation is layered: &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; owns item-level scoring, &amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt; owns per-map/per-round weighting, &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; owns cross-map averaging — each level only knows about the one below it.&lt;br /&gt;
&lt;br /&gt;
=== Don't Repeat Yourself ===&lt;br /&gt;
The weighted-average logic that was duplicated between &amp;lt;code&amp;gt;AssignmentTeam#aggregate_review_grade&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;ReviewAggregator#compute_average_review_score&amp;lt;/code&amp;gt; was consolidated into a single &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt;, and the now-dead &amp;lt;code&amp;gt;ReviewAggregator&amp;lt;/code&amp;gt; concern was deleted.&lt;br /&gt;
&lt;br /&gt;
=== Performance by Design ===&lt;br /&gt;
Eager loading (&amp;lt;code&amp;gt;.includes&amp;lt;/code&amp;gt;) and in-memory filtering (&amp;lt;code&amp;gt;.reject&amp;lt;/code&amp;gt;) were chosen deliberately over additional database round-trips after profiling showed the heatgrid endpoint firing hundreds of queries under the old implementation.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
&lt;br /&gt;
=== Backend Tests ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! # !! Test !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| 1 || &amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt; groups teammates by course || Returns &amp;lt;code&amp;gt;{course_name =&amp;gt; [fullnames]}&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 2 || &amp;lt;code&amp;gt;all_teammates&amp;lt;/code&amp;gt; excludes calibrated assignments || Calibrated-assignment teammates excluded&lt;br /&gt;
|-&lt;br /&gt;
| 3 || &amp;lt;code&amp;gt;all_teammates&amp;lt;/code&amp;gt; excludes the user themself || Solo team returns empty hash&lt;br /&gt;
|-&lt;br /&gt;
| 4 || &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; returns all tasks for user || 200 OK with task array&lt;br /&gt;
|-&lt;br /&gt;
| 5 || &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt; returns due_dates timeline || 200 OK with &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt; array&lt;br /&gt;
|-&lt;br /&gt;
| 6 || &amp;lt;code&amp;gt;GET /student_tasks/team&amp;lt;/code&amp;gt; groups teammates by course || 200 OK with grouped hash&lt;br /&gt;
|-&lt;br /&gt;
| 7 || Heatgrid &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; column excludes SectionHeaders from numbering || Sequential numbering skips header rows&lt;br /&gt;
|-&lt;br /&gt;
| 8 || &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt; filters SectionHeaders without re-querying || No additional SQL fired against preloaded association&lt;br /&gt;
|-&lt;br /&gt;
| 9 || &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; weight validation || &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt; must be 0 when rubric has no scored questions&lt;br /&gt;
|-&lt;br /&gt;
| 10 || &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; || Weighted average matches Σ(grade × weight) / Σ(weight)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Frontend Tests ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! # !! Test !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| 1 || &amp;lt;code&amp;gt;StudentTasks&amp;lt;/code&amp;gt; renders &amp;quot;Assignments&amp;quot; heading with correct styling || Heading uses module class, renders bold and large&lt;br /&gt;
|-&lt;br /&gt;
| 2 || &amp;lt;code&amp;gt;StudentTaskDetail&amp;lt;/code&amp;gt; hides &amp;quot;Your work&amp;quot; when &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; is false || List item not rendered&lt;br /&gt;
|-&lt;br /&gt;
| 3 || &amp;lt;code&amp;gt;StudentTaskDetail&amp;lt;/code&amp;gt; hides &amp;quot;Others' work&amp;quot; when &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; is false || List item not rendered&lt;br /&gt;
|-&lt;br /&gt;
| 4 || &amp;lt;code&amp;gt;heatgridUtils&amp;lt;/code&amp;gt; itemNumber skips SectionHeader rows || Numbering matches scored items only&lt;br /&gt;
|-&lt;br /&gt;
| 5 || &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt; SectionHeader row stays fixed during horizontal scroll || Header cell &amp;lt;code&amp;gt;position: sticky&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 6 || &amp;lt;code&amp;gt;ReviewTableRow&amp;lt;/code&amp;gt; shows tooltip only when a comment exists || No tooltip box when &amp;lt;code&amp;gt;data-question&amp;lt;/code&amp;gt; is undefined&lt;br /&gt;
|-&lt;br /&gt;
| 7 || Heatgrid column borders render as &amp;lt;code&amp;gt;#ddd&amp;lt;/code&amp;gt; || No black borders after &amp;lt;code&amp;gt;tbl_heat&amp;lt;/code&amp;gt; class applied&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Relevant Links ==&lt;br /&gt;
&lt;br /&gt;
=== Repos ===&lt;br /&gt;
* https://github.com/expertiza/reimplementation-back-end&lt;br /&gt;
* https://github.com/expertiza/reimplementation-front-end&lt;br /&gt;
&lt;br /&gt;
=== Pull Requests ===&lt;br /&gt;
* Backend: https://github.com/expertiza/reimplementation-back-end/pull/352&lt;br /&gt;
* Frontend: https://github.com/expertiza/reimplementation-front-end/pull/182&lt;br /&gt;
&lt;br /&gt;
=== Demo Video ===&lt;br /&gt;
* (add link once recorded)&lt;br /&gt;
&lt;br /&gt;
== Developer Guidance ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Setup &amp;amp; Run (via Docker) ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
docker compose up&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Within the container ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
bundle exec rspec spec/models/student_task_spec.rb&lt;br /&gt;
bundle exec rspec spec/requests/api/v1/student_tasks_controller_spec.rb&lt;br /&gt;
bundle exec rspec spec/requests/api/v1/grades_controller_spec.rb&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
==== Setup &amp;amp; Run ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm install&lt;br /&gt;
npm start&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Running Frontend Tests ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm test -- StudentTaskDetail   # run a single suite&lt;br /&gt;
npm test                        # run full suite&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Future Scope ==&lt;br /&gt;
&lt;br /&gt;
=== Reviewer reputation-weighted grading ===&lt;br /&gt;
Integrate an external review-quality grader (e.g. an LLM-based reviewer grader) so &amp;lt;code&amp;gt;ResponseMap.reviewer_reputation_for&amp;lt;/code&amp;gt; reflects actual review quality instead of the current &amp;lt;code&amp;gt;1.0&amp;lt;/code&amp;gt; placeholder for every reviewer.&lt;br /&gt;
&lt;br /&gt;
=== Email-the-authors integration ===&lt;br /&gt;
Wire &amp;quot;Send Email To Reviewers&amp;quot; to a real backend endpoint instead of the current placeholder route.&lt;br /&gt;
&lt;br /&gt;
=== Dedicated StudentTask abstraction ===&lt;br /&gt;
&amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; remains a composed, non-persisted object. A future dedicated table could centralize derived state and simplify queries if the feature's scope grows further.&lt;br /&gt;
&lt;br /&gt;
== Mentor ==&lt;br /&gt;
[mailto:kgudipe@ncsu.edu Koushik Gudipelly]&lt;br /&gt;
&lt;br /&gt;
== Members ==&lt;br /&gt;
[mailto:rsgopara@ncsu.edu Ravi Goparaju]&lt;br /&gt;
[mailto:xmi@ncsu.edu Xiangjun Mi]&lt;br /&gt;
&lt;br /&gt;
== Completed by ==&lt;br /&gt;
[mailto:blalu@ncsu.edu Bestin Lalu]&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2602._Reimplement_student_task_view&amp;diff=168248</id>
		<title>CSC/ECE 517 Spring 2026 - E2602. Reimplement student task view</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2602._Reimplement_student_task_view&amp;diff=168248"/>
		<updated>2026-07-07T00:14:42Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* As a student, I want my team's score heatgrid to load fast and stay readable while scrolling */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= CSC/ECE 517 Spring 2026 - E2602. Reimplement Student Task View =&lt;br /&gt;
&lt;br /&gt;
== About Expertiza ==&lt;br /&gt;
Expertiza is a robust, open-source web platform designed to empower educational environments that emphasize project-based learning. It streamlines collaborative activities including peer reviews, surveys, and team assignments — giving instructors tools to manage assignments and track performance, and giving students a single place to submit work, review peers, and track feedback.&lt;br /&gt;
&lt;br /&gt;
== Introduction ==&lt;br /&gt;
''This page documents the reimplementation of the Student Task view. It supersedes the earlier [[E2602]] team's version. All searches for E2602 will hit here.''&lt;br /&gt;
This iteration of E2602 hardens and extends the Student Task View: consolidating the student dashboard and detail pages into single-call REST endpoints, eliminating N+1 queries in the peer-review score heatgrid, and fixing several display/permission bugs surfaced by manual QA against real assignment data.&lt;br /&gt;
&lt;br /&gt;
=== Tech Stack ===&lt;br /&gt;
* '''Backend:''' Ruby on Rails (API-only)&lt;br /&gt;
* '''Frontend:''' React + TypeScript&lt;br /&gt;
* '''Database:''' MySQL&lt;br /&gt;
* '''Backend Testing:''' RSpec&lt;br /&gt;
* '''Frontend Testing:''' Jest, React Testing Library&lt;br /&gt;
&lt;br /&gt;
=== Key Improvements ===&lt;br /&gt;
* Eliminated N+1 queries in the score heatgrid (&amp;lt;code&amp;gt;get_team_scores&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_my_scores_data&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_answer&amp;lt;/code&amp;gt;) via &amp;lt;code&amp;gt;.includes(responses: { scores: :item })&amp;lt;/code&amp;gt; and in-memory filtering instead of re-querying preloaded associations&lt;br /&gt;
* Converted &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt;'s stylesheet to CSS Modules, fixing a Bootstrap class collision and scoping color classes (&amp;lt;code&amp;gt;c1&amp;lt;/code&amp;gt;–&amp;lt;code&amp;gt;c5&amp;lt;/code&amp;gt;) correctly&lt;br /&gt;
* Redesigned the score/feedback heatgrid with sticky Question column during horizontal scroll&lt;br /&gt;
* Added hover tooltips on score cells showing the reviewer's written feedback&lt;br /&gt;
* Rewrote &amp;lt;code&amp;gt;StudentTasks&amp;lt;/code&amp;gt; from a (mis-implemented) topic sign-up sheet into a proper course-grouped assignment dashboard&lt;br /&gt;
* Added participant-level &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; permission checks, hiding &amp;quot;Your work&amp;quot; / &amp;quot;Others' work&amp;quot; task links accordingly&lt;br /&gt;
* Refactored review grade calculation: &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; now weights each round by its &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;; &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; centralizes the weighted-average logic previously duplicated across &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Problem Statement ==&lt;br /&gt;
The previous Student Task View had several concrete defects:&lt;br /&gt;
* The score heatgrid fired one SQL query per response/score across all reviewers and rounds, producing hundreds of queries for a single page load&lt;br /&gt;
* SectionHeader rubric rows (non-scorable dividers) were incorrectly counted in the &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; column and lost their fixed position during horizontal scroll&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTasks.tsx&amp;lt;/code&amp;gt; was previously implemented as a topic sign-up sheet, not a task dashboard, and required a full rewrite&lt;br /&gt;
* The page did not respect participant-level &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; flags — all task links were always shown regardless of permission&lt;br /&gt;
* Review grade calculation treated every reviewer and every round equally, ignoring each rubric's configured &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== User Stories ==&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want to see all my assignments in one place ===&lt;br /&gt;
&lt;br /&gt;
This is the home view for a student. It shows all of the student's assignments, arranged by the course they pertain to.&lt;br /&gt;
&lt;br /&gt;
[[File:student_view_assignments_page.png|800px|thumb|center|Student View Assignments Page]]&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; returns all tasks for the logged-in student in one call&lt;br /&gt;
* Tasks are grouped by course on the dashboard&lt;br /&gt;
* Each task shows current stage and days remaining, if applicable&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want to view a single assignment's deadlines and details ===&lt;br /&gt;
&lt;br /&gt;
A student can perform several tasks for each assignment, depending upon the role they play (e.g., whether they are a submitter, a reviewer, or both.&lt;br /&gt;
&lt;br /&gt;
[[File:student_view_assignment_detail_page.png|800px|thumb|center|Student View Assignment Detail Page]]&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt; returns the task plus a unified &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt; timeline&lt;br /&gt;
* The timeline visually distinguishes completed, current, and pending stages&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/team&amp;lt;/code&amp;gt; returns teammates grouped by course&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want my team's score heatgrid to load fast and stay readable while scrolling ===&lt;br /&gt;
&lt;br /&gt;
The heatgrid shows all of the reviews that have been written of the team's submission (or, the student's submission, if this is not a team assignment).  The user can hover over any cell with a score that is underlined, meaning that the reviewer has made a comment to go along with the score.  Hovering over a cell will show the comments it contains.&lt;br /&gt;
&lt;br /&gt;
[[File:student_view_assignment_grades_page.png|800px|thumb|left|Student View Assignment View Team Grades]]&lt;br /&gt;
[[File:student_view_assignment_feedback_page.png|800px|thumb|right|Student View Assignment View Feedbacks]]&lt;br /&gt;
&amp;lt;br clear=&amp;quot;all&amp;quot; /&amp;gt;&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* The heatgrid loads from a single &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt; call&lt;br /&gt;
* The first two columns (item number &amp;quot;&amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;&amp;quot; and Item) remain fixed during horizontal scroll, along with section headers&lt;br /&gt;
* Hovering over a score with a comment shows the corresponding reviewer's feedback as a tooltip&lt;br /&gt;
&lt;br /&gt;
=== A student who is not permitted to submit or review will not see those task links ===&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;quot;Your work&amp;quot; is hidden when &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;quot;Others' work&amp;quot; is hidden when &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== As an instructor, I want calculated review grades to reflect each rubric's configured weight ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; weights each round's normalized score by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt; both compute their averages through the same shared &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
=== Student Task Overview ===&lt;br /&gt;
A StudentTask is a task that a student performs in the course of doing an assignment, e.g., forming a team, choosing a topic, submitting work, or reviewing work. No dedicated &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; database table exists. A &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; is a plain Ruby object composed from &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; and its associated &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignedUpTeam&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;ProjectTopic&amp;lt;/code&amp;gt;, and permission fields (&amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;permission_granted&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Request / Response Flow ====&lt;br /&gt;
* Student navigates to &amp;lt;code&amp;gt;/student_tasks&amp;lt;/code&amp;gt; → frontend calls &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTask.tasks(user)&amp;lt;/code&amp;gt; preloads &amp;lt;code&amp;gt;assignment: :course&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;:user&amp;lt;/code&amp;gt; associations, then maps each &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; through &amp;lt;code&amp;gt;StudentTask.create_from_participant&amp;lt;/code&amp;gt;&lt;br /&gt;
* Selecting an assignment navigates to the detail route → frontend calls &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTask.timeline_events&amp;lt;/code&amp;gt; merges the assignment's due dates with timestamps of reviews done of the logged-in user's work into one sorted timeline&lt;br /&gt;
* For the heatgrid: frontend calls &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt; → &amp;lt;code&amp;gt;GradesController#get_team_scores&amp;lt;/code&amp;gt; eager-loads &amp;lt;code&amp;gt;responses: { scores: :item }&amp;lt;/code&amp;gt; and calls &amp;lt;code&amp;gt;insert_section_headers&amp;lt;/code&amp;gt; to splice section-header marker objects into the response before it is returned to the frontend&lt;br /&gt;
&lt;br /&gt;
=== Aggregation of Scores Assigned by a Reviewer ===&lt;br /&gt;
* &amp;lt;code&amp;gt;Response#aggregate_questionnaire_score&amp;lt;/code&amp;gt; - item-weighted score for one submitted response&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; — normalizes a map's score per round and weights it by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score(maps)&amp;lt;/code&amp;gt; — weighted average across multiple maps, the single implementation shared by &amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Backend Implementation ==&lt;br /&gt;
&lt;br /&gt;
=== StudentTasksController ===&lt;br /&gt;
* &amp;lt;code&amp;gt;list&amp;lt;/code&amp;gt; — all &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; objects for the logged-in user&lt;br /&gt;
* &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt; — single task by participant ID with the unified timeline&lt;br /&gt;
* &amp;lt;code&amp;gt;team&amp;lt;/code&amp;gt; — teammates grouped by course name (&amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
=== GradesController — N+1 elimination ===&lt;br /&gt;
&amp;lt;code&amp;gt;get_team_scores&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;get_my_scores_data&amp;lt;/code&amp;gt; eager-load &amp;lt;code&amp;gt;responses: { scores: :item }&amp;lt;/code&amp;gt; on the &amp;lt;code&amp;gt;ReviewResponseMap&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;TeammateReviewResponseMap&amp;lt;/code&amp;gt; queries. &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt; filters SectionHeader scores via Ruby &amp;lt;code&amp;gt;.reject&amp;lt;/code&amp;gt; instead of &amp;lt;code&amp;gt;.joins&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;.where&amp;lt;/code&amp;gt; — calling &amp;lt;code&amp;gt;.where&amp;lt;/code&amp;gt; on an already-preloaded association silently bypasses the cache and re-queries the database. &amp;lt;code&amp;gt;get_answer&amp;lt;/code&amp;gt; now accepts the parent &amp;lt;code&amp;gt;response&amp;lt;/code&amp;gt; directly instead of re-fetching it via &amp;lt;code&amp;gt;score.response&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== insert_section_headers ===&lt;br /&gt;
Inserts &amp;lt;code&amp;gt;{ type: &amp;quot;header&amp;quot;, txt: &amp;quot;...&amp;quot; }&amp;lt;/code&amp;gt; marker objects into the scores array at positions derived from each &amp;lt;code&amp;gt;SectionHeader&amp;lt;/code&amp;gt; item's sequence order, so the frontend renders section heading rows between score rows without those rows affecting item numbering.&lt;br /&gt;
&lt;br /&gt;
=== Review Grade Calculation ===&lt;br /&gt;
For details on how peer review grades are computed, see [[Score Calculation]].&lt;br /&gt;
&lt;br /&gt;
=== Comprehensive Testing ===&lt;br /&gt;
* Model specs for &amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;create_from_participant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_events_for_assignment&amp;lt;/code&amp;gt;&lt;br /&gt;
* Request specs for &amp;lt;code&amp;gt;student_tasks/list&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;student_tasks/show/:id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;student_tasks/team&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;grades/:id/view_our_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
* Routing specs verifying the above routes resolve correctly&lt;br /&gt;
&lt;br /&gt;
== Frontend Implementation ==&lt;br /&gt;
=== New/Updated Files ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! File !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTasks.tsx&amp;lt;/code&amp;gt; || Course-grouped assignment dashboard (rewritten from a topic sign-up sheet)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTasksList.tsx&amp;lt;/code&amp;gt; || Sidebar — not-started tasks, revisions, teammates by course&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTaskDetail.tsx&amp;lt;/code&amp;gt; || Per-assignment detail page — timeline, task links, conditional &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; rendering&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ViewTeamGrades.module.scss&amp;lt;/code&amp;gt; || CSS Modules conversion; scoped &amp;lt;code&amp;gt;c1&amp;lt;/code&amp;gt;–&amp;lt;code&amp;gt;c5&amp;lt;/code&amp;gt; color classes, sticky-column and tooltip styles&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewTable.tsx&amp;lt;/code&amp;gt; || Single-call heatgrid; Scores/Feedback toggle; sticky SectionHeader rows&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewTableRow.tsx&amp;lt;/code&amp;gt; || Sticky &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;/Question columns; reduced row height; hover tooltip via &amp;lt;code&amp;gt;data-question&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;FeedbackTable.tsx&amp;lt;/code&amp;gt; || Feedback-mode table mirroring &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt;'s sticky-column layout&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;heatgridUtils.ts&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;RoundRow&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;isHeader()&amp;lt;/code&amp;gt; types; fixed &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; numbering to skip SectionHeader sentinels&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== StudentTasks (List View) ===&lt;br /&gt;
* Single &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; call, grouped by course&lt;br /&gt;
* Fixed: &amp;quot;Assignments&amp;quot; heading was rendering as body text because its class was referenced as a plain string instead of the CSS Modules export (&amp;lt;code&amp;gt;styles['assignments-title']&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
=== StudentTaskDetail (Detail View) ===&lt;br /&gt;
* Visual timeline with progress line and stage nodes (completed/current/pending)&lt;br /&gt;
* Task links list — &amp;quot;Your work&amp;quot; and &amp;quot;Others' work&amp;quot; conditionally rendered based on &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;quot;Send Email To Reviewers&amp;quot; button repositioned to align with the task list rather than floating disconnected at the page top&lt;br /&gt;
&lt;br /&gt;
=== ViewTeamGrades (Heatgrid) ===&lt;br /&gt;
* Consolidated ~6 sequential API calls into one &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
* SectionHeader rows split into a sticky label cell + scrolling spacer cell so they stay fixed during horizontal scroll&lt;br /&gt;
* Score cells show reviewer feedback as a hover tooltip&lt;br /&gt;
* Round headings simplified from &amp;quot;Review (Round: 1 of 2)&amp;quot; to &amp;quot;Round 1&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Design Principles ==&lt;br /&gt;
&lt;br /&gt;
=== Single Responsibility ===&lt;br /&gt;
Score aggregation is layered: &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; owns item-level scoring, &amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt; owns per-map/per-round weighting, &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; owns cross-map averaging — each level only knows about the one below it.&lt;br /&gt;
&lt;br /&gt;
=== Don't Repeat Yourself ===&lt;br /&gt;
The weighted-average logic that was duplicated between &amp;lt;code&amp;gt;AssignmentTeam#aggregate_review_grade&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;ReviewAggregator#compute_average_review_score&amp;lt;/code&amp;gt; was consolidated into a single &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt;, and the now-dead &amp;lt;code&amp;gt;ReviewAggregator&amp;lt;/code&amp;gt; concern was deleted.&lt;br /&gt;
&lt;br /&gt;
=== Performance by Design ===&lt;br /&gt;
Eager loading (&amp;lt;code&amp;gt;.includes&amp;lt;/code&amp;gt;) and in-memory filtering (&amp;lt;code&amp;gt;.reject&amp;lt;/code&amp;gt;) were chosen deliberately over additional database round-trips after profiling showed the heatgrid endpoint firing hundreds of queries under the old implementation.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
&lt;br /&gt;
=== Backend Tests ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! # !! Test !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| 1 || &amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt; groups teammates by course || Returns &amp;lt;code&amp;gt;{course_name =&amp;gt; [fullnames]}&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 2 || &amp;lt;code&amp;gt;all_teammates&amp;lt;/code&amp;gt; excludes calibrated assignments || Calibrated-assignment teammates excluded&lt;br /&gt;
|-&lt;br /&gt;
| 3 || &amp;lt;code&amp;gt;all_teammates&amp;lt;/code&amp;gt; excludes the user themself || Solo team returns empty hash&lt;br /&gt;
|-&lt;br /&gt;
| 4 || &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; returns all tasks for user || 200 OK with task array&lt;br /&gt;
|-&lt;br /&gt;
| 5 || &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt; returns due_dates timeline || 200 OK with &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt; array&lt;br /&gt;
|-&lt;br /&gt;
| 6 || &amp;lt;code&amp;gt;GET /student_tasks/team&amp;lt;/code&amp;gt; groups teammates by course || 200 OK with grouped hash&lt;br /&gt;
|-&lt;br /&gt;
| 7 || Heatgrid &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; column excludes SectionHeaders from numbering || Sequential numbering skips header rows&lt;br /&gt;
|-&lt;br /&gt;
| 8 || &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt; filters SectionHeaders without re-querying || No additional SQL fired against preloaded association&lt;br /&gt;
|-&lt;br /&gt;
| 9 || &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; weight validation || &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt; must be 0 when rubric has no scored questions&lt;br /&gt;
|-&lt;br /&gt;
| 10 || &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; || Weighted average matches Σ(grade × weight) / Σ(weight)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Frontend Tests ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! # !! Test !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| 1 || &amp;lt;code&amp;gt;StudentTasks&amp;lt;/code&amp;gt; renders &amp;quot;Assignments&amp;quot; heading with correct styling || Heading uses module class, renders bold and large&lt;br /&gt;
|-&lt;br /&gt;
| 2 || &amp;lt;code&amp;gt;StudentTaskDetail&amp;lt;/code&amp;gt; hides &amp;quot;Your work&amp;quot; when &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; is false || List item not rendered&lt;br /&gt;
|-&lt;br /&gt;
| 3 || &amp;lt;code&amp;gt;StudentTaskDetail&amp;lt;/code&amp;gt; hides &amp;quot;Others' work&amp;quot; when &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; is false || List item not rendered&lt;br /&gt;
|-&lt;br /&gt;
| 4 || &amp;lt;code&amp;gt;heatgridUtils&amp;lt;/code&amp;gt; itemNumber skips SectionHeader rows || Numbering matches scored items only&lt;br /&gt;
|-&lt;br /&gt;
| 5 || &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt; SectionHeader row stays fixed during horizontal scroll || Header cell &amp;lt;code&amp;gt;position: sticky&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 6 || &amp;lt;code&amp;gt;ReviewTableRow&amp;lt;/code&amp;gt; shows tooltip only when a comment exists || No tooltip box when &amp;lt;code&amp;gt;data-question&amp;lt;/code&amp;gt; is undefined&lt;br /&gt;
|-&lt;br /&gt;
| 7 || Heatgrid column borders render as &amp;lt;code&amp;gt;#ddd&amp;lt;/code&amp;gt; || No black borders after &amp;lt;code&amp;gt;tbl_heat&amp;lt;/code&amp;gt; class applied&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Relevant Links ==&lt;br /&gt;
&lt;br /&gt;
=== Repos ===&lt;br /&gt;
* https://github.com/expertiza/reimplementation-back-end&lt;br /&gt;
* https://github.com/expertiza/reimplementation-front-end&lt;br /&gt;
&lt;br /&gt;
=== Pull Requests ===&lt;br /&gt;
* Backend: https://github.com/expertiza/reimplementation-back-end/pull/352&lt;br /&gt;
* Frontend: https://github.com/expertiza/reimplementation-front-end/pull/182&lt;br /&gt;
&lt;br /&gt;
=== Demo Video ===&lt;br /&gt;
* (add link once recorded)&lt;br /&gt;
&lt;br /&gt;
== Developer Guidance ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Setup &amp;amp; Run (via Docker) ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
docker compose up&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Within the container ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
bundle exec rspec spec/models/student_task_spec.rb&lt;br /&gt;
bundle exec rspec spec/requests/api/v1/student_tasks_controller_spec.rb&lt;br /&gt;
bundle exec rspec spec/requests/api/v1/grades_controller_spec.rb&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
==== Setup &amp;amp; Run ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm install&lt;br /&gt;
npm start&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Running Frontend Tests ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm test -- StudentTaskDetail   # run a single suite&lt;br /&gt;
npm test                        # run full suite&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Future Scope ==&lt;br /&gt;
&lt;br /&gt;
=== Reviewer reputation-weighted grading ===&lt;br /&gt;
Integrate an external review-quality grader (e.g. an LLM-based reviewer grader) so &amp;lt;code&amp;gt;ResponseMap.reviewer_reputation_for&amp;lt;/code&amp;gt; reflects actual review quality instead of the current &amp;lt;code&amp;gt;1.0&amp;lt;/code&amp;gt; placeholder for every reviewer.&lt;br /&gt;
&lt;br /&gt;
=== Email-the-authors integration ===&lt;br /&gt;
Wire &amp;quot;Send Email To Reviewers&amp;quot; to a real backend endpoint instead of the current placeholder route.&lt;br /&gt;
&lt;br /&gt;
=== Dedicated StudentTask abstraction ===&lt;br /&gt;
&amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; remains a composed, non-persisted object. A future dedicated table could centralize derived state and simplify queries if the feature's scope grows further.&lt;br /&gt;
&lt;br /&gt;
== Mentor ==&lt;br /&gt;
[mailto:kgudipe@ncsu.edu Koushik Gudipelly]&lt;br /&gt;
&lt;br /&gt;
== Members ==&lt;br /&gt;
[mailto:rsgopara@ncsu.edu Ravi Goparaju]&lt;br /&gt;
[mailto:xmi@ncsu.edu Xiangjun Mi]&lt;br /&gt;
&lt;br /&gt;
== Completed by ==&lt;br /&gt;
[mailto:blalu@ncsu.edu Bestin Lalu]&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2602._Reimplement_student_task_view&amp;diff=168247</id>
		<title>CSC/ECE 517 Spring 2026 - E2602. Reimplement student task view</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2602._Reimplement_student_task_view&amp;diff=168247"/>
		<updated>2026-07-07T00:11:57Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* As a student, I want to view a single assignment's deadlines and details */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= CSC/ECE 517 Spring 2026 - E2602. Reimplement Student Task View =&lt;br /&gt;
&lt;br /&gt;
== About Expertiza ==&lt;br /&gt;
Expertiza is a robust, open-source web platform designed to empower educational environments that emphasize project-based learning. It streamlines collaborative activities including peer reviews, surveys, and team assignments — giving instructors tools to manage assignments and track performance, and giving students a single place to submit work, review peers, and track feedback.&lt;br /&gt;
&lt;br /&gt;
== Introduction ==&lt;br /&gt;
''This page documents the reimplementation of the Student Task view. It supersedes the earlier [[E2602]] team's version. All searches for E2602 will hit here.''&lt;br /&gt;
This iteration of E2602 hardens and extends the Student Task View: consolidating the student dashboard and detail pages into single-call REST endpoints, eliminating N+1 queries in the peer-review score heatgrid, and fixing several display/permission bugs surfaced by manual QA against real assignment data.&lt;br /&gt;
&lt;br /&gt;
=== Tech Stack ===&lt;br /&gt;
* '''Backend:''' Ruby on Rails (API-only)&lt;br /&gt;
* '''Frontend:''' React + TypeScript&lt;br /&gt;
* '''Database:''' MySQL&lt;br /&gt;
* '''Backend Testing:''' RSpec&lt;br /&gt;
* '''Frontend Testing:''' Jest, React Testing Library&lt;br /&gt;
&lt;br /&gt;
=== Key Improvements ===&lt;br /&gt;
* Eliminated N+1 queries in the score heatgrid (&amp;lt;code&amp;gt;get_team_scores&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_my_scores_data&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_answer&amp;lt;/code&amp;gt;) via &amp;lt;code&amp;gt;.includes(responses: { scores: :item })&amp;lt;/code&amp;gt; and in-memory filtering instead of re-querying preloaded associations&lt;br /&gt;
* Converted &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt;'s stylesheet to CSS Modules, fixing a Bootstrap class collision and scoping color classes (&amp;lt;code&amp;gt;c1&amp;lt;/code&amp;gt;–&amp;lt;code&amp;gt;c5&amp;lt;/code&amp;gt;) correctly&lt;br /&gt;
* Redesigned the score/feedback heatgrid with sticky Question column during horizontal scroll&lt;br /&gt;
* Added hover tooltips on score cells showing the reviewer's written feedback&lt;br /&gt;
* Rewrote &amp;lt;code&amp;gt;StudentTasks&amp;lt;/code&amp;gt; from a (mis-implemented) topic sign-up sheet into a proper course-grouped assignment dashboard&lt;br /&gt;
* Added participant-level &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; permission checks, hiding &amp;quot;Your work&amp;quot; / &amp;quot;Others' work&amp;quot; task links accordingly&lt;br /&gt;
* Refactored review grade calculation: &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; now weights each round by its &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;; &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; centralizes the weighted-average logic previously duplicated across &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Problem Statement ==&lt;br /&gt;
The previous Student Task View had several concrete defects:&lt;br /&gt;
* The score heatgrid fired one SQL query per response/score across all reviewers and rounds, producing hundreds of queries for a single page load&lt;br /&gt;
* SectionHeader rubric rows (non-scorable dividers) were incorrectly counted in the &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; column and lost their fixed position during horizontal scroll&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTasks.tsx&amp;lt;/code&amp;gt; was previously implemented as a topic sign-up sheet, not a task dashboard, and required a full rewrite&lt;br /&gt;
* The page did not respect participant-level &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; flags — all task links were always shown regardless of permission&lt;br /&gt;
* Review grade calculation treated every reviewer and every round equally, ignoring each rubric's configured &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== User Stories ==&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want to see all my assignments in one place ===&lt;br /&gt;
&lt;br /&gt;
This is the home view for a student. It shows all of the student's assignments, arranged by the course they pertain to.&lt;br /&gt;
&lt;br /&gt;
[[File:student_view_assignments_page.png|800px|thumb|center|Student View Assignments Page]]&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; returns all tasks for the logged-in student in one call&lt;br /&gt;
* Tasks are grouped by course on the dashboard&lt;br /&gt;
* Each task shows current stage and days remaining, if applicable&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want to view a single assignment's deadlines and details ===&lt;br /&gt;
&lt;br /&gt;
A student can perform several tasks for each assignment, depending upon the role they play (e.g., whether they are a submitter, a reviewer, or both.&lt;br /&gt;
&lt;br /&gt;
[[File:student_view_assignment_detail_page.png|800px|thumb|center|Student View Assignment Detail Page]]&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt; returns the task plus a unified &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt; timeline&lt;br /&gt;
* The timeline visually distinguishes completed, current, and pending stages&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/team&amp;lt;/code&amp;gt; returns teammates grouped by course&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want my team's score heatgrid to load fast and stay readable while scrolling ===&lt;br /&gt;
&lt;br /&gt;
[[File:student_view_assignment_grades_page.png|800px|thumb|left|Student View Assignment View Team Grades]]&lt;br /&gt;
[[File:student_view_assignment_feedback_page.png|800px|thumb|right|Student View Assignment View Feedbacks]]&lt;br /&gt;
&amp;lt;br clear=&amp;quot;all&amp;quot; /&amp;gt;&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* The heatgrid loads from a single &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt; call&lt;br /&gt;
* The first two columns (item number &amp;quot;&amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;&amp;quot; and Item) remain fixed during horizontal scroll, along with section headers&lt;br /&gt;
* Hovering over a score with a comment shows the corresponding reviewer's feedback as a tooltip&lt;br /&gt;
&lt;br /&gt;
=== A student who is not permitted to submit or review will not see those task links ===&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;quot;Your work&amp;quot; is hidden when &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;quot;Others' work&amp;quot; is hidden when &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== As an instructor, I want calculated review grades to reflect each rubric's configured weight ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; weights each round's normalized score by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt; both compute their averages through the same shared &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
=== Student Task Overview ===&lt;br /&gt;
A StudentTask is a task that a student performs in the course of doing an assignment, e.g., forming a team, choosing a topic, submitting work, or reviewing work. No dedicated &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; database table exists. A &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; is a plain Ruby object composed from &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; and its associated &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignedUpTeam&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;ProjectTopic&amp;lt;/code&amp;gt;, and permission fields (&amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;permission_granted&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Request / Response Flow ====&lt;br /&gt;
* Student navigates to &amp;lt;code&amp;gt;/student_tasks&amp;lt;/code&amp;gt; → frontend calls &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTask.tasks(user)&amp;lt;/code&amp;gt; preloads &amp;lt;code&amp;gt;assignment: :course&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;:user&amp;lt;/code&amp;gt; associations, then maps each &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; through &amp;lt;code&amp;gt;StudentTask.create_from_participant&amp;lt;/code&amp;gt;&lt;br /&gt;
* Selecting an assignment navigates to the detail route → frontend calls &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTask.timeline_events&amp;lt;/code&amp;gt; merges the assignment's due dates with timestamps of reviews done of the logged-in user's work into one sorted timeline&lt;br /&gt;
* For the heatgrid: frontend calls &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt; → &amp;lt;code&amp;gt;GradesController#get_team_scores&amp;lt;/code&amp;gt; eager-loads &amp;lt;code&amp;gt;responses: { scores: :item }&amp;lt;/code&amp;gt; and calls &amp;lt;code&amp;gt;insert_section_headers&amp;lt;/code&amp;gt; to splice section-header marker objects into the response before it is returned to the frontend&lt;br /&gt;
&lt;br /&gt;
=== Aggregation of Scores Assigned by a Reviewer ===&lt;br /&gt;
* &amp;lt;code&amp;gt;Response#aggregate_questionnaire_score&amp;lt;/code&amp;gt; - item-weighted score for one submitted response&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; — normalizes a map's score per round and weights it by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score(maps)&amp;lt;/code&amp;gt; — weighted average across multiple maps, the single implementation shared by &amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Backend Implementation ==&lt;br /&gt;
&lt;br /&gt;
=== StudentTasksController ===&lt;br /&gt;
* &amp;lt;code&amp;gt;list&amp;lt;/code&amp;gt; — all &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; objects for the logged-in user&lt;br /&gt;
* &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt; — single task by participant ID with the unified timeline&lt;br /&gt;
* &amp;lt;code&amp;gt;team&amp;lt;/code&amp;gt; — teammates grouped by course name (&amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
=== GradesController — N+1 elimination ===&lt;br /&gt;
&amp;lt;code&amp;gt;get_team_scores&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;get_my_scores_data&amp;lt;/code&amp;gt; eager-load &amp;lt;code&amp;gt;responses: { scores: :item }&amp;lt;/code&amp;gt; on the &amp;lt;code&amp;gt;ReviewResponseMap&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;TeammateReviewResponseMap&amp;lt;/code&amp;gt; queries. &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt; filters SectionHeader scores via Ruby &amp;lt;code&amp;gt;.reject&amp;lt;/code&amp;gt; instead of &amp;lt;code&amp;gt;.joins&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;.where&amp;lt;/code&amp;gt; — calling &amp;lt;code&amp;gt;.where&amp;lt;/code&amp;gt; on an already-preloaded association silently bypasses the cache and re-queries the database. &amp;lt;code&amp;gt;get_answer&amp;lt;/code&amp;gt; now accepts the parent &amp;lt;code&amp;gt;response&amp;lt;/code&amp;gt; directly instead of re-fetching it via &amp;lt;code&amp;gt;score.response&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== insert_section_headers ===&lt;br /&gt;
Inserts &amp;lt;code&amp;gt;{ type: &amp;quot;header&amp;quot;, txt: &amp;quot;...&amp;quot; }&amp;lt;/code&amp;gt; marker objects into the scores array at positions derived from each &amp;lt;code&amp;gt;SectionHeader&amp;lt;/code&amp;gt; item's sequence order, so the frontend renders section heading rows between score rows without those rows affecting item numbering.&lt;br /&gt;
&lt;br /&gt;
=== Review Grade Calculation ===&lt;br /&gt;
For details on how peer review grades are computed, see [[Score Calculation]].&lt;br /&gt;
&lt;br /&gt;
=== Comprehensive Testing ===&lt;br /&gt;
* Model specs for &amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;create_from_participant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_events_for_assignment&amp;lt;/code&amp;gt;&lt;br /&gt;
* Request specs for &amp;lt;code&amp;gt;student_tasks/list&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;student_tasks/show/:id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;student_tasks/team&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;grades/:id/view_our_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
* Routing specs verifying the above routes resolve correctly&lt;br /&gt;
&lt;br /&gt;
== Frontend Implementation ==&lt;br /&gt;
=== New/Updated Files ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! File !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTasks.tsx&amp;lt;/code&amp;gt; || Course-grouped assignment dashboard (rewritten from a topic sign-up sheet)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTasksList.tsx&amp;lt;/code&amp;gt; || Sidebar — not-started tasks, revisions, teammates by course&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTaskDetail.tsx&amp;lt;/code&amp;gt; || Per-assignment detail page — timeline, task links, conditional &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; rendering&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ViewTeamGrades.module.scss&amp;lt;/code&amp;gt; || CSS Modules conversion; scoped &amp;lt;code&amp;gt;c1&amp;lt;/code&amp;gt;–&amp;lt;code&amp;gt;c5&amp;lt;/code&amp;gt; color classes, sticky-column and tooltip styles&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewTable.tsx&amp;lt;/code&amp;gt; || Single-call heatgrid; Scores/Feedback toggle; sticky SectionHeader rows&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewTableRow.tsx&amp;lt;/code&amp;gt; || Sticky &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;/Question columns; reduced row height; hover tooltip via &amp;lt;code&amp;gt;data-question&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;FeedbackTable.tsx&amp;lt;/code&amp;gt; || Feedback-mode table mirroring &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt;'s sticky-column layout&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;heatgridUtils.ts&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;RoundRow&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;isHeader()&amp;lt;/code&amp;gt; types; fixed &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; numbering to skip SectionHeader sentinels&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== StudentTasks (List View) ===&lt;br /&gt;
* Single &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; call, grouped by course&lt;br /&gt;
* Fixed: &amp;quot;Assignments&amp;quot; heading was rendering as body text because its class was referenced as a plain string instead of the CSS Modules export (&amp;lt;code&amp;gt;styles['assignments-title']&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
=== StudentTaskDetail (Detail View) ===&lt;br /&gt;
* Visual timeline with progress line and stage nodes (completed/current/pending)&lt;br /&gt;
* Task links list — &amp;quot;Your work&amp;quot; and &amp;quot;Others' work&amp;quot; conditionally rendered based on &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;quot;Send Email To Reviewers&amp;quot; button repositioned to align with the task list rather than floating disconnected at the page top&lt;br /&gt;
&lt;br /&gt;
=== ViewTeamGrades (Heatgrid) ===&lt;br /&gt;
* Consolidated ~6 sequential API calls into one &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
* SectionHeader rows split into a sticky label cell + scrolling spacer cell so they stay fixed during horizontal scroll&lt;br /&gt;
* Score cells show reviewer feedback as a hover tooltip&lt;br /&gt;
* Round headings simplified from &amp;quot;Review (Round: 1 of 2)&amp;quot; to &amp;quot;Round 1&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Design Principles ==&lt;br /&gt;
&lt;br /&gt;
=== Single Responsibility ===&lt;br /&gt;
Score aggregation is layered: &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; owns item-level scoring, &amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt; owns per-map/per-round weighting, &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; owns cross-map averaging — each level only knows about the one below it.&lt;br /&gt;
&lt;br /&gt;
=== Don't Repeat Yourself ===&lt;br /&gt;
The weighted-average logic that was duplicated between &amp;lt;code&amp;gt;AssignmentTeam#aggregate_review_grade&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;ReviewAggregator#compute_average_review_score&amp;lt;/code&amp;gt; was consolidated into a single &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt;, and the now-dead &amp;lt;code&amp;gt;ReviewAggregator&amp;lt;/code&amp;gt; concern was deleted.&lt;br /&gt;
&lt;br /&gt;
=== Performance by Design ===&lt;br /&gt;
Eager loading (&amp;lt;code&amp;gt;.includes&amp;lt;/code&amp;gt;) and in-memory filtering (&amp;lt;code&amp;gt;.reject&amp;lt;/code&amp;gt;) were chosen deliberately over additional database round-trips after profiling showed the heatgrid endpoint firing hundreds of queries under the old implementation.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
&lt;br /&gt;
=== Backend Tests ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! # !! Test !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| 1 || &amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt; groups teammates by course || Returns &amp;lt;code&amp;gt;{course_name =&amp;gt; [fullnames]}&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 2 || &amp;lt;code&amp;gt;all_teammates&amp;lt;/code&amp;gt; excludes calibrated assignments || Calibrated-assignment teammates excluded&lt;br /&gt;
|-&lt;br /&gt;
| 3 || &amp;lt;code&amp;gt;all_teammates&amp;lt;/code&amp;gt; excludes the user themself || Solo team returns empty hash&lt;br /&gt;
|-&lt;br /&gt;
| 4 || &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; returns all tasks for user || 200 OK with task array&lt;br /&gt;
|-&lt;br /&gt;
| 5 || &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt; returns due_dates timeline || 200 OK with &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt; array&lt;br /&gt;
|-&lt;br /&gt;
| 6 || &amp;lt;code&amp;gt;GET /student_tasks/team&amp;lt;/code&amp;gt; groups teammates by course || 200 OK with grouped hash&lt;br /&gt;
|-&lt;br /&gt;
| 7 || Heatgrid &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; column excludes SectionHeaders from numbering || Sequential numbering skips header rows&lt;br /&gt;
|-&lt;br /&gt;
| 8 || &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt; filters SectionHeaders without re-querying || No additional SQL fired against preloaded association&lt;br /&gt;
|-&lt;br /&gt;
| 9 || &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; weight validation || &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt; must be 0 when rubric has no scored questions&lt;br /&gt;
|-&lt;br /&gt;
| 10 || &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; || Weighted average matches Σ(grade × weight) / Σ(weight)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Frontend Tests ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! # !! Test !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| 1 || &amp;lt;code&amp;gt;StudentTasks&amp;lt;/code&amp;gt; renders &amp;quot;Assignments&amp;quot; heading with correct styling || Heading uses module class, renders bold and large&lt;br /&gt;
|-&lt;br /&gt;
| 2 || &amp;lt;code&amp;gt;StudentTaskDetail&amp;lt;/code&amp;gt; hides &amp;quot;Your work&amp;quot; when &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; is false || List item not rendered&lt;br /&gt;
|-&lt;br /&gt;
| 3 || &amp;lt;code&amp;gt;StudentTaskDetail&amp;lt;/code&amp;gt; hides &amp;quot;Others' work&amp;quot; when &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; is false || List item not rendered&lt;br /&gt;
|-&lt;br /&gt;
| 4 || &amp;lt;code&amp;gt;heatgridUtils&amp;lt;/code&amp;gt; itemNumber skips SectionHeader rows || Numbering matches scored items only&lt;br /&gt;
|-&lt;br /&gt;
| 5 || &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt; SectionHeader row stays fixed during horizontal scroll || Header cell &amp;lt;code&amp;gt;position: sticky&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 6 || &amp;lt;code&amp;gt;ReviewTableRow&amp;lt;/code&amp;gt; shows tooltip only when a comment exists || No tooltip box when &amp;lt;code&amp;gt;data-question&amp;lt;/code&amp;gt; is undefined&lt;br /&gt;
|-&lt;br /&gt;
| 7 || Heatgrid column borders render as &amp;lt;code&amp;gt;#ddd&amp;lt;/code&amp;gt; || No black borders after &amp;lt;code&amp;gt;tbl_heat&amp;lt;/code&amp;gt; class applied&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Relevant Links ==&lt;br /&gt;
&lt;br /&gt;
=== Repos ===&lt;br /&gt;
* https://github.com/expertiza/reimplementation-back-end&lt;br /&gt;
* https://github.com/expertiza/reimplementation-front-end&lt;br /&gt;
&lt;br /&gt;
=== Pull Requests ===&lt;br /&gt;
* Backend: https://github.com/expertiza/reimplementation-back-end/pull/352&lt;br /&gt;
* Frontend: https://github.com/expertiza/reimplementation-front-end/pull/182&lt;br /&gt;
&lt;br /&gt;
=== Demo Video ===&lt;br /&gt;
* (add link once recorded)&lt;br /&gt;
&lt;br /&gt;
== Developer Guidance ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Setup &amp;amp; Run (via Docker) ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
docker compose up&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Within the container ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
bundle exec rspec spec/models/student_task_spec.rb&lt;br /&gt;
bundle exec rspec spec/requests/api/v1/student_tasks_controller_spec.rb&lt;br /&gt;
bundle exec rspec spec/requests/api/v1/grades_controller_spec.rb&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
==== Setup &amp;amp; Run ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm install&lt;br /&gt;
npm start&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Running Frontend Tests ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm test -- StudentTaskDetail   # run a single suite&lt;br /&gt;
npm test                        # run full suite&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Future Scope ==&lt;br /&gt;
&lt;br /&gt;
=== Reviewer reputation-weighted grading ===&lt;br /&gt;
Integrate an external review-quality grader (e.g. an LLM-based reviewer grader) so &amp;lt;code&amp;gt;ResponseMap.reviewer_reputation_for&amp;lt;/code&amp;gt; reflects actual review quality instead of the current &amp;lt;code&amp;gt;1.0&amp;lt;/code&amp;gt; placeholder for every reviewer.&lt;br /&gt;
&lt;br /&gt;
=== Email-the-authors integration ===&lt;br /&gt;
Wire &amp;quot;Send Email To Reviewers&amp;quot; to a real backend endpoint instead of the current placeholder route.&lt;br /&gt;
&lt;br /&gt;
=== Dedicated StudentTask abstraction ===&lt;br /&gt;
&amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; remains a composed, non-persisted object. A future dedicated table could centralize derived state and simplify queries if the feature's scope grows further.&lt;br /&gt;
&lt;br /&gt;
== Mentor ==&lt;br /&gt;
[mailto:kgudipe@ncsu.edu Koushik Gudipelly]&lt;br /&gt;
&lt;br /&gt;
== Members ==&lt;br /&gt;
[mailto:rsgopara@ncsu.edu Ravi Goparaju]&lt;br /&gt;
[mailto:xmi@ncsu.edu Xiangjun Mi]&lt;br /&gt;
&lt;br /&gt;
== Completed by ==&lt;br /&gt;
[mailto:blalu@ncsu.edu Bestin Lalu]&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2602._Reimplement_student_task_view&amp;diff=168246</id>
		<title>CSC/ECE 517 Spring 2026 - E2602. Reimplement student task view</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2602._Reimplement_student_task_view&amp;diff=168246"/>
		<updated>2026-07-07T00:10:40Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* As a student, I want to see all my assignments in one place */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= CSC/ECE 517 Spring 2026 - E2602. Reimplement Student Task View =&lt;br /&gt;
&lt;br /&gt;
== About Expertiza ==&lt;br /&gt;
Expertiza is a robust, open-source web platform designed to empower educational environments that emphasize project-based learning. It streamlines collaborative activities including peer reviews, surveys, and team assignments — giving instructors tools to manage assignments and track performance, and giving students a single place to submit work, review peers, and track feedback.&lt;br /&gt;
&lt;br /&gt;
== Introduction ==&lt;br /&gt;
''This page documents the reimplementation of the Student Task view. It supersedes the earlier [[E2602]] team's version. All searches for E2602 will hit here.''&lt;br /&gt;
This iteration of E2602 hardens and extends the Student Task View: consolidating the student dashboard and detail pages into single-call REST endpoints, eliminating N+1 queries in the peer-review score heatgrid, and fixing several display/permission bugs surfaced by manual QA against real assignment data.&lt;br /&gt;
&lt;br /&gt;
=== Tech Stack ===&lt;br /&gt;
* '''Backend:''' Ruby on Rails (API-only)&lt;br /&gt;
* '''Frontend:''' React + TypeScript&lt;br /&gt;
* '''Database:''' MySQL&lt;br /&gt;
* '''Backend Testing:''' RSpec&lt;br /&gt;
* '''Frontend Testing:''' Jest, React Testing Library&lt;br /&gt;
&lt;br /&gt;
=== Key Improvements ===&lt;br /&gt;
* Eliminated N+1 queries in the score heatgrid (&amp;lt;code&amp;gt;get_team_scores&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_my_scores_data&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_answer&amp;lt;/code&amp;gt;) via &amp;lt;code&amp;gt;.includes(responses: { scores: :item })&amp;lt;/code&amp;gt; and in-memory filtering instead of re-querying preloaded associations&lt;br /&gt;
* Converted &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt;'s stylesheet to CSS Modules, fixing a Bootstrap class collision and scoping color classes (&amp;lt;code&amp;gt;c1&amp;lt;/code&amp;gt;–&amp;lt;code&amp;gt;c5&amp;lt;/code&amp;gt;) correctly&lt;br /&gt;
* Redesigned the score/feedback heatgrid with sticky Question column during horizontal scroll&lt;br /&gt;
* Added hover tooltips on score cells showing the reviewer's written feedback&lt;br /&gt;
* Rewrote &amp;lt;code&amp;gt;StudentTasks&amp;lt;/code&amp;gt; from a (mis-implemented) topic sign-up sheet into a proper course-grouped assignment dashboard&lt;br /&gt;
* Added participant-level &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; permission checks, hiding &amp;quot;Your work&amp;quot; / &amp;quot;Others' work&amp;quot; task links accordingly&lt;br /&gt;
* Refactored review grade calculation: &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; now weights each round by its &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;; &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; centralizes the weighted-average logic previously duplicated across &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Problem Statement ==&lt;br /&gt;
The previous Student Task View had several concrete defects:&lt;br /&gt;
* The score heatgrid fired one SQL query per response/score across all reviewers and rounds, producing hundreds of queries for a single page load&lt;br /&gt;
* SectionHeader rubric rows (non-scorable dividers) were incorrectly counted in the &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; column and lost their fixed position during horizontal scroll&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTasks.tsx&amp;lt;/code&amp;gt; was previously implemented as a topic sign-up sheet, not a task dashboard, and required a full rewrite&lt;br /&gt;
* The page did not respect participant-level &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; flags — all task links were always shown regardless of permission&lt;br /&gt;
* Review grade calculation treated every reviewer and every round equally, ignoring each rubric's configured &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== User Stories ==&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want to see all my assignments in one place ===&lt;br /&gt;
&lt;br /&gt;
This is the home view for a student. It shows all of the student's assignments, arranged by the course they pertain to.&lt;br /&gt;
&lt;br /&gt;
[[File:student_view_assignments_page.png|800px|thumb|center|Student View Assignments Page]]&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; returns all tasks for the logged-in student in one call&lt;br /&gt;
* Tasks are grouped by course on the dashboard&lt;br /&gt;
* Each task shows current stage and days remaining, if applicable&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want to view a single assignment's deadlines and details ===&lt;br /&gt;
&lt;br /&gt;
[[File:student_view_assignment_detail_page.png|800px|thumb|center|Student View Assignment Detail Page]]&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt; returns the task plus a unified &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt; timeline&lt;br /&gt;
* The timeline visually distinguishes completed, current, and pending stages&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/team&amp;lt;/code&amp;gt; returns teammates grouped by course&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want my team's score heatgrid to load fast and stay readable while scrolling ===&lt;br /&gt;
&lt;br /&gt;
[[File:student_view_assignment_grades_page.png|800px|thumb|left|Student View Assignment View Team Grades]]&lt;br /&gt;
[[File:student_view_assignment_feedback_page.png|800px|thumb|right|Student View Assignment View Feedbacks]]&lt;br /&gt;
&amp;lt;br clear=&amp;quot;all&amp;quot; /&amp;gt;&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* The heatgrid loads from a single &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt; call&lt;br /&gt;
* The first two columns (item number &amp;quot;&amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;&amp;quot; and Item) remain fixed during horizontal scroll, along with section headers&lt;br /&gt;
* Hovering over a score with a comment shows the corresponding reviewer's feedback as a tooltip&lt;br /&gt;
&lt;br /&gt;
=== A student who is not permitted to submit or review will not see those task links ===&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;quot;Your work&amp;quot; is hidden when &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;quot;Others' work&amp;quot; is hidden when &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== As an instructor, I want calculated review grades to reflect each rubric's configured weight ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; weights each round's normalized score by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt; both compute their averages through the same shared &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
=== Student Task Overview ===&lt;br /&gt;
A StudentTask is a task that a student performs in the course of doing an assignment, e.g., forming a team, choosing a topic, submitting work, or reviewing work. No dedicated &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; database table exists. A &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; is a plain Ruby object composed from &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; and its associated &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignedUpTeam&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;ProjectTopic&amp;lt;/code&amp;gt;, and permission fields (&amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;permission_granted&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Request / Response Flow ====&lt;br /&gt;
* Student navigates to &amp;lt;code&amp;gt;/student_tasks&amp;lt;/code&amp;gt; → frontend calls &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTask.tasks(user)&amp;lt;/code&amp;gt; preloads &amp;lt;code&amp;gt;assignment: :course&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;:user&amp;lt;/code&amp;gt; associations, then maps each &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; through &amp;lt;code&amp;gt;StudentTask.create_from_participant&amp;lt;/code&amp;gt;&lt;br /&gt;
* Selecting an assignment navigates to the detail route → frontend calls &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTask.timeline_events&amp;lt;/code&amp;gt; merges the assignment's due dates with timestamps of reviews done of the logged-in user's work into one sorted timeline&lt;br /&gt;
* For the heatgrid: frontend calls &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt; → &amp;lt;code&amp;gt;GradesController#get_team_scores&amp;lt;/code&amp;gt; eager-loads &amp;lt;code&amp;gt;responses: { scores: :item }&amp;lt;/code&amp;gt; and calls &amp;lt;code&amp;gt;insert_section_headers&amp;lt;/code&amp;gt; to splice section-header marker objects into the response before it is returned to the frontend&lt;br /&gt;
&lt;br /&gt;
=== Aggregation of Scores Assigned by a Reviewer ===&lt;br /&gt;
* &amp;lt;code&amp;gt;Response#aggregate_questionnaire_score&amp;lt;/code&amp;gt; - item-weighted score for one submitted response&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; — normalizes a map's score per round and weights it by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score(maps)&amp;lt;/code&amp;gt; — weighted average across multiple maps, the single implementation shared by &amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Backend Implementation ==&lt;br /&gt;
&lt;br /&gt;
=== StudentTasksController ===&lt;br /&gt;
* &amp;lt;code&amp;gt;list&amp;lt;/code&amp;gt; — all &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; objects for the logged-in user&lt;br /&gt;
* &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt; — single task by participant ID with the unified timeline&lt;br /&gt;
* &amp;lt;code&amp;gt;team&amp;lt;/code&amp;gt; — teammates grouped by course name (&amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
=== GradesController — N+1 elimination ===&lt;br /&gt;
&amp;lt;code&amp;gt;get_team_scores&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;get_my_scores_data&amp;lt;/code&amp;gt; eager-load &amp;lt;code&amp;gt;responses: { scores: :item }&amp;lt;/code&amp;gt; on the &amp;lt;code&amp;gt;ReviewResponseMap&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;TeammateReviewResponseMap&amp;lt;/code&amp;gt; queries. &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt; filters SectionHeader scores via Ruby &amp;lt;code&amp;gt;.reject&amp;lt;/code&amp;gt; instead of &amp;lt;code&amp;gt;.joins&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;.where&amp;lt;/code&amp;gt; — calling &amp;lt;code&amp;gt;.where&amp;lt;/code&amp;gt; on an already-preloaded association silently bypasses the cache and re-queries the database. &amp;lt;code&amp;gt;get_answer&amp;lt;/code&amp;gt; now accepts the parent &amp;lt;code&amp;gt;response&amp;lt;/code&amp;gt; directly instead of re-fetching it via &amp;lt;code&amp;gt;score.response&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== insert_section_headers ===&lt;br /&gt;
Inserts &amp;lt;code&amp;gt;{ type: &amp;quot;header&amp;quot;, txt: &amp;quot;...&amp;quot; }&amp;lt;/code&amp;gt; marker objects into the scores array at positions derived from each &amp;lt;code&amp;gt;SectionHeader&amp;lt;/code&amp;gt; item's sequence order, so the frontend renders section heading rows between score rows without those rows affecting item numbering.&lt;br /&gt;
&lt;br /&gt;
=== Review Grade Calculation ===&lt;br /&gt;
For details on how peer review grades are computed, see [[Score Calculation]].&lt;br /&gt;
&lt;br /&gt;
=== Comprehensive Testing ===&lt;br /&gt;
* Model specs for &amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;create_from_participant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_events_for_assignment&amp;lt;/code&amp;gt;&lt;br /&gt;
* Request specs for &amp;lt;code&amp;gt;student_tasks/list&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;student_tasks/show/:id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;student_tasks/team&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;grades/:id/view_our_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
* Routing specs verifying the above routes resolve correctly&lt;br /&gt;
&lt;br /&gt;
== Frontend Implementation ==&lt;br /&gt;
=== New/Updated Files ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! File !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTasks.tsx&amp;lt;/code&amp;gt; || Course-grouped assignment dashboard (rewritten from a topic sign-up sheet)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTasksList.tsx&amp;lt;/code&amp;gt; || Sidebar — not-started tasks, revisions, teammates by course&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTaskDetail.tsx&amp;lt;/code&amp;gt; || Per-assignment detail page — timeline, task links, conditional &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; rendering&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ViewTeamGrades.module.scss&amp;lt;/code&amp;gt; || CSS Modules conversion; scoped &amp;lt;code&amp;gt;c1&amp;lt;/code&amp;gt;–&amp;lt;code&amp;gt;c5&amp;lt;/code&amp;gt; color classes, sticky-column and tooltip styles&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewTable.tsx&amp;lt;/code&amp;gt; || Single-call heatgrid; Scores/Feedback toggle; sticky SectionHeader rows&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewTableRow.tsx&amp;lt;/code&amp;gt; || Sticky &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;/Question columns; reduced row height; hover tooltip via &amp;lt;code&amp;gt;data-question&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;FeedbackTable.tsx&amp;lt;/code&amp;gt; || Feedback-mode table mirroring &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt;'s sticky-column layout&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;heatgridUtils.ts&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;RoundRow&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;isHeader()&amp;lt;/code&amp;gt; types; fixed &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; numbering to skip SectionHeader sentinels&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== StudentTasks (List View) ===&lt;br /&gt;
* Single &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; call, grouped by course&lt;br /&gt;
* Fixed: &amp;quot;Assignments&amp;quot; heading was rendering as body text because its class was referenced as a plain string instead of the CSS Modules export (&amp;lt;code&amp;gt;styles['assignments-title']&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
=== StudentTaskDetail (Detail View) ===&lt;br /&gt;
* Visual timeline with progress line and stage nodes (completed/current/pending)&lt;br /&gt;
* Task links list — &amp;quot;Your work&amp;quot; and &amp;quot;Others' work&amp;quot; conditionally rendered based on &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;quot;Send Email To Reviewers&amp;quot; button repositioned to align with the task list rather than floating disconnected at the page top&lt;br /&gt;
&lt;br /&gt;
=== ViewTeamGrades (Heatgrid) ===&lt;br /&gt;
* Consolidated ~6 sequential API calls into one &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
* SectionHeader rows split into a sticky label cell + scrolling spacer cell so they stay fixed during horizontal scroll&lt;br /&gt;
* Score cells show reviewer feedback as a hover tooltip&lt;br /&gt;
* Round headings simplified from &amp;quot;Review (Round: 1 of 2)&amp;quot; to &amp;quot;Round 1&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Design Principles ==&lt;br /&gt;
&lt;br /&gt;
=== Single Responsibility ===&lt;br /&gt;
Score aggregation is layered: &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; owns item-level scoring, &amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt; owns per-map/per-round weighting, &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; owns cross-map averaging — each level only knows about the one below it.&lt;br /&gt;
&lt;br /&gt;
=== Don't Repeat Yourself ===&lt;br /&gt;
The weighted-average logic that was duplicated between &amp;lt;code&amp;gt;AssignmentTeam#aggregate_review_grade&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;ReviewAggregator#compute_average_review_score&amp;lt;/code&amp;gt; was consolidated into a single &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt;, and the now-dead &amp;lt;code&amp;gt;ReviewAggregator&amp;lt;/code&amp;gt; concern was deleted.&lt;br /&gt;
&lt;br /&gt;
=== Performance by Design ===&lt;br /&gt;
Eager loading (&amp;lt;code&amp;gt;.includes&amp;lt;/code&amp;gt;) and in-memory filtering (&amp;lt;code&amp;gt;.reject&amp;lt;/code&amp;gt;) were chosen deliberately over additional database round-trips after profiling showed the heatgrid endpoint firing hundreds of queries under the old implementation.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
&lt;br /&gt;
=== Backend Tests ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! # !! Test !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| 1 || &amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt; groups teammates by course || Returns &amp;lt;code&amp;gt;{course_name =&amp;gt; [fullnames]}&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 2 || &amp;lt;code&amp;gt;all_teammates&amp;lt;/code&amp;gt; excludes calibrated assignments || Calibrated-assignment teammates excluded&lt;br /&gt;
|-&lt;br /&gt;
| 3 || &amp;lt;code&amp;gt;all_teammates&amp;lt;/code&amp;gt; excludes the user themself || Solo team returns empty hash&lt;br /&gt;
|-&lt;br /&gt;
| 4 || &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; returns all tasks for user || 200 OK with task array&lt;br /&gt;
|-&lt;br /&gt;
| 5 || &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt; returns due_dates timeline || 200 OK with &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt; array&lt;br /&gt;
|-&lt;br /&gt;
| 6 || &amp;lt;code&amp;gt;GET /student_tasks/team&amp;lt;/code&amp;gt; groups teammates by course || 200 OK with grouped hash&lt;br /&gt;
|-&lt;br /&gt;
| 7 || Heatgrid &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; column excludes SectionHeaders from numbering || Sequential numbering skips header rows&lt;br /&gt;
|-&lt;br /&gt;
| 8 || &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt; filters SectionHeaders without re-querying || No additional SQL fired against preloaded association&lt;br /&gt;
|-&lt;br /&gt;
| 9 || &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; weight validation || &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt; must be 0 when rubric has no scored questions&lt;br /&gt;
|-&lt;br /&gt;
| 10 || &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; || Weighted average matches Σ(grade × weight) / Σ(weight)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Frontend Tests ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! # !! Test !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| 1 || &amp;lt;code&amp;gt;StudentTasks&amp;lt;/code&amp;gt; renders &amp;quot;Assignments&amp;quot; heading with correct styling || Heading uses module class, renders bold and large&lt;br /&gt;
|-&lt;br /&gt;
| 2 || &amp;lt;code&amp;gt;StudentTaskDetail&amp;lt;/code&amp;gt; hides &amp;quot;Your work&amp;quot; when &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; is false || List item not rendered&lt;br /&gt;
|-&lt;br /&gt;
| 3 || &amp;lt;code&amp;gt;StudentTaskDetail&amp;lt;/code&amp;gt; hides &amp;quot;Others' work&amp;quot; when &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; is false || List item not rendered&lt;br /&gt;
|-&lt;br /&gt;
| 4 || &amp;lt;code&amp;gt;heatgridUtils&amp;lt;/code&amp;gt; itemNumber skips SectionHeader rows || Numbering matches scored items only&lt;br /&gt;
|-&lt;br /&gt;
| 5 || &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt; SectionHeader row stays fixed during horizontal scroll || Header cell &amp;lt;code&amp;gt;position: sticky&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 6 || &amp;lt;code&amp;gt;ReviewTableRow&amp;lt;/code&amp;gt; shows tooltip only when a comment exists || No tooltip box when &amp;lt;code&amp;gt;data-question&amp;lt;/code&amp;gt; is undefined&lt;br /&gt;
|-&lt;br /&gt;
| 7 || Heatgrid column borders render as &amp;lt;code&amp;gt;#ddd&amp;lt;/code&amp;gt; || No black borders after &amp;lt;code&amp;gt;tbl_heat&amp;lt;/code&amp;gt; class applied&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Relevant Links ==&lt;br /&gt;
&lt;br /&gt;
=== Repos ===&lt;br /&gt;
* https://github.com/expertiza/reimplementation-back-end&lt;br /&gt;
* https://github.com/expertiza/reimplementation-front-end&lt;br /&gt;
&lt;br /&gt;
=== Pull Requests ===&lt;br /&gt;
* Backend: https://github.com/expertiza/reimplementation-back-end/pull/352&lt;br /&gt;
* Frontend: https://github.com/expertiza/reimplementation-front-end/pull/182&lt;br /&gt;
&lt;br /&gt;
=== Demo Video ===&lt;br /&gt;
* (add link once recorded)&lt;br /&gt;
&lt;br /&gt;
== Developer Guidance ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Setup &amp;amp; Run (via Docker) ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
docker compose up&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Within the container ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
bundle exec rspec spec/models/student_task_spec.rb&lt;br /&gt;
bundle exec rspec spec/requests/api/v1/student_tasks_controller_spec.rb&lt;br /&gt;
bundle exec rspec spec/requests/api/v1/grades_controller_spec.rb&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
==== Setup &amp;amp; Run ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm install&lt;br /&gt;
npm start&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Running Frontend Tests ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm test -- StudentTaskDetail   # run a single suite&lt;br /&gt;
npm test                        # run full suite&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Future Scope ==&lt;br /&gt;
&lt;br /&gt;
=== Reviewer reputation-weighted grading ===&lt;br /&gt;
Integrate an external review-quality grader (e.g. an LLM-based reviewer grader) so &amp;lt;code&amp;gt;ResponseMap.reviewer_reputation_for&amp;lt;/code&amp;gt; reflects actual review quality instead of the current &amp;lt;code&amp;gt;1.0&amp;lt;/code&amp;gt; placeholder for every reviewer.&lt;br /&gt;
&lt;br /&gt;
=== Email-the-authors integration ===&lt;br /&gt;
Wire &amp;quot;Send Email To Reviewers&amp;quot; to a real backend endpoint instead of the current placeholder route.&lt;br /&gt;
&lt;br /&gt;
=== Dedicated StudentTask abstraction ===&lt;br /&gt;
&amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; remains a composed, non-persisted object. A future dedicated table could centralize derived state and simplify queries if the feature's scope grows further.&lt;br /&gt;
&lt;br /&gt;
== Mentor ==&lt;br /&gt;
[mailto:kgudipe@ncsu.edu Koushik Gudipelly]&lt;br /&gt;
&lt;br /&gt;
== Members ==&lt;br /&gt;
[mailto:rsgopara@ncsu.edu Ravi Goparaju]&lt;br /&gt;
[mailto:xmi@ncsu.edu Xiangjun Mi]&lt;br /&gt;
&lt;br /&gt;
== Completed by ==&lt;br /&gt;
[mailto:blalu@ncsu.edu Bestin Lalu]&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2602._Reimplement_student_task_view&amp;diff=168245</id>
		<title>CSC/ECE 517 Spring 2026 - E2602. Reimplement student task view</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2602._Reimplement_student_task_view&amp;diff=168245"/>
		<updated>2026-07-07T00:08:41Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Introduction */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= CSC/ECE 517 Spring 2026 - E2602. Reimplement Student Task View =&lt;br /&gt;
&lt;br /&gt;
== About Expertiza ==&lt;br /&gt;
Expertiza is a robust, open-source web platform designed to empower educational environments that emphasize project-based learning. It streamlines collaborative activities including peer reviews, surveys, and team assignments — giving instructors tools to manage assignments and track performance, and giving students a single place to submit work, review peers, and track feedback.&lt;br /&gt;
&lt;br /&gt;
== Introduction ==&lt;br /&gt;
''This page documents the reimplementation of the Student Task view. It supersedes the earlier [[E2602]] team's version. All searches for E2602 will hit here.''&lt;br /&gt;
This iteration of E2602 hardens and extends the Student Task View: consolidating the student dashboard and detail pages into single-call REST endpoints, eliminating N+1 queries in the peer-review score heatgrid, and fixing several display/permission bugs surfaced by manual QA against real assignment data.&lt;br /&gt;
&lt;br /&gt;
=== Tech Stack ===&lt;br /&gt;
* '''Backend:''' Ruby on Rails (API-only)&lt;br /&gt;
* '''Frontend:''' React + TypeScript&lt;br /&gt;
* '''Database:''' MySQL&lt;br /&gt;
* '''Backend Testing:''' RSpec&lt;br /&gt;
* '''Frontend Testing:''' Jest, React Testing Library&lt;br /&gt;
&lt;br /&gt;
=== Key Improvements ===&lt;br /&gt;
* Eliminated N+1 queries in the score heatgrid (&amp;lt;code&amp;gt;get_team_scores&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_my_scores_data&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_answer&amp;lt;/code&amp;gt;) via &amp;lt;code&amp;gt;.includes(responses: { scores: :item })&amp;lt;/code&amp;gt; and in-memory filtering instead of re-querying preloaded associations&lt;br /&gt;
* Converted &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt;'s stylesheet to CSS Modules, fixing a Bootstrap class collision and scoping color classes (&amp;lt;code&amp;gt;c1&amp;lt;/code&amp;gt;–&amp;lt;code&amp;gt;c5&amp;lt;/code&amp;gt;) correctly&lt;br /&gt;
* Redesigned the score/feedback heatgrid with sticky Question column during horizontal scroll&lt;br /&gt;
* Added hover tooltips on score cells showing the reviewer's written feedback&lt;br /&gt;
* Rewrote &amp;lt;code&amp;gt;StudentTasks&amp;lt;/code&amp;gt; from a (mis-implemented) topic sign-up sheet into a proper course-grouped assignment dashboard&lt;br /&gt;
* Added participant-level &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; permission checks, hiding &amp;quot;Your work&amp;quot; / &amp;quot;Others' work&amp;quot; task links accordingly&lt;br /&gt;
* Refactored review grade calculation: &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; now weights each round by its &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;; &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; centralizes the weighted-average logic previously duplicated across &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Problem Statement ==&lt;br /&gt;
The previous Student Task View had several concrete defects:&lt;br /&gt;
* The score heatgrid fired one SQL query per response/score across all reviewers and rounds, producing hundreds of queries for a single page load&lt;br /&gt;
* SectionHeader rubric rows (non-scorable dividers) were incorrectly counted in the &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; column and lost their fixed position during horizontal scroll&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTasks.tsx&amp;lt;/code&amp;gt; was previously implemented as a topic sign-up sheet, not a task dashboard, and required a full rewrite&lt;br /&gt;
* The page did not respect participant-level &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; flags — all task links were always shown regardless of permission&lt;br /&gt;
* Review grade calculation treated every reviewer and every round equally, ignoring each rubric's configured &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== User Stories ==&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want to see all my assignments in one place ===&lt;br /&gt;
&lt;br /&gt;
[[File:student_view_assignments_page.png|800px|thumb|center|Student View Assignments Page]]&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; returns all tasks for the logged-in student in one call&lt;br /&gt;
* Tasks are grouped by course on the dashboard&lt;br /&gt;
* Each task shows current stage and days remaining, if applicable&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want to view a single assignment's deadlines and details ===&lt;br /&gt;
&lt;br /&gt;
[[File:student_view_assignment_detail_page.png|800px|thumb|center|Student View Assignment Detail Page]]&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt; returns the task plus a unified &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt; timeline&lt;br /&gt;
* The timeline visually distinguishes completed, current, and pending stages&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/team&amp;lt;/code&amp;gt; returns teammates grouped by course&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want my team's score heatgrid to load fast and stay readable while scrolling ===&lt;br /&gt;
&lt;br /&gt;
[[File:student_view_assignment_grades_page.png|800px|thumb|left|Student View Assignment View Team Grades]]&lt;br /&gt;
[[File:student_view_assignment_feedback_page.png|800px|thumb|right|Student View Assignment View Feedbacks]]&lt;br /&gt;
&amp;lt;br clear=&amp;quot;all&amp;quot; /&amp;gt;&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* The heatgrid loads from a single &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt; call&lt;br /&gt;
* The first two columns (item number &amp;quot;&amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;&amp;quot; and Item) remain fixed during horizontal scroll, along with section headers&lt;br /&gt;
* Hovering over a score with a comment shows the corresponding reviewer's feedback as a tooltip&lt;br /&gt;
&lt;br /&gt;
=== A student who is not permitted to submit or review will not see those task links ===&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;quot;Your work&amp;quot; is hidden when &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;quot;Others' work&amp;quot; is hidden when &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== As an instructor, I want calculated review grades to reflect each rubric's configured weight ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; weights each round's normalized score by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt; both compute their averages through the same shared &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
=== Student Task Overview ===&lt;br /&gt;
A StudentTask is a task that a student performs in the course of doing an assignment, e.g., forming a team, choosing a topic, submitting work, or reviewing work. No dedicated &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; database table exists. A &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; is a plain Ruby object composed from &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; and its associated &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignedUpTeam&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;ProjectTopic&amp;lt;/code&amp;gt;, and permission fields (&amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;permission_granted&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Request / Response Flow ====&lt;br /&gt;
* Student navigates to &amp;lt;code&amp;gt;/student_tasks&amp;lt;/code&amp;gt; → frontend calls &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTask.tasks(user)&amp;lt;/code&amp;gt; preloads &amp;lt;code&amp;gt;assignment: :course&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;:user&amp;lt;/code&amp;gt; associations, then maps each &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; through &amp;lt;code&amp;gt;StudentTask.create_from_participant&amp;lt;/code&amp;gt;&lt;br /&gt;
* Selecting an assignment navigates to the detail route → frontend calls &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTask.timeline_events&amp;lt;/code&amp;gt; merges the assignment's due dates with timestamps of reviews done of the logged-in user's work into one sorted timeline&lt;br /&gt;
* For the heatgrid: frontend calls &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt; → &amp;lt;code&amp;gt;GradesController#get_team_scores&amp;lt;/code&amp;gt; eager-loads &amp;lt;code&amp;gt;responses: { scores: :item }&amp;lt;/code&amp;gt; and calls &amp;lt;code&amp;gt;insert_section_headers&amp;lt;/code&amp;gt; to splice section-header marker objects into the response before it is returned to the frontend&lt;br /&gt;
&lt;br /&gt;
=== Aggregation of Scores Assigned by a Reviewer ===&lt;br /&gt;
* &amp;lt;code&amp;gt;Response#aggregate_questionnaire_score&amp;lt;/code&amp;gt; - item-weighted score for one submitted response&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; — normalizes a map's score per round and weights it by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score(maps)&amp;lt;/code&amp;gt; — weighted average across multiple maps, the single implementation shared by &amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Backend Implementation ==&lt;br /&gt;
&lt;br /&gt;
=== StudentTasksController ===&lt;br /&gt;
* &amp;lt;code&amp;gt;list&amp;lt;/code&amp;gt; — all &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; objects for the logged-in user&lt;br /&gt;
* &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt; — single task by participant ID with the unified timeline&lt;br /&gt;
* &amp;lt;code&amp;gt;team&amp;lt;/code&amp;gt; — teammates grouped by course name (&amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
=== GradesController — N+1 elimination ===&lt;br /&gt;
&amp;lt;code&amp;gt;get_team_scores&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;get_my_scores_data&amp;lt;/code&amp;gt; eager-load &amp;lt;code&amp;gt;responses: { scores: :item }&amp;lt;/code&amp;gt; on the &amp;lt;code&amp;gt;ReviewResponseMap&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;TeammateReviewResponseMap&amp;lt;/code&amp;gt; queries. &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt; filters SectionHeader scores via Ruby &amp;lt;code&amp;gt;.reject&amp;lt;/code&amp;gt; instead of &amp;lt;code&amp;gt;.joins&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;.where&amp;lt;/code&amp;gt; — calling &amp;lt;code&amp;gt;.where&amp;lt;/code&amp;gt; on an already-preloaded association silently bypasses the cache and re-queries the database. &amp;lt;code&amp;gt;get_answer&amp;lt;/code&amp;gt; now accepts the parent &amp;lt;code&amp;gt;response&amp;lt;/code&amp;gt; directly instead of re-fetching it via &amp;lt;code&amp;gt;score.response&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== insert_section_headers ===&lt;br /&gt;
Inserts &amp;lt;code&amp;gt;{ type: &amp;quot;header&amp;quot;, txt: &amp;quot;...&amp;quot; }&amp;lt;/code&amp;gt; marker objects into the scores array at positions derived from each &amp;lt;code&amp;gt;SectionHeader&amp;lt;/code&amp;gt; item's sequence order, so the frontend renders section heading rows between score rows without those rows affecting item numbering.&lt;br /&gt;
&lt;br /&gt;
=== Review Grade Calculation ===&lt;br /&gt;
For details on how peer review grades are computed, see [[Score Calculation]].&lt;br /&gt;
&lt;br /&gt;
=== Comprehensive Testing ===&lt;br /&gt;
* Model specs for &amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;create_from_participant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_events_for_assignment&amp;lt;/code&amp;gt;&lt;br /&gt;
* Request specs for &amp;lt;code&amp;gt;student_tasks/list&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;student_tasks/show/:id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;student_tasks/team&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;grades/:id/view_our_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
* Routing specs verifying the above routes resolve correctly&lt;br /&gt;
&lt;br /&gt;
== Frontend Implementation ==&lt;br /&gt;
=== New/Updated Files ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! File !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTasks.tsx&amp;lt;/code&amp;gt; || Course-grouped assignment dashboard (rewritten from a topic sign-up sheet)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTasksList.tsx&amp;lt;/code&amp;gt; || Sidebar — not-started tasks, revisions, teammates by course&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTaskDetail.tsx&amp;lt;/code&amp;gt; || Per-assignment detail page — timeline, task links, conditional &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; rendering&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ViewTeamGrades.module.scss&amp;lt;/code&amp;gt; || CSS Modules conversion; scoped &amp;lt;code&amp;gt;c1&amp;lt;/code&amp;gt;–&amp;lt;code&amp;gt;c5&amp;lt;/code&amp;gt; color classes, sticky-column and tooltip styles&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewTable.tsx&amp;lt;/code&amp;gt; || Single-call heatgrid; Scores/Feedback toggle; sticky SectionHeader rows&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewTableRow.tsx&amp;lt;/code&amp;gt; || Sticky &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;/Question columns; reduced row height; hover tooltip via &amp;lt;code&amp;gt;data-question&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;FeedbackTable.tsx&amp;lt;/code&amp;gt; || Feedback-mode table mirroring &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt;'s sticky-column layout&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;heatgridUtils.ts&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;RoundRow&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;isHeader()&amp;lt;/code&amp;gt; types; fixed &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; numbering to skip SectionHeader sentinels&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== StudentTasks (List View) ===&lt;br /&gt;
* Single &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; call, grouped by course&lt;br /&gt;
* Fixed: &amp;quot;Assignments&amp;quot; heading was rendering as body text because its class was referenced as a plain string instead of the CSS Modules export (&amp;lt;code&amp;gt;styles['assignments-title']&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
=== StudentTaskDetail (Detail View) ===&lt;br /&gt;
* Visual timeline with progress line and stage nodes (completed/current/pending)&lt;br /&gt;
* Task links list — &amp;quot;Your work&amp;quot; and &amp;quot;Others' work&amp;quot; conditionally rendered based on &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;quot;Send Email To Reviewers&amp;quot; button repositioned to align with the task list rather than floating disconnected at the page top&lt;br /&gt;
&lt;br /&gt;
=== ViewTeamGrades (Heatgrid) ===&lt;br /&gt;
* Consolidated ~6 sequential API calls into one &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
* SectionHeader rows split into a sticky label cell + scrolling spacer cell so they stay fixed during horizontal scroll&lt;br /&gt;
* Score cells show reviewer feedback as a hover tooltip&lt;br /&gt;
* Round headings simplified from &amp;quot;Review (Round: 1 of 2)&amp;quot; to &amp;quot;Round 1&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Design Principles ==&lt;br /&gt;
&lt;br /&gt;
=== Single Responsibility ===&lt;br /&gt;
Score aggregation is layered: &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; owns item-level scoring, &amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt; owns per-map/per-round weighting, &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; owns cross-map averaging — each level only knows about the one below it.&lt;br /&gt;
&lt;br /&gt;
=== Don't Repeat Yourself ===&lt;br /&gt;
The weighted-average logic that was duplicated between &amp;lt;code&amp;gt;AssignmentTeam#aggregate_review_grade&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;ReviewAggregator#compute_average_review_score&amp;lt;/code&amp;gt; was consolidated into a single &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt;, and the now-dead &amp;lt;code&amp;gt;ReviewAggregator&amp;lt;/code&amp;gt; concern was deleted.&lt;br /&gt;
&lt;br /&gt;
=== Performance by Design ===&lt;br /&gt;
Eager loading (&amp;lt;code&amp;gt;.includes&amp;lt;/code&amp;gt;) and in-memory filtering (&amp;lt;code&amp;gt;.reject&amp;lt;/code&amp;gt;) were chosen deliberately over additional database round-trips after profiling showed the heatgrid endpoint firing hundreds of queries under the old implementation.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
&lt;br /&gt;
=== Backend Tests ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! # !! Test !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| 1 || &amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt; groups teammates by course || Returns &amp;lt;code&amp;gt;{course_name =&amp;gt; [fullnames]}&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 2 || &amp;lt;code&amp;gt;all_teammates&amp;lt;/code&amp;gt; excludes calibrated assignments || Calibrated-assignment teammates excluded&lt;br /&gt;
|-&lt;br /&gt;
| 3 || &amp;lt;code&amp;gt;all_teammates&amp;lt;/code&amp;gt; excludes the user themself || Solo team returns empty hash&lt;br /&gt;
|-&lt;br /&gt;
| 4 || &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; returns all tasks for user || 200 OK with task array&lt;br /&gt;
|-&lt;br /&gt;
| 5 || &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt; returns due_dates timeline || 200 OK with &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt; array&lt;br /&gt;
|-&lt;br /&gt;
| 6 || &amp;lt;code&amp;gt;GET /student_tasks/team&amp;lt;/code&amp;gt; groups teammates by course || 200 OK with grouped hash&lt;br /&gt;
|-&lt;br /&gt;
| 7 || Heatgrid &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; column excludes SectionHeaders from numbering || Sequential numbering skips header rows&lt;br /&gt;
|-&lt;br /&gt;
| 8 || &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt; filters SectionHeaders without re-querying || No additional SQL fired against preloaded association&lt;br /&gt;
|-&lt;br /&gt;
| 9 || &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; weight validation || &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt; must be 0 when rubric has no scored questions&lt;br /&gt;
|-&lt;br /&gt;
| 10 || &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; || Weighted average matches Σ(grade × weight) / Σ(weight)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Frontend Tests ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! # !! Test !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| 1 || &amp;lt;code&amp;gt;StudentTasks&amp;lt;/code&amp;gt; renders &amp;quot;Assignments&amp;quot; heading with correct styling || Heading uses module class, renders bold and large&lt;br /&gt;
|-&lt;br /&gt;
| 2 || &amp;lt;code&amp;gt;StudentTaskDetail&amp;lt;/code&amp;gt; hides &amp;quot;Your work&amp;quot; when &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; is false || List item not rendered&lt;br /&gt;
|-&lt;br /&gt;
| 3 || &amp;lt;code&amp;gt;StudentTaskDetail&amp;lt;/code&amp;gt; hides &amp;quot;Others' work&amp;quot; when &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; is false || List item not rendered&lt;br /&gt;
|-&lt;br /&gt;
| 4 || &amp;lt;code&amp;gt;heatgridUtils&amp;lt;/code&amp;gt; itemNumber skips SectionHeader rows || Numbering matches scored items only&lt;br /&gt;
|-&lt;br /&gt;
| 5 || &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt; SectionHeader row stays fixed during horizontal scroll || Header cell &amp;lt;code&amp;gt;position: sticky&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 6 || &amp;lt;code&amp;gt;ReviewTableRow&amp;lt;/code&amp;gt; shows tooltip only when a comment exists || No tooltip box when &amp;lt;code&amp;gt;data-question&amp;lt;/code&amp;gt; is undefined&lt;br /&gt;
|-&lt;br /&gt;
| 7 || Heatgrid column borders render as &amp;lt;code&amp;gt;#ddd&amp;lt;/code&amp;gt; || No black borders after &amp;lt;code&amp;gt;tbl_heat&amp;lt;/code&amp;gt; class applied&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Relevant Links ==&lt;br /&gt;
&lt;br /&gt;
=== Repos ===&lt;br /&gt;
* https://github.com/expertiza/reimplementation-back-end&lt;br /&gt;
* https://github.com/expertiza/reimplementation-front-end&lt;br /&gt;
&lt;br /&gt;
=== Pull Requests ===&lt;br /&gt;
* Backend: https://github.com/expertiza/reimplementation-back-end/pull/352&lt;br /&gt;
* Frontend: https://github.com/expertiza/reimplementation-front-end/pull/182&lt;br /&gt;
&lt;br /&gt;
=== Demo Video ===&lt;br /&gt;
* (add link once recorded)&lt;br /&gt;
&lt;br /&gt;
== Developer Guidance ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Setup &amp;amp; Run (via Docker) ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
docker compose up&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Within the container ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
bundle exec rspec spec/models/student_task_spec.rb&lt;br /&gt;
bundle exec rspec spec/requests/api/v1/student_tasks_controller_spec.rb&lt;br /&gt;
bundle exec rspec spec/requests/api/v1/grades_controller_spec.rb&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
==== Setup &amp;amp; Run ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm install&lt;br /&gt;
npm start&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Running Frontend Tests ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm test -- StudentTaskDetail   # run a single suite&lt;br /&gt;
npm test                        # run full suite&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Future Scope ==&lt;br /&gt;
&lt;br /&gt;
=== Reviewer reputation-weighted grading ===&lt;br /&gt;
Integrate an external review-quality grader (e.g. an LLM-based reviewer grader) so &amp;lt;code&amp;gt;ResponseMap.reviewer_reputation_for&amp;lt;/code&amp;gt; reflects actual review quality instead of the current &amp;lt;code&amp;gt;1.0&amp;lt;/code&amp;gt; placeholder for every reviewer.&lt;br /&gt;
&lt;br /&gt;
=== Email-the-authors integration ===&lt;br /&gt;
Wire &amp;quot;Send Email To Reviewers&amp;quot; to a real backend endpoint instead of the current placeholder route.&lt;br /&gt;
&lt;br /&gt;
=== Dedicated StudentTask abstraction ===&lt;br /&gt;
&amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; remains a composed, non-persisted object. A future dedicated table could centralize derived state and simplify queries if the feature's scope grows further.&lt;br /&gt;
&lt;br /&gt;
== Mentor ==&lt;br /&gt;
[mailto:kgudipe@ncsu.edu Koushik Gudipelly]&lt;br /&gt;
&lt;br /&gt;
== Members ==&lt;br /&gt;
[mailto:rsgopara@ncsu.edu Ravi Goparaju]&lt;br /&gt;
[mailto:xmi@ncsu.edu Xiangjun Mi]&lt;br /&gt;
&lt;br /&gt;
== Completed by ==&lt;br /&gt;
[mailto:blalu@ncsu.edu Bestin Lalu]&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2602._Reimplement_student_task_view&amp;diff=168237</id>
		<title>CSC/ECE 517 Spring 2026 - E2602. Reimplement student task view</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2602._Reimplement_student_task_view&amp;diff=168237"/>
		<updated>2026-07-02T23:54:39Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Review Grade Calculation */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= CSC/ECE 517 Spring 2026 - E2602. Reimplement Student Task View =&lt;br /&gt;
&lt;br /&gt;
== About Expertiza ==&lt;br /&gt;
Expertiza is a robust, open-source web platform designed to empower educational environments that emphasize project-based learning. It streamlines collaborative activities including peer reviews, surveys, and team assignments — giving instructors tools to manage assignments and track performance, and giving students a single place to submit work, review peers, and track feedback.&lt;br /&gt;
&lt;br /&gt;
== Introduction ==&lt;br /&gt;
This iteration of E2602 hardens and extends the Student Task View: consolidating the student dashboard and detail pages into single-call REST endpoints, eliminating N+1 queries in the peer-review score heatgrid, and fixing several display/permission bugs surfaced by manual QA against real assignment data.&lt;br /&gt;
&lt;br /&gt;
=== Tech Stack ===&lt;br /&gt;
* '''Backend:''' Ruby on Rails (API-only)&lt;br /&gt;
* '''Frontend:''' React + TypeScript&lt;br /&gt;
* '''Database:''' MySQL&lt;br /&gt;
* '''Backend Testing:''' RSpec&lt;br /&gt;
* '''Frontend Testing:''' Jest, React Testing Library&lt;br /&gt;
&lt;br /&gt;
=== Key Improvements ===&lt;br /&gt;
* Eliminated N+1 queries in the score heatgrid (&amp;lt;code&amp;gt;get_team_scores&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_my_scores_data&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_answer&amp;lt;/code&amp;gt;) via &amp;lt;code&amp;gt;.includes(responses: { scores: :item })&amp;lt;/code&amp;gt; and in-memory filtering instead of re-querying preloaded associations&lt;br /&gt;
* Converted &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt;'s stylesheet to CSS Modules, fixing a Bootstrap class collision and scoping color classes (&amp;lt;code&amp;gt;c1&amp;lt;/code&amp;gt;–&amp;lt;code&amp;gt;c5&amp;lt;/code&amp;gt;) correctly&lt;br /&gt;
* Redesigned the score/feedback heatgrid with sticky &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;/Question columns and a sticky, full-width SectionHeader row that stays fixed during horizontal scroll&lt;br /&gt;
* Added hover tooltips on score cells showing the reviewer's written feedback&lt;br /&gt;
* Rewrote &amp;lt;code&amp;gt;StudentTasks&amp;lt;/code&amp;gt; from a (mis-implemented) topic sign-up sheet into a proper course-grouped assignment dashboard&lt;br /&gt;
* Added participant-level &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; permission checks, hiding &amp;quot;Your work&amp;quot; / &amp;quot;Others' work&amp;quot; task links accordingly&lt;br /&gt;
* Refactored review grade calculation: &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; now weights each round by its &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;; &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; centralizes the weighted-average logic previously duplicated across &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Problem Statement ==&lt;br /&gt;
The previous Student Task View had several concrete defects:&lt;br /&gt;
* The score heatgrid fired one SQL query per response/score across all reviewers and rounds, producing hundreds of queries for a single page load&lt;br /&gt;
* SectionHeader rubric rows (non-scorable dividers) were incorrectly counted in the &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; column numbering and lost their fixed position during horizontal scroll&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTasks.tsx&amp;lt;/code&amp;gt; was previously implemented as a topic sign-up sheet, not a task dashboard, and required a full rewrite&lt;br /&gt;
* The page did not respect participant-level &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; flags — all task links were always shown regardless of permission&lt;br /&gt;
* Review grade calculation treated every reviewer and every round equally, ignoring each rubric's configured &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== User Stories ==&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want to see all my assignments in one place ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; returns all tasks for the logged-in student in one call&lt;br /&gt;
* Tasks are grouped by course on the dashboard&lt;br /&gt;
* Each task shows current stage and days remaining, if applicable&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want to view a single assignment's deadlines and details ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt; returns the task plus a unified &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt; timeline&lt;br /&gt;
* The timeline visually distinguishes completed, current, and pending stages&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/team&amp;lt;/code&amp;gt; returns teammates grouped by course&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want my team's score heatgrid to load fast and stay readable while scrolling ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* The heatgrid loads from a single &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt; call&lt;br /&gt;
* The first two columns (item number &amp;quot;&amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;&amp;quot; and Item) remain fixed during horizontal scroll, along with section headers&lt;br /&gt;
* Hovering over a score with a comment shows the corresponding reviewer's feedback as a tooltip&lt;br /&gt;
&lt;br /&gt;
=== A student who isn't permitted to submit or review will not see those task links ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;quot;Your work&amp;quot; is hidden when &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;quot;Others' work&amp;quot; is hidden when &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== As an instructor, I want calculated review grades to reflect each rubric's configured weight ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; weights each round's normalized score by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt; both compute their averages through the same shared &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
=== Student Task Overview ===&lt;br /&gt;
A StudentTask is a task that a student performs in the course of doing an assignment, e.g., forming a team, choosing a topic, submitting work, or reviewing work. No dedicated &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; database table exists. A &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; is a plain Ruby object composed from &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; and its associated &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignedUpTeam&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;ProjectTopic&amp;lt;/code&amp;gt;, and permission fields (&amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;permission_granted&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Request / Response Flow ====&lt;br /&gt;
* Student navigates to &amp;lt;code&amp;gt;/student_tasks&amp;lt;/code&amp;gt; → frontend calls &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTask.tasks(user)&amp;lt;/code&amp;gt; preloads &amp;lt;code&amp;gt;assignment: :course&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;:user&amp;lt;/code&amp;gt; associations, then maps each &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; through &amp;lt;code&amp;gt;StudentTask.create_from_participant&amp;lt;/code&amp;gt;&lt;br /&gt;
* Selecting an assignment navigates to the detail route → frontend calls &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTask.timeline_events&amp;lt;/code&amp;gt; merges the assignment's due dates with timestamps of reviews done of the logged-in user's work into one sorted timeline&lt;br /&gt;
* For the heatgrid: frontend calls &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt; → &amp;lt;code&amp;gt;GradesController#get_team_scores&amp;lt;/code&amp;gt; eager-loads &amp;lt;code&amp;gt;responses: { scores: :item }&amp;lt;/code&amp;gt; and calls &amp;lt;code&amp;gt;insert_section_headers&amp;lt;/code&amp;gt; to splice headers into the displayed review (which is a Response instance).&lt;br /&gt;
&lt;br /&gt;
=== Aggregation of Scores Assigned by a Reviewer ===&lt;br /&gt;
* &amp;lt;code&amp;gt;Response#aggregate_questionnaire_score&amp;lt;/code&amp;gt; — item-weighted score for one submitted response&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; — normalizes a map's score per round and weights it by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score(maps)&amp;lt;/code&amp;gt; — weighted average across multiple maps, the single implementation shared by &amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Backend Implementation ==&lt;br /&gt;
&lt;br /&gt;
=== StudentTasksController ===&lt;br /&gt;
* &amp;lt;code&amp;gt;list&amp;lt;/code&amp;gt; — all &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; objects for the logged-in user&lt;br /&gt;
* &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt; — single task by participant ID with the unified timeline&lt;br /&gt;
* &amp;lt;code&amp;gt;team&amp;lt;/code&amp;gt; — teammates grouped by course name (&amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
=== GradesController — N+1 elimination ===&lt;br /&gt;
&amp;lt;code&amp;gt;get_team_scores&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;get_my_scores_data&amp;lt;/code&amp;gt; eager-load &amp;lt;code&amp;gt;responses: { scores: :item }&amp;lt;/code&amp;gt; on the &amp;lt;code&amp;gt;ReviewResponseMap&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;TeammateReviewResponseMap&amp;lt;/code&amp;gt; queries. &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt; filters SectionHeader scores via Ruby &amp;lt;code&amp;gt;.reject&amp;lt;/code&amp;gt; instead of &amp;lt;code&amp;gt;.joins&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;.where&amp;lt;/code&amp;gt; — calling &amp;lt;code&amp;gt;.where&amp;lt;/code&amp;gt; on an already-preloaded association silently bypasses the cache and re-queries the database. &amp;lt;code&amp;gt;get_answer&amp;lt;/code&amp;gt; now accepts the parent &amp;lt;code&amp;gt;response&amp;lt;/code&amp;gt; directly instead of re-fetching it via &amp;lt;code&amp;gt;score.response&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== insert_section_headers ===&lt;br /&gt;
Splices &amp;lt;code&amp;gt;{ type: &amp;quot;header&amp;quot;, txt: &amp;quot;...&amp;quot; }&amp;lt;/code&amp;gt; sentinel hashes into a round's scores array at positions matching each &amp;lt;code&amp;gt;SectionHeader&amp;lt;/code&amp;gt; item in the questionnaire's sequence, so the frontend can render heading rows between score rows without those rows affecting numbering.&lt;br /&gt;
&lt;br /&gt;
=== Review Grade Calculation ===&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; — latest submitted response per round, normalized (&amp;lt;code&amp;gt;score/max&amp;lt;/code&amp;gt;), weighted by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;ResponseMap.reviewer_reputation_for&amp;lt;/code&amp;gt; — shared weighted-average logic; reviewer reputation currently defaults to &amp;lt;code&amp;gt;1.0&amp;lt;/code&amp;gt; (placeholder for a future review-quality grader integration)&lt;br /&gt;
* &amp;lt;code&amp;gt;Questionnaire#total_item_weight&amp;lt;/code&amp;gt; — pre-calculates the sum of weights of all the scored items in the rubric&lt;br /&gt;
&lt;br /&gt;
=== Comprehensive Testing ===&lt;br /&gt;
* Model specs for &amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;create_from_participant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_events_for_assignment&amp;lt;/code&amp;gt;&lt;br /&gt;
* Request specs for &amp;lt;code&amp;gt;student_tasks/list&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;student_tasks/show/:id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;student_tasks/team&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;grades/:id/view_our_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
* Routing specs verifying the above routes resolve correctly&lt;br /&gt;
&lt;br /&gt;
== Frontend Implementation ==&lt;br /&gt;
[[File:student_view_assignments_page.png|600px|thumb|right|Student View Assignments Page]]&lt;br /&gt;
[[File:student_view_assignment_detail_page.png|600px|thumb|right|Student View Assignment Detail Page]]&lt;br /&gt;
[[File:student_view_assignment_grades_page.png|600px|thumb|right|Student View Assignment View Team Grades]]&lt;br /&gt;
[[File:student_view_assignment_feedback_page.png|600px|thumb|right|Student View Assignment View Feedbacks]]&lt;br /&gt;
=== New/Updated Files ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! File !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTasks.tsx&amp;lt;/code&amp;gt; || Course-grouped assignment dashboard (rewritten from a topic sign-up sheet)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTasksList.tsx&amp;lt;/code&amp;gt; || Sidebar — not-started tasks, revisions, teammates by course&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTaskDetail.tsx&amp;lt;/code&amp;gt; || Per-assignment detail page — timeline, task links, conditional &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; rendering&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ViewTeamGrades.module.scss&amp;lt;/code&amp;gt; || CSS Modules conversion; scoped &amp;lt;code&amp;gt;c1&amp;lt;/code&amp;gt;–&amp;lt;code&amp;gt;c5&amp;lt;/code&amp;gt; color classes, sticky-column and tooltip styles&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewTable.tsx&amp;lt;/code&amp;gt; || Single-call heatgrid; Scores/Feedback toggle; sticky SectionHeader rows&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewTableRow.tsx&amp;lt;/code&amp;gt; || Sticky &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;/Question columns; reduced row height; hover tooltip via &amp;lt;code&amp;gt;data-question&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;FeedbackTable.tsx&amp;lt;/code&amp;gt; || Feedback-mode table mirroring &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt;'s sticky-column layout&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;heatgridUtils.ts&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;RoundRow&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;isHeader()&amp;lt;/code&amp;gt; types; fixed &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; numbering to skip SectionHeader sentinels&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== StudentTasks (List View) ===&lt;br /&gt;
* Single &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; call, grouped by course&lt;br /&gt;
* Fixed: &amp;quot;Assignments&amp;quot; heading was rendering as body text because its class was referenced as a plain string instead of the CSS Modules export (&amp;lt;code&amp;gt;styles['assignments-title']&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
=== StudentTaskDetail (Detail View) ===&lt;br /&gt;
* Visual timeline with progress line and stage nodes (completed/current/pending)&lt;br /&gt;
* Task links list — &amp;quot;Your work&amp;quot; and &amp;quot;Others' work&amp;quot; conditionally rendered based on &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;quot;Send Email To Reviewers&amp;quot; button repositioned to align with the task list rather than floating disconnected at the page top&lt;br /&gt;
&lt;br /&gt;
=== ViewTeamGrades (Heatgrid) ===&lt;br /&gt;
* Consolidated ~6 sequential API calls into one &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
* SectionHeader rows split into a sticky label cell + scrolling spacer cell so they stay fixed during horizontal scroll&lt;br /&gt;
* Score cells show reviewer feedback as a hover tooltip&lt;br /&gt;
* Round headings simplified from &amp;quot;Review (Round: 1 of 2)&amp;quot; to &amp;quot;Round 1&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Design Principles ==&lt;br /&gt;
&lt;br /&gt;
=== Single Responsibility ===&lt;br /&gt;
Score aggregation is layered: &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; owns item-level scoring, &amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt; owns per-map/per-round weighting, &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; owns cross-map averaging — each level only knows about the one below it.&lt;br /&gt;
&lt;br /&gt;
=== Don't Repeat Yourself ===&lt;br /&gt;
The weighted-average logic that was duplicated between &amp;lt;code&amp;gt;AssignmentTeam#aggregate_review_grade&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;ReviewAggregator#compute_average_review_score&amp;lt;/code&amp;gt; was consolidated into a single &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt;, and the now-dead &amp;lt;code&amp;gt;ReviewAggregator&amp;lt;/code&amp;gt; concern was deleted.&lt;br /&gt;
&lt;br /&gt;
=== Performance by Design ===&lt;br /&gt;
Eager loading (&amp;lt;code&amp;gt;.includes&amp;lt;/code&amp;gt;) and in-memory filtering (&amp;lt;code&amp;gt;.reject&amp;lt;/code&amp;gt;) were chosen deliberately over additional database round-trips after profiling showed the heatgrid endpoint firing hundreds of queries under the old implementation.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
&lt;br /&gt;
=== Backend Tests ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! # !! Test !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| 1 || &amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt; groups teammates by course || Returns &amp;lt;code&amp;gt;{course_name =&amp;gt; [fullnames]}&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 2 || &amp;lt;code&amp;gt;all_teammates&amp;lt;/code&amp;gt; excludes calibrated assignments || Calibrated-assignment teammates excluded&lt;br /&gt;
|-&lt;br /&gt;
| 3 || &amp;lt;code&amp;gt;all_teammates&amp;lt;/code&amp;gt; excludes the user themself || Solo team returns empty hash&lt;br /&gt;
|-&lt;br /&gt;
| 4 || &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; returns all tasks for user || 200 OK with task array&lt;br /&gt;
|-&lt;br /&gt;
| 5 || &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt; returns due_dates timeline || 200 OK with &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt; array&lt;br /&gt;
|-&lt;br /&gt;
| 6 || &amp;lt;code&amp;gt;GET /student_tasks/team&amp;lt;/code&amp;gt; groups teammates by course || 200 OK with grouped hash&lt;br /&gt;
|-&lt;br /&gt;
| 7 || Heatgrid &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; column excludes SectionHeaders from numbering || Sequential numbering skips header rows&lt;br /&gt;
|-&lt;br /&gt;
| 8 || &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt; filters SectionHeaders without re-querying || No additional SQL fired against preloaded association&lt;br /&gt;
|-&lt;br /&gt;
| 9 || &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; weight validation || &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt; must be 0 when rubric has no scored questions&lt;br /&gt;
|-&lt;br /&gt;
| 10 || &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; || Weighted average matches Σ(grade × weight) / Σ(weight)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Frontend Tests ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! # !! Test !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| 1 || &amp;lt;code&amp;gt;StudentTasks&amp;lt;/code&amp;gt; renders &amp;quot;Assignments&amp;quot; heading with correct styling || Heading uses module class, renders bold and large&lt;br /&gt;
|-&lt;br /&gt;
| 2 || &amp;lt;code&amp;gt;StudentTaskDetail&amp;lt;/code&amp;gt; hides &amp;quot;Your work&amp;quot; when &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; is false || List item not rendered&lt;br /&gt;
|-&lt;br /&gt;
| 3 || &amp;lt;code&amp;gt;StudentTaskDetail&amp;lt;/code&amp;gt; hides &amp;quot;Others' work&amp;quot; when &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; is false || List item not rendered&lt;br /&gt;
|-&lt;br /&gt;
| 4 || &amp;lt;code&amp;gt;heatgridUtils&amp;lt;/code&amp;gt; itemNumber skips SectionHeader rows || Numbering matches scored items only&lt;br /&gt;
|-&lt;br /&gt;
| 5 || &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt; SectionHeader row stays fixed during horizontal scroll || Header cell &amp;lt;code&amp;gt;position: sticky&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 6 || &amp;lt;code&amp;gt;ReviewTableRow&amp;lt;/code&amp;gt; shows tooltip only when a comment exists || No tooltip box when &amp;lt;code&amp;gt;data-question&amp;lt;/code&amp;gt; is undefined&lt;br /&gt;
|-&lt;br /&gt;
| 7 || Heatgrid column borders render as &amp;lt;code&amp;gt;#ddd&amp;lt;/code&amp;gt; || No black borders after &amp;lt;code&amp;gt;tbl_heat&amp;lt;/code&amp;gt; class applied&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Relevant Links ==&lt;br /&gt;
&lt;br /&gt;
=== Repos ===&lt;br /&gt;
* https://github.com/expertiza/reimplementation-back-end&lt;br /&gt;
* https://github.com/expertiza/reimplementation-front-end&lt;br /&gt;
&lt;br /&gt;
=== Pull Requests ===&lt;br /&gt;
* Backend: https://github.com/expertiza/reimplementation-back-end/pull/352&lt;br /&gt;
* Frontend: https://github.com/expertiza/reimplementation-front-end/pull/182&lt;br /&gt;
&lt;br /&gt;
=== Demo Video ===&lt;br /&gt;
* (add link once recorded)&lt;br /&gt;
&lt;br /&gt;
== Developer Guidance ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Setup &amp;amp; Run (via Docker) ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
docker compose up&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Within the container ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
bundle exec rspec spec/models/student_task_spec.rb&lt;br /&gt;
bundle exec rspec spec/requests/api/v1/student_tasks_controller_spec.rb&lt;br /&gt;
bundle exec rspec spec/requests/api/v1/grades_controller_spec.rb&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
==== Setup &amp;amp; Run ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm install&lt;br /&gt;
npm start&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Running Frontend Tests ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm test -- StudentTaskDetail   # run a single suite&lt;br /&gt;
npm test                        # run full suite&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Future Scope ==&lt;br /&gt;
&lt;br /&gt;
=== Reviewer reputation-weighted grading ===&lt;br /&gt;
Integrate an external review-quality grader (e.g. an LLM-based reviewer grader) so &amp;lt;code&amp;gt;ResponseMap.reviewer_reputation_for&amp;lt;/code&amp;gt; reflects actual review quality instead of the current &amp;lt;code&amp;gt;1.0&amp;lt;/code&amp;gt; placeholder for every reviewer.&lt;br /&gt;
&lt;br /&gt;
=== Email-the-authors integration ===&lt;br /&gt;
Wire &amp;quot;Send Email To Reviewers&amp;quot; to a real backend endpoint instead of the current placeholder route.&lt;br /&gt;
&lt;br /&gt;
=== Dedicated StudentTask abstraction ===&lt;br /&gt;
&amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; remains a composed, non-persisted object. A future dedicated table could centralize derived state and simplify queries if the feature's scope grows further.&lt;br /&gt;
&lt;br /&gt;
== Mentor ==&lt;br /&gt;
[mailto:kgudipe@ncsu.edu Koushik Gudipelly]&lt;br /&gt;
&lt;br /&gt;
== Members ==&lt;br /&gt;
[mailto:rsgopara@ncsu.edu Ravi Goparaju]&lt;br /&gt;
[mailto:xmi@ncsu.edu Xiangjun Mi]&lt;br /&gt;
&lt;br /&gt;
== Completed by ==&lt;br /&gt;
[mailto:blalu@ncsu.edu Bestin Lalu]&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2602._Reimplement_student_task_view&amp;diff=168236</id>
		<title>CSC/ECE 517 Spring 2026 - E2602. Reimplement student task view</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2602._Reimplement_student_task_view&amp;diff=168236"/>
		<updated>2026-07-02T23:48:58Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Score Aggregation Flow */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= CSC/ECE 517 Spring 2026 - E2602. Reimplement Student Task View =&lt;br /&gt;
&lt;br /&gt;
== About Expertiza ==&lt;br /&gt;
Expertiza is a robust, open-source web platform designed to empower educational environments that emphasize project-based learning. It streamlines collaborative activities including peer reviews, surveys, and team assignments — giving instructors tools to manage assignments and track performance, and giving students a single place to submit work, review peers, and track feedback.&lt;br /&gt;
&lt;br /&gt;
== Introduction ==&lt;br /&gt;
This iteration of E2602 hardens and extends the Student Task View: consolidating the student dashboard and detail pages into single-call REST endpoints, eliminating N+1 queries in the peer-review score heatgrid, and fixing several display/permission bugs surfaced by manual QA against real assignment data.&lt;br /&gt;
&lt;br /&gt;
=== Tech Stack ===&lt;br /&gt;
* '''Backend:''' Ruby on Rails (API-only)&lt;br /&gt;
* '''Frontend:''' React + TypeScript&lt;br /&gt;
* '''Database:''' MySQL&lt;br /&gt;
* '''Backend Testing:''' RSpec&lt;br /&gt;
* '''Frontend Testing:''' Jest, React Testing Library&lt;br /&gt;
&lt;br /&gt;
=== Key Improvements ===&lt;br /&gt;
* Eliminated N+1 queries in the score heatgrid (&amp;lt;code&amp;gt;get_team_scores&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_my_scores_data&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_answer&amp;lt;/code&amp;gt;) via &amp;lt;code&amp;gt;.includes(responses: { scores: :item })&amp;lt;/code&amp;gt; and in-memory filtering instead of re-querying preloaded associations&lt;br /&gt;
* Converted &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt;'s stylesheet to CSS Modules, fixing a Bootstrap class collision and scoping color classes (&amp;lt;code&amp;gt;c1&amp;lt;/code&amp;gt;–&amp;lt;code&amp;gt;c5&amp;lt;/code&amp;gt;) correctly&lt;br /&gt;
* Redesigned the score/feedback heatgrid with sticky &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;/Question columns and a sticky, full-width SectionHeader row that stays fixed during horizontal scroll&lt;br /&gt;
* Added hover tooltips on score cells showing the reviewer's written feedback&lt;br /&gt;
* Rewrote &amp;lt;code&amp;gt;StudentTasks&amp;lt;/code&amp;gt; from a (mis-implemented) topic sign-up sheet into a proper course-grouped assignment dashboard&lt;br /&gt;
* Added participant-level &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; permission checks, hiding &amp;quot;Your work&amp;quot; / &amp;quot;Others' work&amp;quot; task links accordingly&lt;br /&gt;
* Refactored review grade calculation: &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; now weights each round by its &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;; &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; centralizes the weighted-average logic previously duplicated across &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Problem Statement ==&lt;br /&gt;
The previous Student Task View had several concrete defects:&lt;br /&gt;
* The score heatgrid fired one SQL query per response/score across all reviewers and rounds, producing hundreds of queries for a single page load&lt;br /&gt;
* SectionHeader rubric rows (non-scorable dividers) were incorrectly counted in the &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; column numbering and lost their fixed position during horizontal scroll&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTasks.tsx&amp;lt;/code&amp;gt; was previously implemented as a topic sign-up sheet, not a task dashboard, and required a full rewrite&lt;br /&gt;
* The page did not respect participant-level &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; flags — all task links were always shown regardless of permission&lt;br /&gt;
* Review grade calculation treated every reviewer and every round equally, ignoring each rubric's configured &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== User Stories ==&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want to see all my assignments in one place ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; returns all tasks for the logged-in student in one call&lt;br /&gt;
* Tasks are grouped by course on the dashboard&lt;br /&gt;
* Each task shows current stage and days remaining, if applicable&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want to view a single assignment's deadlines and details ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt; returns the task plus a unified &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt; timeline&lt;br /&gt;
* The timeline visually distinguishes completed, current, and pending stages&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/team&amp;lt;/code&amp;gt; returns teammates grouped by course&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want my team's score heatgrid to load fast and stay readable while scrolling ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* The heatgrid loads from a single &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt; call&lt;br /&gt;
* The first two columns (item number &amp;quot;&amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;&amp;quot; and Item) remain fixed during horizontal scroll, along with section headers&lt;br /&gt;
* Hovering over a score with a comment shows the corresponding reviewer's feedback as a tooltip&lt;br /&gt;
&lt;br /&gt;
=== A student who isn't permitted to submit or review will not see those task links ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;quot;Your work&amp;quot; is hidden when &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;quot;Others' work&amp;quot; is hidden when &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== As an instructor, I want calculated review grades to reflect each rubric's configured weight ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; weights each round's normalized score by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt; both compute their averages through the same shared &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
=== Student Task Overview ===&lt;br /&gt;
A StudentTask is a task that a student performs in the course of doing an assignment, e.g., forming a team, choosing a topic, submitting work, or reviewing work. No dedicated &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; database table exists. A &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; is a plain Ruby object composed from &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; and its associated &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignedUpTeam&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;ProjectTopic&amp;lt;/code&amp;gt;, and permission fields (&amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;permission_granted&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Request / Response Flow ====&lt;br /&gt;
* Student navigates to &amp;lt;code&amp;gt;/student_tasks&amp;lt;/code&amp;gt; → frontend calls &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTask.tasks(user)&amp;lt;/code&amp;gt; preloads &amp;lt;code&amp;gt;assignment: :course&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;:user&amp;lt;/code&amp;gt; associations, then maps each &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; through &amp;lt;code&amp;gt;StudentTask.create_from_participant&amp;lt;/code&amp;gt;&lt;br /&gt;
* Selecting an assignment navigates to the detail route → frontend calls &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTask.timeline_events&amp;lt;/code&amp;gt; merges the assignment's due dates with timestamps of reviews done of the logged-in user's work into one sorted timeline&lt;br /&gt;
* For the heatgrid: frontend calls &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt; → &amp;lt;code&amp;gt;GradesController#get_team_scores&amp;lt;/code&amp;gt; eager-loads &amp;lt;code&amp;gt;responses: { scores: :item }&amp;lt;/code&amp;gt; and calls &amp;lt;code&amp;gt;insert_section_headers&amp;lt;/code&amp;gt; to splice headers into the displayed review (which is a Response instance).&lt;br /&gt;
&lt;br /&gt;
=== Aggregation of Scores Assigned by a Reviewer ===&lt;br /&gt;
* &amp;lt;code&amp;gt;Response#aggregate_questionnaire_score&amp;lt;/code&amp;gt; — item-weighted score for one submitted response&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; — normalizes a map's score per round and weights it by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score(maps)&amp;lt;/code&amp;gt; — weighted average across multiple maps, the single implementation shared by &amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Backend Implementation ==&lt;br /&gt;
&lt;br /&gt;
=== StudentTasksController ===&lt;br /&gt;
* &amp;lt;code&amp;gt;list&amp;lt;/code&amp;gt; — all &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; objects for the logged-in user&lt;br /&gt;
* &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt; — single task by participant ID with the unified timeline&lt;br /&gt;
* &amp;lt;code&amp;gt;team&amp;lt;/code&amp;gt; — teammates grouped by course name (&amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
=== GradesController — N+1 elimination ===&lt;br /&gt;
&amp;lt;code&amp;gt;get_team_scores&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;get_my_scores_data&amp;lt;/code&amp;gt; eager-load &amp;lt;code&amp;gt;responses: { scores: :item }&amp;lt;/code&amp;gt; on the &amp;lt;code&amp;gt;ReviewResponseMap&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;TeammateReviewResponseMap&amp;lt;/code&amp;gt; queries. &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt; filters SectionHeader scores via Ruby &amp;lt;code&amp;gt;.reject&amp;lt;/code&amp;gt; instead of &amp;lt;code&amp;gt;.joins&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;.where&amp;lt;/code&amp;gt; — calling &amp;lt;code&amp;gt;.where&amp;lt;/code&amp;gt; on an already-preloaded association silently bypasses the cache and re-queries the database. &amp;lt;code&amp;gt;get_answer&amp;lt;/code&amp;gt; now accepts the parent &amp;lt;code&amp;gt;response&amp;lt;/code&amp;gt; directly instead of re-fetching it via &amp;lt;code&amp;gt;score.response&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== insert_section_headers ===&lt;br /&gt;
Splices &amp;lt;code&amp;gt;{ type: &amp;quot;header&amp;quot;, txt: &amp;quot;...&amp;quot; }&amp;lt;/code&amp;gt; sentinel hashes into a round's scores array at positions matching each &amp;lt;code&amp;gt;SectionHeader&amp;lt;/code&amp;gt; item in the questionnaire's sequence, so the frontend can render heading rows between score rows without those rows affecting numbering.&lt;br /&gt;
&lt;br /&gt;
=== Review Grade Calculation ===&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; — latest submitted response per round, normalized (&amp;lt;code&amp;gt;score/max&amp;lt;/code&amp;gt;), weighted by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;ResponseMap.reviewer_reputation_for&amp;lt;/code&amp;gt; — shared weighted-average logic; reviewer reputation currently defaults to &amp;lt;code&amp;gt;1.0&amp;lt;/code&amp;gt; (placeholder for a future review-quality grader integration)&lt;br /&gt;
* &amp;lt;code&amp;gt;Questionnaire#total_item_weight&amp;lt;/code&amp;gt; — pre-calculates the sum of scored item weights, excluding &amp;lt;code&amp;gt;SectionHeader&amp;lt;/code&amp;gt; items&lt;br /&gt;
&lt;br /&gt;
=== Comprehensive Testing ===&lt;br /&gt;
* Model specs for &amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;create_from_participant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_events_for_assignment&amp;lt;/code&amp;gt;&lt;br /&gt;
* Request specs for &amp;lt;code&amp;gt;student_tasks/list&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;student_tasks/show/:id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;student_tasks/team&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;grades/:id/view_our_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
* Routing specs verifying the above routes resolve correctly&lt;br /&gt;
&lt;br /&gt;
== Frontend Implementation ==&lt;br /&gt;
[[File:student_view_assignments_page.png|600px|thumb|right|Student View Assignments Page]]&lt;br /&gt;
[[File:student_view_assignment_detail_page.png|600px|thumb|right|Student View Assignment Detail Page]]&lt;br /&gt;
[[File:student_view_assignment_grades_page.png|600px|thumb|right|Student View Assignment View Team Grades]]&lt;br /&gt;
[[File:student_view_assignment_feedback_page.png|600px|thumb|right|Student View Assignment View Feedbacks]]&lt;br /&gt;
=== New/Updated Files ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! File !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTasks.tsx&amp;lt;/code&amp;gt; || Course-grouped assignment dashboard (rewritten from a topic sign-up sheet)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTasksList.tsx&amp;lt;/code&amp;gt; || Sidebar — not-started tasks, revisions, teammates by course&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTaskDetail.tsx&amp;lt;/code&amp;gt; || Per-assignment detail page — timeline, task links, conditional &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; rendering&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ViewTeamGrades.module.scss&amp;lt;/code&amp;gt; || CSS Modules conversion; scoped &amp;lt;code&amp;gt;c1&amp;lt;/code&amp;gt;–&amp;lt;code&amp;gt;c5&amp;lt;/code&amp;gt; color classes, sticky-column and tooltip styles&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewTable.tsx&amp;lt;/code&amp;gt; || Single-call heatgrid; Scores/Feedback toggle; sticky SectionHeader rows&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewTableRow.tsx&amp;lt;/code&amp;gt; || Sticky &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;/Question columns; reduced row height; hover tooltip via &amp;lt;code&amp;gt;data-question&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;FeedbackTable.tsx&amp;lt;/code&amp;gt; || Feedback-mode table mirroring &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt;'s sticky-column layout&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;heatgridUtils.ts&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;RoundRow&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;isHeader()&amp;lt;/code&amp;gt; types; fixed &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; numbering to skip SectionHeader sentinels&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== StudentTasks (List View) ===&lt;br /&gt;
* Single &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; call, grouped by course&lt;br /&gt;
* Fixed: &amp;quot;Assignments&amp;quot; heading was rendering as body text because its class was referenced as a plain string instead of the CSS Modules export (&amp;lt;code&amp;gt;styles['assignments-title']&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
=== StudentTaskDetail (Detail View) ===&lt;br /&gt;
* Visual timeline with progress line and stage nodes (completed/current/pending)&lt;br /&gt;
* Task links list — &amp;quot;Your work&amp;quot; and &amp;quot;Others' work&amp;quot; conditionally rendered based on &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;quot;Send Email To Reviewers&amp;quot; button repositioned to align with the task list rather than floating disconnected at the page top&lt;br /&gt;
&lt;br /&gt;
=== ViewTeamGrades (Heatgrid) ===&lt;br /&gt;
* Consolidated ~6 sequential API calls into one &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
* SectionHeader rows split into a sticky label cell + scrolling spacer cell so they stay fixed during horizontal scroll&lt;br /&gt;
* Score cells show reviewer feedback as a hover tooltip&lt;br /&gt;
* Round headings simplified from &amp;quot;Review (Round: 1 of 2)&amp;quot; to &amp;quot;Round 1&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Design Principles ==&lt;br /&gt;
&lt;br /&gt;
=== Single Responsibility ===&lt;br /&gt;
Score aggregation is layered: &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; owns item-level scoring, &amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt; owns per-map/per-round weighting, &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; owns cross-map averaging — each level only knows about the one below it.&lt;br /&gt;
&lt;br /&gt;
=== Don't Repeat Yourself ===&lt;br /&gt;
The weighted-average logic that was duplicated between &amp;lt;code&amp;gt;AssignmentTeam#aggregate_review_grade&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;ReviewAggregator#compute_average_review_score&amp;lt;/code&amp;gt; was consolidated into a single &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt;, and the now-dead &amp;lt;code&amp;gt;ReviewAggregator&amp;lt;/code&amp;gt; concern was deleted.&lt;br /&gt;
&lt;br /&gt;
=== Performance by Design ===&lt;br /&gt;
Eager loading (&amp;lt;code&amp;gt;.includes&amp;lt;/code&amp;gt;) and in-memory filtering (&amp;lt;code&amp;gt;.reject&amp;lt;/code&amp;gt;) were chosen deliberately over additional database round-trips after profiling showed the heatgrid endpoint firing hundreds of queries under the old implementation.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
&lt;br /&gt;
=== Backend Tests ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! # !! Test !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| 1 || &amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt; groups teammates by course || Returns &amp;lt;code&amp;gt;{course_name =&amp;gt; [fullnames]}&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 2 || &amp;lt;code&amp;gt;all_teammates&amp;lt;/code&amp;gt; excludes calibrated assignments || Calibrated-assignment teammates excluded&lt;br /&gt;
|-&lt;br /&gt;
| 3 || &amp;lt;code&amp;gt;all_teammates&amp;lt;/code&amp;gt; excludes the user themself || Solo team returns empty hash&lt;br /&gt;
|-&lt;br /&gt;
| 4 || &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; returns all tasks for user || 200 OK with task array&lt;br /&gt;
|-&lt;br /&gt;
| 5 || &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt; returns due_dates timeline || 200 OK with &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt; array&lt;br /&gt;
|-&lt;br /&gt;
| 6 || &amp;lt;code&amp;gt;GET /student_tasks/team&amp;lt;/code&amp;gt; groups teammates by course || 200 OK with grouped hash&lt;br /&gt;
|-&lt;br /&gt;
| 7 || Heatgrid &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; column excludes SectionHeaders from numbering || Sequential numbering skips header rows&lt;br /&gt;
|-&lt;br /&gt;
| 8 || &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt; filters SectionHeaders without re-querying || No additional SQL fired against preloaded association&lt;br /&gt;
|-&lt;br /&gt;
| 9 || &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; weight validation || &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt; must be 0 when rubric has no scored questions&lt;br /&gt;
|-&lt;br /&gt;
| 10 || &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; || Weighted average matches Σ(grade × weight) / Σ(weight)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Frontend Tests ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! # !! Test !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| 1 || &amp;lt;code&amp;gt;StudentTasks&amp;lt;/code&amp;gt; renders &amp;quot;Assignments&amp;quot; heading with correct styling || Heading uses module class, renders bold and large&lt;br /&gt;
|-&lt;br /&gt;
| 2 || &amp;lt;code&amp;gt;StudentTaskDetail&amp;lt;/code&amp;gt; hides &amp;quot;Your work&amp;quot; when &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; is false || List item not rendered&lt;br /&gt;
|-&lt;br /&gt;
| 3 || &amp;lt;code&amp;gt;StudentTaskDetail&amp;lt;/code&amp;gt; hides &amp;quot;Others' work&amp;quot; when &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; is false || List item not rendered&lt;br /&gt;
|-&lt;br /&gt;
| 4 || &amp;lt;code&amp;gt;heatgridUtils&amp;lt;/code&amp;gt; itemNumber skips SectionHeader rows || Numbering matches scored items only&lt;br /&gt;
|-&lt;br /&gt;
| 5 || &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt; SectionHeader row stays fixed during horizontal scroll || Header cell &amp;lt;code&amp;gt;position: sticky&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 6 || &amp;lt;code&amp;gt;ReviewTableRow&amp;lt;/code&amp;gt; shows tooltip only when a comment exists || No tooltip box when &amp;lt;code&amp;gt;data-question&amp;lt;/code&amp;gt; is undefined&lt;br /&gt;
|-&lt;br /&gt;
| 7 || Heatgrid column borders render as &amp;lt;code&amp;gt;#ddd&amp;lt;/code&amp;gt; || No black borders after &amp;lt;code&amp;gt;tbl_heat&amp;lt;/code&amp;gt; class applied&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Relevant Links ==&lt;br /&gt;
&lt;br /&gt;
=== Repos ===&lt;br /&gt;
* https://github.com/expertiza/reimplementation-back-end&lt;br /&gt;
* https://github.com/expertiza/reimplementation-front-end&lt;br /&gt;
&lt;br /&gt;
=== Pull Requests ===&lt;br /&gt;
* Backend: https://github.com/expertiza/reimplementation-back-end/pull/352&lt;br /&gt;
* Frontend: https://github.com/expertiza/reimplementation-front-end/pull/182&lt;br /&gt;
&lt;br /&gt;
=== Demo Video ===&lt;br /&gt;
* (add link once recorded)&lt;br /&gt;
&lt;br /&gt;
== Developer Guidance ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Setup &amp;amp; Run (via Docker) ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
docker compose up&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Within the container ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
bundle exec rspec spec/models/student_task_spec.rb&lt;br /&gt;
bundle exec rspec spec/requests/api/v1/student_tasks_controller_spec.rb&lt;br /&gt;
bundle exec rspec spec/requests/api/v1/grades_controller_spec.rb&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
==== Setup &amp;amp; Run ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm install&lt;br /&gt;
npm start&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Running Frontend Tests ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm test -- StudentTaskDetail   # run a single suite&lt;br /&gt;
npm test                        # run full suite&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Future Scope ==&lt;br /&gt;
&lt;br /&gt;
=== Reviewer reputation-weighted grading ===&lt;br /&gt;
Integrate an external review-quality grader (e.g. an LLM-based reviewer grader) so &amp;lt;code&amp;gt;ResponseMap.reviewer_reputation_for&amp;lt;/code&amp;gt; reflects actual review quality instead of the current &amp;lt;code&amp;gt;1.0&amp;lt;/code&amp;gt; placeholder for every reviewer.&lt;br /&gt;
&lt;br /&gt;
=== Email-the-authors integration ===&lt;br /&gt;
Wire &amp;quot;Send Email To Reviewers&amp;quot; to a real backend endpoint instead of the current placeholder route.&lt;br /&gt;
&lt;br /&gt;
=== Dedicated StudentTask abstraction ===&lt;br /&gt;
&amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; remains a composed, non-persisted object. A future dedicated table could centralize derived state and simplify queries if the feature's scope grows further.&lt;br /&gt;
&lt;br /&gt;
== Mentor ==&lt;br /&gt;
[mailto:kgudipe@ncsu.edu Koushik Gudipelly]&lt;br /&gt;
&lt;br /&gt;
== Members ==&lt;br /&gt;
[mailto:rsgopara@ncsu.edu Ravi Goparaju]&lt;br /&gt;
[mailto:xmi@ncsu.edu Xiangjun Mi]&lt;br /&gt;
&lt;br /&gt;
== Completed by ==&lt;br /&gt;
[mailto:blalu@ncsu.edu Bestin Lalu]&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2602._Reimplement_student_task_view&amp;diff=168235</id>
		<title>CSC/ECE 517 Spring 2026 - E2602. Reimplement student task view</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2602._Reimplement_student_task_view&amp;diff=168235"/>
		<updated>2026-07-02T23:45:44Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Request / Response Flow */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= CSC/ECE 517 Spring 2026 - E2602. Reimplement Student Task View =&lt;br /&gt;
&lt;br /&gt;
== About Expertiza ==&lt;br /&gt;
Expertiza is a robust, open-source web platform designed to empower educational environments that emphasize project-based learning. It streamlines collaborative activities including peer reviews, surveys, and team assignments — giving instructors tools to manage assignments and track performance, and giving students a single place to submit work, review peers, and track feedback.&lt;br /&gt;
&lt;br /&gt;
== Introduction ==&lt;br /&gt;
This iteration of E2602 hardens and extends the Student Task View: consolidating the student dashboard and detail pages into single-call REST endpoints, eliminating N+1 queries in the peer-review score heatgrid, and fixing several display/permission bugs surfaced by manual QA against real assignment data.&lt;br /&gt;
&lt;br /&gt;
=== Tech Stack ===&lt;br /&gt;
* '''Backend:''' Ruby on Rails (API-only)&lt;br /&gt;
* '''Frontend:''' React + TypeScript&lt;br /&gt;
* '''Database:''' MySQL&lt;br /&gt;
* '''Backend Testing:''' RSpec&lt;br /&gt;
* '''Frontend Testing:''' Jest, React Testing Library&lt;br /&gt;
&lt;br /&gt;
=== Key Improvements ===&lt;br /&gt;
* Eliminated N+1 queries in the score heatgrid (&amp;lt;code&amp;gt;get_team_scores&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_my_scores_data&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_answer&amp;lt;/code&amp;gt;) via &amp;lt;code&amp;gt;.includes(responses: { scores: :item })&amp;lt;/code&amp;gt; and in-memory filtering instead of re-querying preloaded associations&lt;br /&gt;
* Converted &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt;'s stylesheet to CSS Modules, fixing a Bootstrap class collision and scoping color classes (&amp;lt;code&amp;gt;c1&amp;lt;/code&amp;gt;–&amp;lt;code&amp;gt;c5&amp;lt;/code&amp;gt;) correctly&lt;br /&gt;
* Redesigned the score/feedback heatgrid with sticky &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;/Question columns and a sticky, full-width SectionHeader row that stays fixed during horizontal scroll&lt;br /&gt;
* Added hover tooltips on score cells showing the reviewer's written feedback&lt;br /&gt;
* Rewrote &amp;lt;code&amp;gt;StudentTasks&amp;lt;/code&amp;gt; from a (mis-implemented) topic sign-up sheet into a proper course-grouped assignment dashboard&lt;br /&gt;
* Added participant-level &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; permission checks, hiding &amp;quot;Your work&amp;quot; / &amp;quot;Others' work&amp;quot; task links accordingly&lt;br /&gt;
* Refactored review grade calculation: &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; now weights each round by its &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;; &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; centralizes the weighted-average logic previously duplicated across &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Problem Statement ==&lt;br /&gt;
The previous Student Task View had several concrete defects:&lt;br /&gt;
* The score heatgrid fired one SQL query per response/score across all reviewers and rounds, producing hundreds of queries for a single page load&lt;br /&gt;
* SectionHeader rubric rows (non-scorable dividers) were incorrectly counted in the &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; column numbering and lost their fixed position during horizontal scroll&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTasks.tsx&amp;lt;/code&amp;gt; was previously implemented as a topic sign-up sheet, not a task dashboard, and required a full rewrite&lt;br /&gt;
* The page did not respect participant-level &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; flags — all task links were always shown regardless of permission&lt;br /&gt;
* Review grade calculation treated every reviewer and every round equally, ignoring each rubric's configured &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== User Stories ==&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want to see all my assignments in one place ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; returns all tasks for the logged-in student in one call&lt;br /&gt;
* Tasks are grouped by course on the dashboard&lt;br /&gt;
* Each task shows current stage and days remaining, if applicable&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want to view a single assignment's deadlines and details ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt; returns the task plus a unified &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt; timeline&lt;br /&gt;
* The timeline visually distinguishes completed, current, and pending stages&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/team&amp;lt;/code&amp;gt; returns teammates grouped by course&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want my team's score heatgrid to load fast and stay readable while scrolling ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* The heatgrid loads from a single &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt; call&lt;br /&gt;
* The first two columns (item number &amp;quot;&amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;&amp;quot; and Item) remain fixed during horizontal scroll, along with section headers&lt;br /&gt;
* Hovering over a score with a comment shows the corresponding reviewer's feedback as a tooltip&lt;br /&gt;
&lt;br /&gt;
=== A student who isn't permitted to submit or review will not see those task links ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;quot;Your work&amp;quot; is hidden when &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;quot;Others' work&amp;quot; is hidden when &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== As an instructor, I want calculated review grades to reflect each rubric's configured weight ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; weights each round's normalized score by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt; both compute their averages through the same shared &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
=== Student Task Overview ===&lt;br /&gt;
A StudentTask is a task that a student performs in the course of doing an assignment, e.g., forming a team, choosing a topic, submitting work, or reviewing work. No dedicated &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; database table exists. A &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; is a plain Ruby object composed from &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; and its associated &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignedUpTeam&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;ProjectTopic&amp;lt;/code&amp;gt;, and permission fields (&amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;permission_granted&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Request / Response Flow ====&lt;br /&gt;
* Student navigates to &amp;lt;code&amp;gt;/student_tasks&amp;lt;/code&amp;gt; → frontend calls &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTask.tasks(user)&amp;lt;/code&amp;gt; preloads &amp;lt;code&amp;gt;assignment: :course&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;:user&amp;lt;/code&amp;gt; associations, then maps each &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; through &amp;lt;code&amp;gt;StudentTask.create_from_participant&amp;lt;/code&amp;gt;&lt;br /&gt;
* Selecting an assignment navigates to the detail route → frontend calls &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTask.timeline_events&amp;lt;/code&amp;gt; merges the assignment's due dates with timestamps of reviews done of the logged-in user's work into one sorted timeline&lt;br /&gt;
* For the heatgrid: frontend calls &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt; → &amp;lt;code&amp;gt;GradesController#get_team_scores&amp;lt;/code&amp;gt; eager-loads &amp;lt;code&amp;gt;responses: { scores: :item }&amp;lt;/code&amp;gt; and calls &amp;lt;code&amp;gt;insert_section_headers&amp;lt;/code&amp;gt; to splice headers into the displayed review (which is a Response instance).&lt;br /&gt;
&lt;br /&gt;
=== Score Aggregation Flow ===&lt;br /&gt;
* &amp;lt;code&amp;gt;Response#aggregate_questionnaire_score&amp;lt;/code&amp;gt; — item-weighted score for one submitted response&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; — normalizes a map's score per round and weights it by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score(maps)&amp;lt;/code&amp;gt; — weighted average across multiple maps, the single implementation shared by &amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Backend Implementation ==&lt;br /&gt;
&lt;br /&gt;
=== StudentTasksController ===&lt;br /&gt;
* &amp;lt;code&amp;gt;list&amp;lt;/code&amp;gt; — all &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; objects for the logged-in user&lt;br /&gt;
* &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt; — single task by participant ID with the unified timeline&lt;br /&gt;
* &amp;lt;code&amp;gt;team&amp;lt;/code&amp;gt; — teammates grouped by course name (&amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
=== GradesController — N+1 elimination ===&lt;br /&gt;
&amp;lt;code&amp;gt;get_team_scores&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;get_my_scores_data&amp;lt;/code&amp;gt; eager-load &amp;lt;code&amp;gt;responses: { scores: :item }&amp;lt;/code&amp;gt; on the &amp;lt;code&amp;gt;ReviewResponseMap&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;TeammateReviewResponseMap&amp;lt;/code&amp;gt; queries. &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt; filters SectionHeader scores via Ruby &amp;lt;code&amp;gt;.reject&amp;lt;/code&amp;gt; instead of &amp;lt;code&amp;gt;.joins&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;.where&amp;lt;/code&amp;gt; — calling &amp;lt;code&amp;gt;.where&amp;lt;/code&amp;gt; on an already-preloaded association silently bypasses the cache and re-queries the database. &amp;lt;code&amp;gt;get_answer&amp;lt;/code&amp;gt; now accepts the parent &amp;lt;code&amp;gt;response&amp;lt;/code&amp;gt; directly instead of re-fetching it via &amp;lt;code&amp;gt;score.response&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== insert_section_headers ===&lt;br /&gt;
Splices &amp;lt;code&amp;gt;{ type: &amp;quot;header&amp;quot;, txt: &amp;quot;...&amp;quot; }&amp;lt;/code&amp;gt; sentinel hashes into a round's scores array at positions matching each &amp;lt;code&amp;gt;SectionHeader&amp;lt;/code&amp;gt; item in the questionnaire's sequence, so the frontend can render heading rows between score rows without those rows affecting numbering.&lt;br /&gt;
&lt;br /&gt;
=== Review Grade Calculation ===&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; — latest submitted response per round, normalized (&amp;lt;code&amp;gt;score/max&amp;lt;/code&amp;gt;), weighted by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;ResponseMap.reviewer_reputation_for&amp;lt;/code&amp;gt; — shared weighted-average logic; reviewer reputation currently defaults to &amp;lt;code&amp;gt;1.0&amp;lt;/code&amp;gt; (placeholder for a future review-quality grader integration)&lt;br /&gt;
* &amp;lt;code&amp;gt;Questionnaire#total_item_weight&amp;lt;/code&amp;gt; — pre-calculates the sum of scored item weights, excluding &amp;lt;code&amp;gt;SectionHeader&amp;lt;/code&amp;gt; items&lt;br /&gt;
&lt;br /&gt;
=== Comprehensive Testing ===&lt;br /&gt;
* Model specs for &amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;create_from_participant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_events_for_assignment&amp;lt;/code&amp;gt;&lt;br /&gt;
* Request specs for &amp;lt;code&amp;gt;student_tasks/list&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;student_tasks/show/:id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;student_tasks/team&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;grades/:id/view_our_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
* Routing specs verifying the above routes resolve correctly&lt;br /&gt;
&lt;br /&gt;
== Frontend Implementation ==&lt;br /&gt;
[[File:student_view_assignments_page.png|600px|thumb|right|Student View Assignments Page]]&lt;br /&gt;
[[File:student_view_assignment_detail_page.png|600px|thumb|right|Student View Assignment Detail Page]]&lt;br /&gt;
[[File:student_view_assignment_grades_page.png|600px|thumb|right|Student View Assignment View Team Grades]]&lt;br /&gt;
[[File:student_view_assignment_feedback_page.png|600px|thumb|right|Student View Assignment View Feedbacks]]&lt;br /&gt;
=== New/Updated Files ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! File !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTasks.tsx&amp;lt;/code&amp;gt; || Course-grouped assignment dashboard (rewritten from a topic sign-up sheet)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTasksList.tsx&amp;lt;/code&amp;gt; || Sidebar — not-started tasks, revisions, teammates by course&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTaskDetail.tsx&amp;lt;/code&amp;gt; || Per-assignment detail page — timeline, task links, conditional &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; rendering&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ViewTeamGrades.module.scss&amp;lt;/code&amp;gt; || CSS Modules conversion; scoped &amp;lt;code&amp;gt;c1&amp;lt;/code&amp;gt;–&amp;lt;code&amp;gt;c5&amp;lt;/code&amp;gt; color classes, sticky-column and tooltip styles&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewTable.tsx&amp;lt;/code&amp;gt; || Single-call heatgrid; Scores/Feedback toggle; sticky SectionHeader rows&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewTableRow.tsx&amp;lt;/code&amp;gt; || Sticky &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;/Question columns; reduced row height; hover tooltip via &amp;lt;code&amp;gt;data-question&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;FeedbackTable.tsx&amp;lt;/code&amp;gt; || Feedback-mode table mirroring &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt;'s sticky-column layout&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;heatgridUtils.ts&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;RoundRow&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;isHeader()&amp;lt;/code&amp;gt; types; fixed &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; numbering to skip SectionHeader sentinels&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== StudentTasks (List View) ===&lt;br /&gt;
* Single &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; call, grouped by course&lt;br /&gt;
* Fixed: &amp;quot;Assignments&amp;quot; heading was rendering as body text because its class was referenced as a plain string instead of the CSS Modules export (&amp;lt;code&amp;gt;styles['assignments-title']&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
=== StudentTaskDetail (Detail View) ===&lt;br /&gt;
* Visual timeline with progress line and stage nodes (completed/current/pending)&lt;br /&gt;
* Task links list — &amp;quot;Your work&amp;quot; and &amp;quot;Others' work&amp;quot; conditionally rendered based on &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;quot;Send Email To Reviewers&amp;quot; button repositioned to align with the task list rather than floating disconnected at the page top&lt;br /&gt;
&lt;br /&gt;
=== ViewTeamGrades (Heatgrid) ===&lt;br /&gt;
* Consolidated ~6 sequential API calls into one &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
* SectionHeader rows split into a sticky label cell + scrolling spacer cell so they stay fixed during horizontal scroll&lt;br /&gt;
* Score cells show reviewer feedback as a hover tooltip&lt;br /&gt;
* Round headings simplified from &amp;quot;Review (Round: 1 of 2)&amp;quot; to &amp;quot;Round 1&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Design Principles ==&lt;br /&gt;
&lt;br /&gt;
=== Single Responsibility ===&lt;br /&gt;
Score aggregation is layered: &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; owns item-level scoring, &amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt; owns per-map/per-round weighting, &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; owns cross-map averaging — each level only knows about the one below it.&lt;br /&gt;
&lt;br /&gt;
=== Don't Repeat Yourself ===&lt;br /&gt;
The weighted-average logic that was duplicated between &amp;lt;code&amp;gt;AssignmentTeam#aggregate_review_grade&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;ReviewAggregator#compute_average_review_score&amp;lt;/code&amp;gt; was consolidated into a single &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt;, and the now-dead &amp;lt;code&amp;gt;ReviewAggregator&amp;lt;/code&amp;gt; concern was deleted.&lt;br /&gt;
&lt;br /&gt;
=== Performance by Design ===&lt;br /&gt;
Eager loading (&amp;lt;code&amp;gt;.includes&amp;lt;/code&amp;gt;) and in-memory filtering (&amp;lt;code&amp;gt;.reject&amp;lt;/code&amp;gt;) were chosen deliberately over additional database round-trips after profiling showed the heatgrid endpoint firing hundreds of queries under the old implementation.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
&lt;br /&gt;
=== Backend Tests ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! # !! Test !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| 1 || &amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt; groups teammates by course || Returns &amp;lt;code&amp;gt;{course_name =&amp;gt; [fullnames]}&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 2 || &amp;lt;code&amp;gt;all_teammates&amp;lt;/code&amp;gt; excludes calibrated assignments || Calibrated-assignment teammates excluded&lt;br /&gt;
|-&lt;br /&gt;
| 3 || &amp;lt;code&amp;gt;all_teammates&amp;lt;/code&amp;gt; excludes the user themself || Solo team returns empty hash&lt;br /&gt;
|-&lt;br /&gt;
| 4 || &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; returns all tasks for user || 200 OK with task array&lt;br /&gt;
|-&lt;br /&gt;
| 5 || &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt; returns due_dates timeline || 200 OK with &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt; array&lt;br /&gt;
|-&lt;br /&gt;
| 6 || &amp;lt;code&amp;gt;GET /student_tasks/team&amp;lt;/code&amp;gt; groups teammates by course || 200 OK with grouped hash&lt;br /&gt;
|-&lt;br /&gt;
| 7 || Heatgrid &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; column excludes SectionHeaders from numbering || Sequential numbering skips header rows&lt;br /&gt;
|-&lt;br /&gt;
| 8 || &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt; filters SectionHeaders without re-querying || No additional SQL fired against preloaded association&lt;br /&gt;
|-&lt;br /&gt;
| 9 || &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; weight validation || &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt; must be 0 when rubric has no scored questions&lt;br /&gt;
|-&lt;br /&gt;
| 10 || &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; || Weighted average matches Σ(grade × weight) / Σ(weight)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Frontend Tests ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! # !! Test !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| 1 || &amp;lt;code&amp;gt;StudentTasks&amp;lt;/code&amp;gt; renders &amp;quot;Assignments&amp;quot; heading with correct styling || Heading uses module class, renders bold and large&lt;br /&gt;
|-&lt;br /&gt;
| 2 || &amp;lt;code&amp;gt;StudentTaskDetail&amp;lt;/code&amp;gt; hides &amp;quot;Your work&amp;quot; when &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; is false || List item not rendered&lt;br /&gt;
|-&lt;br /&gt;
| 3 || &amp;lt;code&amp;gt;StudentTaskDetail&amp;lt;/code&amp;gt; hides &amp;quot;Others' work&amp;quot; when &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; is false || List item not rendered&lt;br /&gt;
|-&lt;br /&gt;
| 4 || &amp;lt;code&amp;gt;heatgridUtils&amp;lt;/code&amp;gt; itemNumber skips SectionHeader rows || Numbering matches scored items only&lt;br /&gt;
|-&lt;br /&gt;
| 5 || &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt; SectionHeader row stays fixed during horizontal scroll || Header cell &amp;lt;code&amp;gt;position: sticky&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 6 || &amp;lt;code&amp;gt;ReviewTableRow&amp;lt;/code&amp;gt; shows tooltip only when a comment exists || No tooltip box when &amp;lt;code&amp;gt;data-question&amp;lt;/code&amp;gt; is undefined&lt;br /&gt;
|-&lt;br /&gt;
| 7 || Heatgrid column borders render as &amp;lt;code&amp;gt;#ddd&amp;lt;/code&amp;gt; || No black borders after &amp;lt;code&amp;gt;tbl_heat&amp;lt;/code&amp;gt; class applied&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Relevant Links ==&lt;br /&gt;
&lt;br /&gt;
=== Repos ===&lt;br /&gt;
* https://github.com/expertiza/reimplementation-back-end&lt;br /&gt;
* https://github.com/expertiza/reimplementation-front-end&lt;br /&gt;
&lt;br /&gt;
=== Pull Requests ===&lt;br /&gt;
* Backend: https://github.com/expertiza/reimplementation-back-end/pull/352&lt;br /&gt;
* Frontend: https://github.com/expertiza/reimplementation-front-end/pull/182&lt;br /&gt;
&lt;br /&gt;
=== Demo Video ===&lt;br /&gt;
* (add link once recorded)&lt;br /&gt;
&lt;br /&gt;
== Developer Guidance ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Setup &amp;amp; Run (via Docker) ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
docker compose up&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Within the container ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
bundle exec rspec spec/models/student_task_spec.rb&lt;br /&gt;
bundle exec rspec spec/requests/api/v1/student_tasks_controller_spec.rb&lt;br /&gt;
bundle exec rspec spec/requests/api/v1/grades_controller_spec.rb&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
==== Setup &amp;amp; Run ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm install&lt;br /&gt;
npm start&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Running Frontend Tests ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm test -- StudentTaskDetail   # run a single suite&lt;br /&gt;
npm test                        # run full suite&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Future Scope ==&lt;br /&gt;
&lt;br /&gt;
=== Reviewer reputation-weighted grading ===&lt;br /&gt;
Integrate an external review-quality grader (e.g. an LLM-based reviewer grader) so &amp;lt;code&amp;gt;ResponseMap.reviewer_reputation_for&amp;lt;/code&amp;gt; reflects actual review quality instead of the current &amp;lt;code&amp;gt;1.0&amp;lt;/code&amp;gt; placeholder for every reviewer.&lt;br /&gt;
&lt;br /&gt;
=== Email-the-authors integration ===&lt;br /&gt;
Wire &amp;quot;Send Email To Reviewers&amp;quot; to a real backend endpoint instead of the current placeholder route.&lt;br /&gt;
&lt;br /&gt;
=== Dedicated StudentTask abstraction ===&lt;br /&gt;
&amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; remains a composed, non-persisted object. A future dedicated table could centralize derived state and simplify queries if the feature's scope grows further.&lt;br /&gt;
&lt;br /&gt;
== Mentor ==&lt;br /&gt;
[mailto:kgudipe@ncsu.edu Koushik Gudipelly]&lt;br /&gt;
&lt;br /&gt;
== Members ==&lt;br /&gt;
[mailto:rsgopara@ncsu.edu Ravi Goparaju]&lt;br /&gt;
[mailto:xmi@ncsu.edu Xiangjun Mi]&lt;br /&gt;
&lt;br /&gt;
== Completed by ==&lt;br /&gt;
[mailto:blalu@ncsu.edu Bestin Lalu]&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2602._Reimplement_student_task_view&amp;diff=168234</id>
		<title>CSC/ECE 517 Spring 2026 - E2602. Reimplement student task view</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2602._Reimplement_student_task_view&amp;diff=168234"/>
		<updated>2026-07-02T23:38:53Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Request / Response Flow */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= CSC/ECE 517 Spring 2026 - E2602. Reimplement Student Task View =&lt;br /&gt;
&lt;br /&gt;
== About Expertiza ==&lt;br /&gt;
Expertiza is a robust, open-source web platform designed to empower educational environments that emphasize project-based learning. It streamlines collaborative activities including peer reviews, surveys, and team assignments — giving instructors tools to manage assignments and track performance, and giving students a single place to submit work, review peers, and track feedback.&lt;br /&gt;
&lt;br /&gt;
== Introduction ==&lt;br /&gt;
This iteration of E2602 hardens and extends the Student Task View: consolidating the student dashboard and detail pages into single-call REST endpoints, eliminating N+1 queries in the peer-review score heatgrid, and fixing several display/permission bugs surfaced by manual QA against real assignment data.&lt;br /&gt;
&lt;br /&gt;
=== Tech Stack ===&lt;br /&gt;
* '''Backend:''' Ruby on Rails (API-only)&lt;br /&gt;
* '''Frontend:''' React + TypeScript&lt;br /&gt;
* '''Database:''' MySQL&lt;br /&gt;
* '''Backend Testing:''' RSpec&lt;br /&gt;
* '''Frontend Testing:''' Jest, React Testing Library&lt;br /&gt;
&lt;br /&gt;
=== Key Improvements ===&lt;br /&gt;
* Eliminated N+1 queries in the score heatgrid (&amp;lt;code&amp;gt;get_team_scores&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_my_scores_data&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_answer&amp;lt;/code&amp;gt;) via &amp;lt;code&amp;gt;.includes(responses: { scores: :item })&amp;lt;/code&amp;gt; and in-memory filtering instead of re-querying preloaded associations&lt;br /&gt;
* Converted &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt;'s stylesheet to CSS Modules, fixing a Bootstrap class collision and scoping color classes (&amp;lt;code&amp;gt;c1&amp;lt;/code&amp;gt;–&amp;lt;code&amp;gt;c5&amp;lt;/code&amp;gt;) correctly&lt;br /&gt;
* Redesigned the score/feedback heatgrid with sticky &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;/Question columns and a sticky, full-width SectionHeader row that stays fixed during horizontal scroll&lt;br /&gt;
* Added hover tooltips on score cells showing the reviewer's written feedback&lt;br /&gt;
* Rewrote &amp;lt;code&amp;gt;StudentTasks&amp;lt;/code&amp;gt; from a (mis-implemented) topic sign-up sheet into a proper course-grouped assignment dashboard&lt;br /&gt;
* Added participant-level &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; permission checks, hiding &amp;quot;Your work&amp;quot; / &amp;quot;Others' work&amp;quot; task links accordingly&lt;br /&gt;
* Refactored review grade calculation: &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; now weights each round by its &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;; &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; centralizes the weighted-average logic previously duplicated across &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Problem Statement ==&lt;br /&gt;
The previous Student Task View had several concrete defects:&lt;br /&gt;
* The score heatgrid fired one SQL query per response/score across all reviewers and rounds, producing hundreds of queries for a single page load&lt;br /&gt;
* SectionHeader rubric rows (non-scorable dividers) were incorrectly counted in the &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; column numbering and lost their fixed position during horizontal scroll&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTasks.tsx&amp;lt;/code&amp;gt; was previously implemented as a topic sign-up sheet, not a task dashboard, and required a full rewrite&lt;br /&gt;
* The page did not respect participant-level &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; flags — all task links were always shown regardless of permission&lt;br /&gt;
* Review grade calculation treated every reviewer and every round equally, ignoring each rubric's configured &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== User Stories ==&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want to see all my assignments in one place ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; returns all tasks for the logged-in student in one call&lt;br /&gt;
* Tasks are grouped by course on the dashboard&lt;br /&gt;
* Each task shows current stage and days remaining, if applicable&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want to view a single assignment's deadlines and details ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt; returns the task plus a unified &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt; timeline&lt;br /&gt;
* The timeline visually distinguishes completed, current, and pending stages&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/team&amp;lt;/code&amp;gt; returns teammates grouped by course&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want my team's score heatgrid to load fast and stay readable while scrolling ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* The heatgrid loads from a single &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt; call&lt;br /&gt;
* The first two columns (item number &amp;quot;&amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;&amp;quot; and Item) remain fixed during horizontal scroll, along with section headers&lt;br /&gt;
* Hovering over a score with a comment shows the corresponding reviewer's feedback as a tooltip&lt;br /&gt;
&lt;br /&gt;
=== A student who isn't permitted to submit or review will not see those task links ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;quot;Your work&amp;quot; is hidden when &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;quot;Others' work&amp;quot; is hidden when &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== As an instructor, I want calculated review grades to reflect each rubric's configured weight ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; weights each round's normalized score by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt; both compute their averages through the same shared &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
=== Student Task Overview ===&lt;br /&gt;
A StudentTask is a task that a student performs in the course of doing an assignment, e.g., forming a team, choosing a topic, submitting work, or reviewing work. No dedicated &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; database table exists. A &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; is a plain Ruby object composed from &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; and its associated &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignedUpTeam&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;ProjectTopic&amp;lt;/code&amp;gt;, and permission fields (&amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;permission_granted&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Request / Response Flow ====&lt;br /&gt;
* Student navigates to &amp;lt;code&amp;gt;/student_tasks&amp;lt;/code&amp;gt; → frontend calls &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTask.tasks(user)&amp;lt;/code&amp;gt; preloads &amp;lt;code&amp;gt;assignment: :course&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;:user&amp;lt;/code&amp;gt; associations, then maps each &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; through &amp;lt;code&amp;gt;StudentTask.create_from_participant&amp;lt;/code&amp;gt;&lt;br /&gt;
* Selecting an assignment navigates to the detail route → frontend calls &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTask.timeline_events&amp;lt;/code&amp;gt; merges the assignment's due dates with timestamps of reviews done of the logged-in user's work into one sorted timeline&lt;br /&gt;
* For the heatgrid: frontend calls &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt; → &amp;lt;code&amp;gt;GradesController#get_team_scores&amp;lt;/code&amp;gt; eager-loads &amp;lt;code&amp;gt;responses: { scores: :item }&amp;lt;/code&amp;gt; and calls &amp;lt;code&amp;gt;insert_section_headers&amp;lt;/code&amp;gt; to splice header sentinels into the response&lt;br /&gt;
&lt;br /&gt;
=== Score Aggregation Flow ===&lt;br /&gt;
* &amp;lt;code&amp;gt;Response#aggregate_questionnaire_score&amp;lt;/code&amp;gt; — item-weighted score for one submitted response&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; — normalizes a map's score per round and weights it by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score(maps)&amp;lt;/code&amp;gt; — weighted average across multiple maps, the single implementation shared by &amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Backend Implementation ==&lt;br /&gt;
&lt;br /&gt;
=== StudentTasksController ===&lt;br /&gt;
* &amp;lt;code&amp;gt;list&amp;lt;/code&amp;gt; — all &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; objects for the logged-in user&lt;br /&gt;
* &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt; — single task by participant ID with the unified timeline&lt;br /&gt;
* &amp;lt;code&amp;gt;team&amp;lt;/code&amp;gt; — teammates grouped by course name (&amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
=== GradesController — N+1 elimination ===&lt;br /&gt;
&amp;lt;code&amp;gt;get_team_scores&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;get_my_scores_data&amp;lt;/code&amp;gt; eager-load &amp;lt;code&amp;gt;responses: { scores: :item }&amp;lt;/code&amp;gt; on the &amp;lt;code&amp;gt;ReviewResponseMap&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;TeammateReviewResponseMap&amp;lt;/code&amp;gt; queries. &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt; filters SectionHeader scores via Ruby &amp;lt;code&amp;gt;.reject&amp;lt;/code&amp;gt; instead of &amp;lt;code&amp;gt;.joins&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;.where&amp;lt;/code&amp;gt; — calling &amp;lt;code&amp;gt;.where&amp;lt;/code&amp;gt; on an already-preloaded association silently bypasses the cache and re-queries the database. &amp;lt;code&amp;gt;get_answer&amp;lt;/code&amp;gt; now accepts the parent &amp;lt;code&amp;gt;response&amp;lt;/code&amp;gt; directly instead of re-fetching it via &amp;lt;code&amp;gt;score.response&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== insert_section_headers ===&lt;br /&gt;
Splices &amp;lt;code&amp;gt;{ type: &amp;quot;header&amp;quot;, txt: &amp;quot;...&amp;quot; }&amp;lt;/code&amp;gt; sentinel hashes into a round's scores array at positions matching each &amp;lt;code&amp;gt;SectionHeader&amp;lt;/code&amp;gt; item in the questionnaire's sequence, so the frontend can render heading rows between score rows without those rows affecting numbering.&lt;br /&gt;
&lt;br /&gt;
=== Review Grade Calculation ===&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; — latest submitted response per round, normalized (&amp;lt;code&amp;gt;score/max&amp;lt;/code&amp;gt;), weighted by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;ResponseMap.reviewer_reputation_for&amp;lt;/code&amp;gt; — shared weighted-average logic; reviewer reputation currently defaults to &amp;lt;code&amp;gt;1.0&amp;lt;/code&amp;gt; (placeholder for a future review-quality grader integration)&lt;br /&gt;
* &amp;lt;code&amp;gt;Questionnaire#total_item_weight&amp;lt;/code&amp;gt; — pre-calculates the sum of scored item weights, excluding &amp;lt;code&amp;gt;SectionHeader&amp;lt;/code&amp;gt; items&lt;br /&gt;
&lt;br /&gt;
=== Comprehensive Testing ===&lt;br /&gt;
* Model specs for &amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;create_from_participant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_events_for_assignment&amp;lt;/code&amp;gt;&lt;br /&gt;
* Request specs for &amp;lt;code&amp;gt;student_tasks/list&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;student_tasks/show/:id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;student_tasks/team&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;grades/:id/view_our_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
* Routing specs verifying the above routes resolve correctly&lt;br /&gt;
&lt;br /&gt;
== Frontend Implementation ==&lt;br /&gt;
[[File:student_view_assignments_page.png|600px|thumb|right|Student View Assignments Page]]&lt;br /&gt;
[[File:student_view_assignment_detail_page.png|600px|thumb|right|Student View Assignment Detail Page]]&lt;br /&gt;
[[File:student_view_assignment_grades_page.png|600px|thumb|right|Student View Assignment View Team Grades]]&lt;br /&gt;
[[File:student_view_assignment_feedback_page.png|600px|thumb|right|Student View Assignment View Feedbacks]]&lt;br /&gt;
=== New/Updated Files ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! File !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTasks.tsx&amp;lt;/code&amp;gt; || Course-grouped assignment dashboard (rewritten from a topic sign-up sheet)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTasksList.tsx&amp;lt;/code&amp;gt; || Sidebar — not-started tasks, revisions, teammates by course&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTaskDetail.tsx&amp;lt;/code&amp;gt; || Per-assignment detail page — timeline, task links, conditional &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; rendering&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ViewTeamGrades.module.scss&amp;lt;/code&amp;gt; || CSS Modules conversion; scoped &amp;lt;code&amp;gt;c1&amp;lt;/code&amp;gt;–&amp;lt;code&amp;gt;c5&amp;lt;/code&amp;gt; color classes, sticky-column and tooltip styles&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewTable.tsx&amp;lt;/code&amp;gt; || Single-call heatgrid; Scores/Feedback toggle; sticky SectionHeader rows&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewTableRow.tsx&amp;lt;/code&amp;gt; || Sticky &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;/Question columns; reduced row height; hover tooltip via &amp;lt;code&amp;gt;data-question&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;FeedbackTable.tsx&amp;lt;/code&amp;gt; || Feedback-mode table mirroring &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt;'s sticky-column layout&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;heatgridUtils.ts&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;RoundRow&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;isHeader()&amp;lt;/code&amp;gt; types; fixed &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; numbering to skip SectionHeader sentinels&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== StudentTasks (List View) ===&lt;br /&gt;
* Single &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; call, grouped by course&lt;br /&gt;
* Fixed: &amp;quot;Assignments&amp;quot; heading was rendering as body text because its class was referenced as a plain string instead of the CSS Modules export (&amp;lt;code&amp;gt;styles['assignments-title']&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
=== StudentTaskDetail (Detail View) ===&lt;br /&gt;
* Visual timeline with progress line and stage nodes (completed/current/pending)&lt;br /&gt;
* Task links list — &amp;quot;Your work&amp;quot; and &amp;quot;Others' work&amp;quot; conditionally rendered based on &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;quot;Send Email To Reviewers&amp;quot; button repositioned to align with the task list rather than floating disconnected at the page top&lt;br /&gt;
&lt;br /&gt;
=== ViewTeamGrades (Heatgrid) ===&lt;br /&gt;
* Consolidated ~6 sequential API calls into one &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
* SectionHeader rows split into a sticky label cell + scrolling spacer cell so they stay fixed during horizontal scroll&lt;br /&gt;
* Score cells show reviewer feedback as a hover tooltip&lt;br /&gt;
* Round headings simplified from &amp;quot;Review (Round: 1 of 2)&amp;quot; to &amp;quot;Round 1&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Design Principles ==&lt;br /&gt;
&lt;br /&gt;
=== Single Responsibility ===&lt;br /&gt;
Score aggregation is layered: &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; owns item-level scoring, &amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt; owns per-map/per-round weighting, &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; owns cross-map averaging — each level only knows about the one below it.&lt;br /&gt;
&lt;br /&gt;
=== Don't Repeat Yourself ===&lt;br /&gt;
The weighted-average logic that was duplicated between &amp;lt;code&amp;gt;AssignmentTeam#aggregate_review_grade&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;ReviewAggregator#compute_average_review_score&amp;lt;/code&amp;gt; was consolidated into a single &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt;, and the now-dead &amp;lt;code&amp;gt;ReviewAggregator&amp;lt;/code&amp;gt; concern was deleted.&lt;br /&gt;
&lt;br /&gt;
=== Performance by Design ===&lt;br /&gt;
Eager loading (&amp;lt;code&amp;gt;.includes&amp;lt;/code&amp;gt;) and in-memory filtering (&amp;lt;code&amp;gt;.reject&amp;lt;/code&amp;gt;) were chosen deliberately over additional database round-trips after profiling showed the heatgrid endpoint firing hundreds of queries under the old implementation.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
&lt;br /&gt;
=== Backend Tests ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! # !! Test !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| 1 || &amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt; groups teammates by course || Returns &amp;lt;code&amp;gt;{course_name =&amp;gt; [fullnames]}&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 2 || &amp;lt;code&amp;gt;all_teammates&amp;lt;/code&amp;gt; excludes calibrated assignments || Calibrated-assignment teammates excluded&lt;br /&gt;
|-&lt;br /&gt;
| 3 || &amp;lt;code&amp;gt;all_teammates&amp;lt;/code&amp;gt; excludes the user themself || Solo team returns empty hash&lt;br /&gt;
|-&lt;br /&gt;
| 4 || &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; returns all tasks for user || 200 OK with task array&lt;br /&gt;
|-&lt;br /&gt;
| 5 || &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt; returns due_dates timeline || 200 OK with &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt; array&lt;br /&gt;
|-&lt;br /&gt;
| 6 || &amp;lt;code&amp;gt;GET /student_tasks/team&amp;lt;/code&amp;gt; groups teammates by course || 200 OK with grouped hash&lt;br /&gt;
|-&lt;br /&gt;
| 7 || Heatgrid &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; column excludes SectionHeaders from numbering || Sequential numbering skips header rows&lt;br /&gt;
|-&lt;br /&gt;
| 8 || &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt; filters SectionHeaders without re-querying || No additional SQL fired against preloaded association&lt;br /&gt;
|-&lt;br /&gt;
| 9 || &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; weight validation || &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt; must be 0 when rubric has no scored questions&lt;br /&gt;
|-&lt;br /&gt;
| 10 || &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; || Weighted average matches Σ(grade × weight) / Σ(weight)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Frontend Tests ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! # !! Test !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| 1 || &amp;lt;code&amp;gt;StudentTasks&amp;lt;/code&amp;gt; renders &amp;quot;Assignments&amp;quot; heading with correct styling || Heading uses module class, renders bold and large&lt;br /&gt;
|-&lt;br /&gt;
| 2 || &amp;lt;code&amp;gt;StudentTaskDetail&amp;lt;/code&amp;gt; hides &amp;quot;Your work&amp;quot; when &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; is false || List item not rendered&lt;br /&gt;
|-&lt;br /&gt;
| 3 || &amp;lt;code&amp;gt;StudentTaskDetail&amp;lt;/code&amp;gt; hides &amp;quot;Others' work&amp;quot; when &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; is false || List item not rendered&lt;br /&gt;
|-&lt;br /&gt;
| 4 || &amp;lt;code&amp;gt;heatgridUtils&amp;lt;/code&amp;gt; itemNumber skips SectionHeader rows || Numbering matches scored items only&lt;br /&gt;
|-&lt;br /&gt;
| 5 || &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt; SectionHeader row stays fixed during horizontal scroll || Header cell &amp;lt;code&amp;gt;position: sticky&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 6 || &amp;lt;code&amp;gt;ReviewTableRow&amp;lt;/code&amp;gt; shows tooltip only when a comment exists || No tooltip box when &amp;lt;code&amp;gt;data-question&amp;lt;/code&amp;gt; is undefined&lt;br /&gt;
|-&lt;br /&gt;
| 7 || Heatgrid column borders render as &amp;lt;code&amp;gt;#ddd&amp;lt;/code&amp;gt; || No black borders after &amp;lt;code&amp;gt;tbl_heat&amp;lt;/code&amp;gt; class applied&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Relevant Links ==&lt;br /&gt;
&lt;br /&gt;
=== Repos ===&lt;br /&gt;
* https://github.com/expertiza/reimplementation-back-end&lt;br /&gt;
* https://github.com/expertiza/reimplementation-front-end&lt;br /&gt;
&lt;br /&gt;
=== Pull Requests ===&lt;br /&gt;
* Backend: https://github.com/expertiza/reimplementation-back-end/pull/352&lt;br /&gt;
* Frontend: https://github.com/expertiza/reimplementation-front-end/pull/182&lt;br /&gt;
&lt;br /&gt;
=== Demo Video ===&lt;br /&gt;
* (add link once recorded)&lt;br /&gt;
&lt;br /&gt;
== Developer Guidance ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Setup &amp;amp; Run (via Docker) ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
docker compose up&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Within the container ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
bundle exec rspec spec/models/student_task_spec.rb&lt;br /&gt;
bundle exec rspec spec/requests/api/v1/student_tasks_controller_spec.rb&lt;br /&gt;
bundle exec rspec spec/requests/api/v1/grades_controller_spec.rb&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
==== Setup &amp;amp; Run ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm install&lt;br /&gt;
npm start&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Running Frontend Tests ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm test -- StudentTaskDetail   # run a single suite&lt;br /&gt;
npm test                        # run full suite&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Future Scope ==&lt;br /&gt;
&lt;br /&gt;
=== Reviewer reputation-weighted grading ===&lt;br /&gt;
Integrate an external review-quality grader (e.g. an LLM-based reviewer grader) so &amp;lt;code&amp;gt;ResponseMap.reviewer_reputation_for&amp;lt;/code&amp;gt; reflects actual review quality instead of the current &amp;lt;code&amp;gt;1.0&amp;lt;/code&amp;gt; placeholder for every reviewer.&lt;br /&gt;
&lt;br /&gt;
=== Email-the-authors integration ===&lt;br /&gt;
Wire &amp;quot;Send Email To Reviewers&amp;quot; to a real backend endpoint instead of the current placeholder route.&lt;br /&gt;
&lt;br /&gt;
=== Dedicated StudentTask abstraction ===&lt;br /&gt;
&amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; remains a composed, non-persisted object. A future dedicated table could centralize derived state and simplify queries if the feature's scope grows further.&lt;br /&gt;
&lt;br /&gt;
== Mentor ==&lt;br /&gt;
[mailto:kgudipe@ncsu.edu Koushik Gudipelly]&lt;br /&gt;
&lt;br /&gt;
== Members ==&lt;br /&gt;
[mailto:rsgopara@ncsu.edu Ravi Goparaju]&lt;br /&gt;
[mailto:xmi@ncsu.edu Xiangjun Mi]&lt;br /&gt;
&lt;br /&gt;
== Completed by ==&lt;br /&gt;
[mailto:blalu@ncsu.edu Bestin Lalu]&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2602._Reimplement_student_task_view&amp;diff=168233</id>
		<title>CSC/ECE 517 Spring 2026 - E2602. Reimplement student task view</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2602._Reimplement_student_task_view&amp;diff=168233"/>
		<updated>2026-07-02T23:38:08Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Request / Response Flow */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= CSC/ECE 517 Spring 2026 - E2602. Reimplement Student Task View =&lt;br /&gt;
&lt;br /&gt;
== About Expertiza ==&lt;br /&gt;
Expertiza is a robust, open-source web platform designed to empower educational environments that emphasize project-based learning. It streamlines collaborative activities including peer reviews, surveys, and team assignments — giving instructors tools to manage assignments and track performance, and giving students a single place to submit work, review peers, and track feedback.&lt;br /&gt;
&lt;br /&gt;
== Introduction ==&lt;br /&gt;
This iteration of E2602 hardens and extends the Student Task View: consolidating the student dashboard and detail pages into single-call REST endpoints, eliminating N+1 queries in the peer-review score heatgrid, and fixing several display/permission bugs surfaced by manual QA against real assignment data.&lt;br /&gt;
&lt;br /&gt;
=== Tech Stack ===&lt;br /&gt;
* '''Backend:''' Ruby on Rails (API-only)&lt;br /&gt;
* '''Frontend:''' React + TypeScript&lt;br /&gt;
* '''Database:''' MySQL&lt;br /&gt;
* '''Backend Testing:''' RSpec&lt;br /&gt;
* '''Frontend Testing:''' Jest, React Testing Library&lt;br /&gt;
&lt;br /&gt;
=== Key Improvements ===&lt;br /&gt;
* Eliminated N+1 queries in the score heatgrid (&amp;lt;code&amp;gt;get_team_scores&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_my_scores_data&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_answer&amp;lt;/code&amp;gt;) via &amp;lt;code&amp;gt;.includes(responses: { scores: :item })&amp;lt;/code&amp;gt; and in-memory filtering instead of re-querying preloaded associations&lt;br /&gt;
* Converted &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt;'s stylesheet to CSS Modules, fixing a Bootstrap class collision and scoping color classes (&amp;lt;code&amp;gt;c1&amp;lt;/code&amp;gt;–&amp;lt;code&amp;gt;c5&amp;lt;/code&amp;gt;) correctly&lt;br /&gt;
* Redesigned the score/feedback heatgrid with sticky &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;/Question columns and a sticky, full-width SectionHeader row that stays fixed during horizontal scroll&lt;br /&gt;
* Added hover tooltips on score cells showing the reviewer's written feedback&lt;br /&gt;
* Rewrote &amp;lt;code&amp;gt;StudentTasks&amp;lt;/code&amp;gt; from a (mis-implemented) topic sign-up sheet into a proper course-grouped assignment dashboard&lt;br /&gt;
* Added participant-level &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; permission checks, hiding &amp;quot;Your work&amp;quot; / &amp;quot;Others' work&amp;quot; task links accordingly&lt;br /&gt;
* Refactored review grade calculation: &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; now weights each round by its &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;; &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; centralizes the weighted-average logic previously duplicated across &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Problem Statement ==&lt;br /&gt;
The previous Student Task View had several concrete defects:&lt;br /&gt;
* The score heatgrid fired one SQL query per response/score across all reviewers and rounds, producing hundreds of queries for a single page load&lt;br /&gt;
* SectionHeader rubric rows (non-scorable dividers) were incorrectly counted in the &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; column numbering and lost their fixed position during horizontal scroll&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTasks.tsx&amp;lt;/code&amp;gt; was previously implemented as a topic sign-up sheet, not a task dashboard, and required a full rewrite&lt;br /&gt;
* The page did not respect participant-level &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; flags — all task links were always shown regardless of permission&lt;br /&gt;
* Review grade calculation treated every reviewer and every round equally, ignoring each rubric's configured &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== User Stories ==&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want to see all my assignments in one place ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; returns all tasks for the logged-in student in one call&lt;br /&gt;
* Tasks are grouped by course on the dashboard&lt;br /&gt;
* Each task shows current stage and days remaining, if applicable&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want to view a single assignment's deadlines and details ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt; returns the task plus a unified &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt; timeline&lt;br /&gt;
* The timeline visually distinguishes completed, current, and pending stages&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/team&amp;lt;/code&amp;gt; returns teammates grouped by course&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want my team's score heatgrid to load fast and stay readable while scrolling ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* The heatgrid loads from a single &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt; call&lt;br /&gt;
* The first two columns (item number &amp;quot;&amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;&amp;quot; and Item) remain fixed during horizontal scroll, along with section headers&lt;br /&gt;
* Hovering over a score with a comment shows the corresponding reviewer's feedback as a tooltip&lt;br /&gt;
&lt;br /&gt;
=== A student who isn't permitted to submit or review will not see those task links ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;quot;Your work&amp;quot; is hidden when &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;quot;Others' work&amp;quot; is hidden when &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== As an instructor, I want calculated review grades to reflect each rubric's configured weight ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; weights each round's normalized score by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt; both compute their averages through the same shared &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
=== Student Task Overview ===&lt;br /&gt;
A StudentTask is a task that a student performs in the course of doing an assignment, e.g., forming a team, choosing a topic, submitting work, or reviewing work. No dedicated &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; database table exists. A &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; is a plain Ruby object composed from &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; and its associated &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignedUpTeam&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;ProjectTopic&amp;lt;/code&amp;gt;, and permission fields (&amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;permission_granted&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Request / Response Flow ====&lt;br /&gt;
* Student navigates to &amp;lt;code&amp;gt;/student_tasks&amp;lt;/code&amp;gt; → frontend calls &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTask.tasks(user)&amp;lt;/code&amp;gt; preloads &amp;lt;code&amp;gt;assignment: :course&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;:user&amp;lt;/code&amp;gt; associations, then maps each &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; through &amp;lt;code&amp;gt;StudentTask.create_from_participant&amp;lt;/code&amp;gt;&lt;br /&gt;
* Selecting an assignment navigates to the detail route → frontend calls &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTask.timeline_events&amp;lt;/code&amp;gt; merges the assignment's due dates with timestamps of reviews done of the logged-in user's work into one sorted timeline&lt;br /&gt;
* For the score heatgrid: frontend calls &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt; → &amp;lt;code&amp;gt;GradesController#get_team_scores&amp;lt;/code&amp;gt; eager-loads &amp;lt;code&amp;gt;responses: { scores: :item }&amp;lt;/code&amp;gt; and calls &amp;lt;code&amp;gt;insert_section_headers&amp;lt;/code&amp;gt; to splice header sentinels into the response&lt;br /&gt;
&lt;br /&gt;
=== Score Aggregation Flow ===&lt;br /&gt;
* &amp;lt;code&amp;gt;Response#aggregate_questionnaire_score&amp;lt;/code&amp;gt; — item-weighted score for one submitted response&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; — normalizes a map's score per round and weights it by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score(maps)&amp;lt;/code&amp;gt; — weighted average across multiple maps, the single implementation shared by &amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Backend Implementation ==&lt;br /&gt;
&lt;br /&gt;
=== StudentTasksController ===&lt;br /&gt;
* &amp;lt;code&amp;gt;list&amp;lt;/code&amp;gt; — all &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; objects for the logged-in user&lt;br /&gt;
* &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt; — single task by participant ID with the unified timeline&lt;br /&gt;
* &amp;lt;code&amp;gt;team&amp;lt;/code&amp;gt; — teammates grouped by course name (&amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
=== GradesController — N+1 elimination ===&lt;br /&gt;
&amp;lt;code&amp;gt;get_team_scores&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;get_my_scores_data&amp;lt;/code&amp;gt; eager-load &amp;lt;code&amp;gt;responses: { scores: :item }&amp;lt;/code&amp;gt; on the &amp;lt;code&amp;gt;ReviewResponseMap&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;TeammateReviewResponseMap&amp;lt;/code&amp;gt; queries. &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt; filters SectionHeader scores via Ruby &amp;lt;code&amp;gt;.reject&amp;lt;/code&amp;gt; instead of &amp;lt;code&amp;gt;.joins&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;.where&amp;lt;/code&amp;gt; — calling &amp;lt;code&amp;gt;.where&amp;lt;/code&amp;gt; on an already-preloaded association silently bypasses the cache and re-queries the database. &amp;lt;code&amp;gt;get_answer&amp;lt;/code&amp;gt; now accepts the parent &amp;lt;code&amp;gt;response&amp;lt;/code&amp;gt; directly instead of re-fetching it via &amp;lt;code&amp;gt;score.response&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== insert_section_headers ===&lt;br /&gt;
Splices &amp;lt;code&amp;gt;{ type: &amp;quot;header&amp;quot;, txt: &amp;quot;...&amp;quot; }&amp;lt;/code&amp;gt; sentinel hashes into a round's scores array at positions matching each &amp;lt;code&amp;gt;SectionHeader&amp;lt;/code&amp;gt; item in the questionnaire's sequence, so the frontend can render heading rows between score rows without those rows affecting numbering.&lt;br /&gt;
&lt;br /&gt;
=== Review Grade Calculation ===&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; — latest submitted response per round, normalized (&amp;lt;code&amp;gt;score/max&amp;lt;/code&amp;gt;), weighted by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;ResponseMap.reviewer_reputation_for&amp;lt;/code&amp;gt; — shared weighted-average logic; reviewer reputation currently defaults to &amp;lt;code&amp;gt;1.0&amp;lt;/code&amp;gt; (placeholder for a future review-quality grader integration)&lt;br /&gt;
* &amp;lt;code&amp;gt;Questionnaire#total_item_weight&amp;lt;/code&amp;gt; — pre-calculates the sum of scored item weights, excluding &amp;lt;code&amp;gt;SectionHeader&amp;lt;/code&amp;gt; items&lt;br /&gt;
&lt;br /&gt;
=== Comprehensive Testing ===&lt;br /&gt;
* Model specs for &amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;create_from_participant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_events_for_assignment&amp;lt;/code&amp;gt;&lt;br /&gt;
* Request specs for &amp;lt;code&amp;gt;student_tasks/list&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;student_tasks/show/:id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;student_tasks/team&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;grades/:id/view_our_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
* Routing specs verifying the above routes resolve correctly&lt;br /&gt;
&lt;br /&gt;
== Frontend Implementation ==&lt;br /&gt;
[[File:student_view_assignments_page.png|600px|thumb|right|Student View Assignments Page]]&lt;br /&gt;
[[File:student_view_assignment_detail_page.png|600px|thumb|right|Student View Assignment Detail Page]]&lt;br /&gt;
[[File:student_view_assignment_grades_page.png|600px|thumb|right|Student View Assignment View Team Grades]]&lt;br /&gt;
[[File:student_view_assignment_feedback_page.png|600px|thumb|right|Student View Assignment View Feedbacks]]&lt;br /&gt;
=== New/Updated Files ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! File !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTasks.tsx&amp;lt;/code&amp;gt; || Course-grouped assignment dashboard (rewritten from a topic sign-up sheet)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTasksList.tsx&amp;lt;/code&amp;gt; || Sidebar — not-started tasks, revisions, teammates by course&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTaskDetail.tsx&amp;lt;/code&amp;gt; || Per-assignment detail page — timeline, task links, conditional &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; rendering&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ViewTeamGrades.module.scss&amp;lt;/code&amp;gt; || CSS Modules conversion; scoped &amp;lt;code&amp;gt;c1&amp;lt;/code&amp;gt;–&amp;lt;code&amp;gt;c5&amp;lt;/code&amp;gt; color classes, sticky-column and tooltip styles&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewTable.tsx&amp;lt;/code&amp;gt; || Single-call heatgrid; Scores/Feedback toggle; sticky SectionHeader rows&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewTableRow.tsx&amp;lt;/code&amp;gt; || Sticky &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;/Question columns; reduced row height; hover tooltip via &amp;lt;code&amp;gt;data-question&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;FeedbackTable.tsx&amp;lt;/code&amp;gt; || Feedback-mode table mirroring &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt;'s sticky-column layout&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;heatgridUtils.ts&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;RoundRow&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;isHeader()&amp;lt;/code&amp;gt; types; fixed &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; numbering to skip SectionHeader sentinels&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== StudentTasks (List View) ===&lt;br /&gt;
* Single &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; call, grouped by course&lt;br /&gt;
* Fixed: &amp;quot;Assignments&amp;quot; heading was rendering as body text because its class was referenced as a plain string instead of the CSS Modules export (&amp;lt;code&amp;gt;styles['assignments-title']&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
=== StudentTaskDetail (Detail View) ===&lt;br /&gt;
* Visual timeline with progress line and stage nodes (completed/current/pending)&lt;br /&gt;
* Task links list — &amp;quot;Your work&amp;quot; and &amp;quot;Others' work&amp;quot; conditionally rendered based on &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;quot;Send Email To Reviewers&amp;quot; button repositioned to align with the task list rather than floating disconnected at the page top&lt;br /&gt;
&lt;br /&gt;
=== ViewTeamGrades (Heatgrid) ===&lt;br /&gt;
* Consolidated ~6 sequential API calls into one &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
* SectionHeader rows split into a sticky label cell + scrolling spacer cell so they stay fixed during horizontal scroll&lt;br /&gt;
* Score cells show reviewer feedback as a hover tooltip&lt;br /&gt;
* Round headings simplified from &amp;quot;Review (Round: 1 of 2)&amp;quot; to &amp;quot;Round 1&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Design Principles ==&lt;br /&gt;
&lt;br /&gt;
=== Single Responsibility ===&lt;br /&gt;
Score aggregation is layered: &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; owns item-level scoring, &amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt; owns per-map/per-round weighting, &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; owns cross-map averaging — each level only knows about the one below it.&lt;br /&gt;
&lt;br /&gt;
=== Don't Repeat Yourself ===&lt;br /&gt;
The weighted-average logic that was duplicated between &amp;lt;code&amp;gt;AssignmentTeam#aggregate_review_grade&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;ReviewAggregator#compute_average_review_score&amp;lt;/code&amp;gt; was consolidated into a single &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt;, and the now-dead &amp;lt;code&amp;gt;ReviewAggregator&amp;lt;/code&amp;gt; concern was deleted.&lt;br /&gt;
&lt;br /&gt;
=== Performance by Design ===&lt;br /&gt;
Eager loading (&amp;lt;code&amp;gt;.includes&amp;lt;/code&amp;gt;) and in-memory filtering (&amp;lt;code&amp;gt;.reject&amp;lt;/code&amp;gt;) were chosen deliberately over additional database round-trips after profiling showed the heatgrid endpoint firing hundreds of queries under the old implementation.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
&lt;br /&gt;
=== Backend Tests ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! # !! Test !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| 1 || &amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt; groups teammates by course || Returns &amp;lt;code&amp;gt;{course_name =&amp;gt; [fullnames]}&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 2 || &amp;lt;code&amp;gt;all_teammates&amp;lt;/code&amp;gt; excludes calibrated assignments || Calibrated-assignment teammates excluded&lt;br /&gt;
|-&lt;br /&gt;
| 3 || &amp;lt;code&amp;gt;all_teammates&amp;lt;/code&amp;gt; excludes the user themself || Solo team returns empty hash&lt;br /&gt;
|-&lt;br /&gt;
| 4 || &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; returns all tasks for user || 200 OK with task array&lt;br /&gt;
|-&lt;br /&gt;
| 5 || &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt; returns due_dates timeline || 200 OK with &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt; array&lt;br /&gt;
|-&lt;br /&gt;
| 6 || &amp;lt;code&amp;gt;GET /student_tasks/team&amp;lt;/code&amp;gt; groups teammates by course || 200 OK with grouped hash&lt;br /&gt;
|-&lt;br /&gt;
| 7 || Heatgrid &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; column excludes SectionHeaders from numbering || Sequential numbering skips header rows&lt;br /&gt;
|-&lt;br /&gt;
| 8 || &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt; filters SectionHeaders without re-querying || No additional SQL fired against preloaded association&lt;br /&gt;
|-&lt;br /&gt;
| 9 || &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; weight validation || &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt; must be 0 when rubric has no scored questions&lt;br /&gt;
|-&lt;br /&gt;
| 10 || &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; || Weighted average matches Σ(grade × weight) / Σ(weight)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Frontend Tests ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! # !! Test !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| 1 || &amp;lt;code&amp;gt;StudentTasks&amp;lt;/code&amp;gt; renders &amp;quot;Assignments&amp;quot; heading with correct styling || Heading uses module class, renders bold and large&lt;br /&gt;
|-&lt;br /&gt;
| 2 || &amp;lt;code&amp;gt;StudentTaskDetail&amp;lt;/code&amp;gt; hides &amp;quot;Your work&amp;quot; when &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; is false || List item not rendered&lt;br /&gt;
|-&lt;br /&gt;
| 3 || &amp;lt;code&amp;gt;StudentTaskDetail&amp;lt;/code&amp;gt; hides &amp;quot;Others' work&amp;quot; when &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; is false || List item not rendered&lt;br /&gt;
|-&lt;br /&gt;
| 4 || &amp;lt;code&amp;gt;heatgridUtils&amp;lt;/code&amp;gt; itemNumber skips SectionHeader rows || Numbering matches scored items only&lt;br /&gt;
|-&lt;br /&gt;
| 5 || &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt; SectionHeader row stays fixed during horizontal scroll || Header cell &amp;lt;code&amp;gt;position: sticky&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 6 || &amp;lt;code&amp;gt;ReviewTableRow&amp;lt;/code&amp;gt; shows tooltip only when a comment exists || No tooltip box when &amp;lt;code&amp;gt;data-question&amp;lt;/code&amp;gt; is undefined&lt;br /&gt;
|-&lt;br /&gt;
| 7 || Heatgrid column borders render as &amp;lt;code&amp;gt;#ddd&amp;lt;/code&amp;gt; || No black borders after &amp;lt;code&amp;gt;tbl_heat&amp;lt;/code&amp;gt; class applied&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Relevant Links ==&lt;br /&gt;
&lt;br /&gt;
=== Repos ===&lt;br /&gt;
* https://github.com/expertiza/reimplementation-back-end&lt;br /&gt;
* https://github.com/expertiza/reimplementation-front-end&lt;br /&gt;
&lt;br /&gt;
=== Pull Requests ===&lt;br /&gt;
* Backend: https://github.com/expertiza/reimplementation-back-end/pull/352&lt;br /&gt;
* Frontend: https://github.com/expertiza/reimplementation-front-end/pull/182&lt;br /&gt;
&lt;br /&gt;
=== Demo Video ===&lt;br /&gt;
* (add link once recorded)&lt;br /&gt;
&lt;br /&gt;
== Developer Guidance ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Setup &amp;amp; Run (via Docker) ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
docker compose up&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Within the container ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
bundle exec rspec spec/models/student_task_spec.rb&lt;br /&gt;
bundle exec rspec spec/requests/api/v1/student_tasks_controller_spec.rb&lt;br /&gt;
bundle exec rspec spec/requests/api/v1/grades_controller_spec.rb&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
==== Setup &amp;amp; Run ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm install&lt;br /&gt;
npm start&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Running Frontend Tests ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm test -- StudentTaskDetail   # run a single suite&lt;br /&gt;
npm test                        # run full suite&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Future Scope ==&lt;br /&gt;
&lt;br /&gt;
=== Reviewer reputation-weighted grading ===&lt;br /&gt;
Integrate an external review-quality grader (e.g. an LLM-based reviewer grader) so &amp;lt;code&amp;gt;ResponseMap.reviewer_reputation_for&amp;lt;/code&amp;gt; reflects actual review quality instead of the current &amp;lt;code&amp;gt;1.0&amp;lt;/code&amp;gt; placeholder for every reviewer.&lt;br /&gt;
&lt;br /&gt;
=== Email-the-authors integration ===&lt;br /&gt;
Wire &amp;quot;Send Email To Reviewers&amp;quot; to a real backend endpoint instead of the current placeholder route.&lt;br /&gt;
&lt;br /&gt;
=== Dedicated StudentTask abstraction ===&lt;br /&gt;
&amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; remains a composed, non-persisted object. A future dedicated table could centralize derived state and simplify queries if the feature's scope grows further.&lt;br /&gt;
&lt;br /&gt;
== Mentor ==&lt;br /&gt;
[mailto:kgudipe@ncsu.edu Koushik Gudipelly]&lt;br /&gt;
&lt;br /&gt;
== Members ==&lt;br /&gt;
[mailto:rsgopara@ncsu.edu Ravi Goparaju]&lt;br /&gt;
[mailto:xmi@ncsu.edu Xiangjun Mi]&lt;br /&gt;
&lt;br /&gt;
== Completed by ==&lt;br /&gt;
[mailto:blalu@ncsu.edu Bestin Lalu]&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2602._Reimplement_student_task_view&amp;diff=168232</id>
		<title>CSC/ECE 517 Spring 2026 - E2602. Reimplement student task view</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2602._Reimplement_student_task_view&amp;diff=168232"/>
		<updated>2026-07-02T23:28:57Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Student Task Overview */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= CSC/ECE 517 Spring 2026 - E2602. Reimplement Student Task View =&lt;br /&gt;
&lt;br /&gt;
== About Expertiza ==&lt;br /&gt;
Expertiza is a robust, open-source web platform designed to empower educational environments that emphasize project-based learning. It streamlines collaborative activities including peer reviews, surveys, and team assignments — giving instructors tools to manage assignments and track performance, and giving students a single place to submit work, review peers, and track feedback.&lt;br /&gt;
&lt;br /&gt;
== Introduction ==&lt;br /&gt;
This iteration of E2602 hardens and extends the Student Task View: consolidating the student dashboard and detail pages into single-call REST endpoints, eliminating N+1 queries in the peer-review score heatgrid, and fixing several display/permission bugs surfaced by manual QA against real assignment data.&lt;br /&gt;
&lt;br /&gt;
=== Tech Stack ===&lt;br /&gt;
* '''Backend:''' Ruby on Rails (API-only)&lt;br /&gt;
* '''Frontend:''' React + TypeScript&lt;br /&gt;
* '''Database:''' MySQL&lt;br /&gt;
* '''Backend Testing:''' RSpec&lt;br /&gt;
* '''Frontend Testing:''' Jest, React Testing Library&lt;br /&gt;
&lt;br /&gt;
=== Key Improvements ===&lt;br /&gt;
* Eliminated N+1 queries in the score heatgrid (&amp;lt;code&amp;gt;get_team_scores&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_my_scores_data&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_answer&amp;lt;/code&amp;gt;) via &amp;lt;code&amp;gt;.includes(responses: { scores: :item })&amp;lt;/code&amp;gt; and in-memory filtering instead of re-querying preloaded associations&lt;br /&gt;
* Converted &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt;'s stylesheet to CSS Modules, fixing a Bootstrap class collision and scoping color classes (&amp;lt;code&amp;gt;c1&amp;lt;/code&amp;gt;–&amp;lt;code&amp;gt;c5&amp;lt;/code&amp;gt;) correctly&lt;br /&gt;
* Redesigned the score/feedback heatgrid with sticky &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;/Question columns and a sticky, full-width SectionHeader row that stays fixed during horizontal scroll&lt;br /&gt;
* Added hover tooltips on score cells showing the reviewer's written feedback&lt;br /&gt;
* Rewrote &amp;lt;code&amp;gt;StudentTasks&amp;lt;/code&amp;gt; from a (mis-implemented) topic sign-up sheet into a proper course-grouped assignment dashboard&lt;br /&gt;
* Added participant-level &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; permission checks, hiding &amp;quot;Your work&amp;quot; / &amp;quot;Others' work&amp;quot; task links accordingly&lt;br /&gt;
* Refactored review grade calculation: &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; now weights each round by its &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;; &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; centralizes the weighted-average logic previously duplicated across &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Problem Statement ==&lt;br /&gt;
The previous Student Task View had several concrete defects:&lt;br /&gt;
* The score heatgrid fired one SQL query per response/score across all reviewers and rounds, producing hundreds of queries for a single page load&lt;br /&gt;
* SectionHeader rubric rows (non-scorable dividers) were incorrectly counted in the &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; column numbering and lost their fixed position during horizontal scroll&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTasks.tsx&amp;lt;/code&amp;gt; was previously implemented as a topic sign-up sheet, not a task dashboard, and required a full rewrite&lt;br /&gt;
* The page did not respect participant-level &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; flags — all task links were always shown regardless of permission&lt;br /&gt;
* Review grade calculation treated every reviewer and every round equally, ignoring each rubric's configured &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== User Stories ==&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want to see all my assignments in one place ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; returns all tasks for the logged-in student in one call&lt;br /&gt;
* Tasks are grouped by course on the dashboard&lt;br /&gt;
* Each task shows current stage and days remaining, if applicable&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want to view a single assignment's deadlines and details ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt; returns the task plus a unified &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt; timeline&lt;br /&gt;
* The timeline visually distinguishes completed, current, and pending stages&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/team&amp;lt;/code&amp;gt; returns teammates grouped by course&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want my team's score heatgrid to load fast and stay readable while scrolling ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* The heatgrid loads from a single &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt; call&lt;br /&gt;
* The first two columns (item number &amp;quot;&amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;&amp;quot; and Item) remain fixed during horizontal scroll, along with section headers&lt;br /&gt;
* Hovering over a score with a comment shows the corresponding reviewer's feedback as a tooltip&lt;br /&gt;
&lt;br /&gt;
=== A student who isn't permitted to submit or review will not see those task links ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;quot;Your work&amp;quot; is hidden when &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;quot;Others' work&amp;quot; is hidden when &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== As an instructor, I want calculated review grades to reflect each rubric's configured weight ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; weights each round's normalized score by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt; both compute their averages through the same shared &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
=== Student Task Overview ===&lt;br /&gt;
A StudentTask is a task that a student performs in the course of doing an assignment, e.g., forming a team, choosing a topic, submitting work, or reviewing work. No dedicated &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; database table exists. A &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; is a plain Ruby object composed from &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; and its associated &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignedUpTeam&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;ProjectTopic&amp;lt;/code&amp;gt;, and permission fields (&amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;permission_granted&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Request / Response Flow ====&lt;br /&gt;
* Student navigates to &amp;lt;code&amp;gt;/student_tasks&amp;lt;/code&amp;gt; → frontend calls &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTask.tasks(user)&amp;lt;/code&amp;gt; preloads &amp;lt;code&amp;gt;assignment: :course&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;:user&amp;lt;/code&amp;gt; associations, then maps each &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; through &amp;lt;code&amp;gt;StudentTask.create_from_participant&amp;lt;/code&amp;gt;&lt;br /&gt;
* Selecting an assignment navigates to the detail route → frontend calls &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTask.get_events_for_assignment&amp;lt;/code&amp;gt; merges due dates with submitted review/feedback activity into one sorted timeline&lt;br /&gt;
* For the score heatgrid: frontend calls &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt; → &amp;lt;code&amp;gt;GradesController#get_team_scores&amp;lt;/code&amp;gt; eager-loads &amp;lt;code&amp;gt;responses: { scores: :item }&amp;lt;/code&amp;gt; and calls &amp;lt;code&amp;gt;insert_section_headers&amp;lt;/code&amp;gt; to splice header sentinels into the response&lt;br /&gt;
&lt;br /&gt;
=== Score Aggregation Flow ===&lt;br /&gt;
* &amp;lt;code&amp;gt;Response#aggregate_questionnaire_score&amp;lt;/code&amp;gt; — item-weighted score for one submitted response&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; — normalizes a map's score per round and weights it by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score(maps)&amp;lt;/code&amp;gt; — weighted average across multiple maps, the single implementation shared by &amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Backend Implementation ==&lt;br /&gt;
&lt;br /&gt;
=== StudentTasksController ===&lt;br /&gt;
* &amp;lt;code&amp;gt;list&amp;lt;/code&amp;gt; — all &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; objects for the logged-in user&lt;br /&gt;
* &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt; — single task by participant ID with the unified timeline&lt;br /&gt;
* &amp;lt;code&amp;gt;team&amp;lt;/code&amp;gt; — teammates grouped by course name (&amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
=== GradesController — N+1 elimination ===&lt;br /&gt;
&amp;lt;code&amp;gt;get_team_scores&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;get_my_scores_data&amp;lt;/code&amp;gt; eager-load &amp;lt;code&amp;gt;responses: { scores: :item }&amp;lt;/code&amp;gt; on the &amp;lt;code&amp;gt;ReviewResponseMap&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;TeammateReviewResponseMap&amp;lt;/code&amp;gt; queries. &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt; filters SectionHeader scores via Ruby &amp;lt;code&amp;gt;.reject&amp;lt;/code&amp;gt; instead of &amp;lt;code&amp;gt;.joins&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;.where&amp;lt;/code&amp;gt; — calling &amp;lt;code&amp;gt;.where&amp;lt;/code&amp;gt; on an already-preloaded association silently bypasses the cache and re-queries the database. &amp;lt;code&amp;gt;get_answer&amp;lt;/code&amp;gt; now accepts the parent &amp;lt;code&amp;gt;response&amp;lt;/code&amp;gt; directly instead of re-fetching it via &amp;lt;code&amp;gt;score.response&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== insert_section_headers ===&lt;br /&gt;
Splices &amp;lt;code&amp;gt;{ type: &amp;quot;header&amp;quot;, txt: &amp;quot;...&amp;quot; }&amp;lt;/code&amp;gt; sentinel hashes into a round's scores array at positions matching each &amp;lt;code&amp;gt;SectionHeader&amp;lt;/code&amp;gt; item in the questionnaire's sequence, so the frontend can render heading rows between score rows without those rows affecting numbering.&lt;br /&gt;
&lt;br /&gt;
=== Review Grade Calculation ===&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; — latest submitted response per round, normalized (&amp;lt;code&amp;gt;score/max&amp;lt;/code&amp;gt;), weighted by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;ResponseMap.reviewer_reputation_for&amp;lt;/code&amp;gt; — shared weighted-average logic; reviewer reputation currently defaults to &amp;lt;code&amp;gt;1.0&amp;lt;/code&amp;gt; (placeholder for a future review-quality grader integration)&lt;br /&gt;
* &amp;lt;code&amp;gt;Questionnaire#total_item_weight&amp;lt;/code&amp;gt; — pre-calculates the sum of scored item weights, excluding &amp;lt;code&amp;gt;SectionHeader&amp;lt;/code&amp;gt; items&lt;br /&gt;
&lt;br /&gt;
=== Comprehensive Testing ===&lt;br /&gt;
* Model specs for &amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;create_from_participant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_events_for_assignment&amp;lt;/code&amp;gt;&lt;br /&gt;
* Request specs for &amp;lt;code&amp;gt;student_tasks/list&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;student_tasks/show/:id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;student_tasks/team&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;grades/:id/view_our_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
* Routing specs verifying the above routes resolve correctly&lt;br /&gt;
&lt;br /&gt;
== Frontend Implementation ==&lt;br /&gt;
[[File:student_view_assignments_page.png|600px|thumb|right|Student View Assignments Page]]&lt;br /&gt;
[[File:student_view_assignment_detail_page.png|600px|thumb|right|Student View Assignment Detail Page]]&lt;br /&gt;
[[File:student_view_assignment_grades_page.png|600px|thumb|right|Student View Assignment View Team Grades]]&lt;br /&gt;
[[File:student_view_assignment_feedback_page.png|600px|thumb|right|Student View Assignment View Feedbacks]]&lt;br /&gt;
=== New/Updated Files ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! File !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTasks.tsx&amp;lt;/code&amp;gt; || Course-grouped assignment dashboard (rewritten from a topic sign-up sheet)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTasksList.tsx&amp;lt;/code&amp;gt; || Sidebar — not-started tasks, revisions, teammates by course&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTaskDetail.tsx&amp;lt;/code&amp;gt; || Per-assignment detail page — timeline, task links, conditional &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; rendering&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ViewTeamGrades.module.scss&amp;lt;/code&amp;gt; || CSS Modules conversion; scoped &amp;lt;code&amp;gt;c1&amp;lt;/code&amp;gt;–&amp;lt;code&amp;gt;c5&amp;lt;/code&amp;gt; color classes, sticky-column and tooltip styles&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewTable.tsx&amp;lt;/code&amp;gt; || Single-call heatgrid; Scores/Feedback toggle; sticky SectionHeader rows&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewTableRow.tsx&amp;lt;/code&amp;gt; || Sticky &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;/Question columns; reduced row height; hover tooltip via &amp;lt;code&amp;gt;data-question&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;FeedbackTable.tsx&amp;lt;/code&amp;gt; || Feedback-mode table mirroring &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt;'s sticky-column layout&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;heatgridUtils.ts&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;RoundRow&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;isHeader()&amp;lt;/code&amp;gt; types; fixed &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; numbering to skip SectionHeader sentinels&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== StudentTasks (List View) ===&lt;br /&gt;
* Single &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; call, grouped by course&lt;br /&gt;
* Fixed: &amp;quot;Assignments&amp;quot; heading was rendering as body text because its class was referenced as a plain string instead of the CSS Modules export (&amp;lt;code&amp;gt;styles['assignments-title']&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
=== StudentTaskDetail (Detail View) ===&lt;br /&gt;
* Visual timeline with progress line and stage nodes (completed/current/pending)&lt;br /&gt;
* Task links list — &amp;quot;Your work&amp;quot; and &amp;quot;Others' work&amp;quot; conditionally rendered based on &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;quot;Send Email To Reviewers&amp;quot; button repositioned to align with the task list rather than floating disconnected at the page top&lt;br /&gt;
&lt;br /&gt;
=== ViewTeamGrades (Heatgrid) ===&lt;br /&gt;
* Consolidated ~6 sequential API calls into one &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
* SectionHeader rows split into a sticky label cell + scrolling spacer cell so they stay fixed during horizontal scroll&lt;br /&gt;
* Score cells show reviewer feedback as a hover tooltip&lt;br /&gt;
* Round headings simplified from &amp;quot;Review (Round: 1 of 2)&amp;quot; to &amp;quot;Round 1&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Design Principles ==&lt;br /&gt;
&lt;br /&gt;
=== Single Responsibility ===&lt;br /&gt;
Score aggregation is layered: &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; owns item-level scoring, &amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt; owns per-map/per-round weighting, &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; owns cross-map averaging — each level only knows about the one below it.&lt;br /&gt;
&lt;br /&gt;
=== Don't Repeat Yourself ===&lt;br /&gt;
The weighted-average logic that was duplicated between &amp;lt;code&amp;gt;AssignmentTeam#aggregate_review_grade&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;ReviewAggregator#compute_average_review_score&amp;lt;/code&amp;gt; was consolidated into a single &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt;, and the now-dead &amp;lt;code&amp;gt;ReviewAggregator&amp;lt;/code&amp;gt; concern was deleted.&lt;br /&gt;
&lt;br /&gt;
=== Performance by Design ===&lt;br /&gt;
Eager loading (&amp;lt;code&amp;gt;.includes&amp;lt;/code&amp;gt;) and in-memory filtering (&amp;lt;code&amp;gt;.reject&amp;lt;/code&amp;gt;) were chosen deliberately over additional database round-trips after profiling showed the heatgrid endpoint firing hundreds of queries under the old implementation.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
&lt;br /&gt;
=== Backend Tests ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! # !! Test !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| 1 || &amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt; groups teammates by course || Returns &amp;lt;code&amp;gt;{course_name =&amp;gt; [fullnames]}&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 2 || &amp;lt;code&amp;gt;all_teammates&amp;lt;/code&amp;gt; excludes calibrated assignments || Calibrated-assignment teammates excluded&lt;br /&gt;
|-&lt;br /&gt;
| 3 || &amp;lt;code&amp;gt;all_teammates&amp;lt;/code&amp;gt; excludes the user themself || Solo team returns empty hash&lt;br /&gt;
|-&lt;br /&gt;
| 4 || &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; returns all tasks for user || 200 OK with task array&lt;br /&gt;
|-&lt;br /&gt;
| 5 || &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt; returns due_dates timeline || 200 OK with &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt; array&lt;br /&gt;
|-&lt;br /&gt;
| 6 || &amp;lt;code&amp;gt;GET /student_tasks/team&amp;lt;/code&amp;gt; groups teammates by course || 200 OK with grouped hash&lt;br /&gt;
|-&lt;br /&gt;
| 7 || Heatgrid &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; column excludes SectionHeaders from numbering || Sequential numbering skips header rows&lt;br /&gt;
|-&lt;br /&gt;
| 8 || &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt; filters SectionHeaders without re-querying || No additional SQL fired against preloaded association&lt;br /&gt;
|-&lt;br /&gt;
| 9 || &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; weight validation || &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt; must be 0 when rubric has no scored questions&lt;br /&gt;
|-&lt;br /&gt;
| 10 || &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; || Weighted average matches Σ(grade × weight) / Σ(weight)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Frontend Tests ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! # !! Test !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| 1 || &amp;lt;code&amp;gt;StudentTasks&amp;lt;/code&amp;gt; renders &amp;quot;Assignments&amp;quot; heading with correct styling || Heading uses module class, renders bold and large&lt;br /&gt;
|-&lt;br /&gt;
| 2 || &amp;lt;code&amp;gt;StudentTaskDetail&amp;lt;/code&amp;gt; hides &amp;quot;Your work&amp;quot; when &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; is false || List item not rendered&lt;br /&gt;
|-&lt;br /&gt;
| 3 || &amp;lt;code&amp;gt;StudentTaskDetail&amp;lt;/code&amp;gt; hides &amp;quot;Others' work&amp;quot; when &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; is false || List item not rendered&lt;br /&gt;
|-&lt;br /&gt;
| 4 || &amp;lt;code&amp;gt;heatgridUtils&amp;lt;/code&amp;gt; itemNumber skips SectionHeader rows || Numbering matches scored items only&lt;br /&gt;
|-&lt;br /&gt;
| 5 || &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt; SectionHeader row stays fixed during horizontal scroll || Header cell &amp;lt;code&amp;gt;position: sticky&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 6 || &amp;lt;code&amp;gt;ReviewTableRow&amp;lt;/code&amp;gt; shows tooltip only when a comment exists || No tooltip box when &amp;lt;code&amp;gt;data-question&amp;lt;/code&amp;gt; is undefined&lt;br /&gt;
|-&lt;br /&gt;
| 7 || Heatgrid column borders render as &amp;lt;code&amp;gt;#ddd&amp;lt;/code&amp;gt; || No black borders after &amp;lt;code&amp;gt;tbl_heat&amp;lt;/code&amp;gt; class applied&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Relevant Links ==&lt;br /&gt;
&lt;br /&gt;
=== Repos ===&lt;br /&gt;
* https://github.com/expertiza/reimplementation-back-end&lt;br /&gt;
* https://github.com/expertiza/reimplementation-front-end&lt;br /&gt;
&lt;br /&gt;
=== Pull Requests ===&lt;br /&gt;
* Backend: https://github.com/expertiza/reimplementation-back-end/pull/352&lt;br /&gt;
* Frontend: https://github.com/expertiza/reimplementation-front-end/pull/182&lt;br /&gt;
&lt;br /&gt;
=== Demo Video ===&lt;br /&gt;
* (add link once recorded)&lt;br /&gt;
&lt;br /&gt;
== Developer Guidance ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Setup &amp;amp; Run (via Docker) ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
docker compose up&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Within the container ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
bundle exec rspec spec/models/student_task_spec.rb&lt;br /&gt;
bundle exec rspec spec/requests/api/v1/student_tasks_controller_spec.rb&lt;br /&gt;
bundle exec rspec spec/requests/api/v1/grades_controller_spec.rb&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
==== Setup &amp;amp; Run ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm install&lt;br /&gt;
npm start&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Running Frontend Tests ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm test -- StudentTaskDetail   # run a single suite&lt;br /&gt;
npm test                        # run full suite&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Future Scope ==&lt;br /&gt;
&lt;br /&gt;
=== Reviewer reputation-weighted grading ===&lt;br /&gt;
Integrate an external review-quality grader (e.g. an LLM-based reviewer grader) so &amp;lt;code&amp;gt;ResponseMap.reviewer_reputation_for&amp;lt;/code&amp;gt; reflects actual review quality instead of the current &amp;lt;code&amp;gt;1.0&amp;lt;/code&amp;gt; placeholder for every reviewer.&lt;br /&gt;
&lt;br /&gt;
=== Email-the-authors integration ===&lt;br /&gt;
Wire &amp;quot;Send Email To Reviewers&amp;quot; to a real backend endpoint instead of the current placeholder route.&lt;br /&gt;
&lt;br /&gt;
=== Dedicated StudentTask abstraction ===&lt;br /&gt;
&amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; remains a composed, non-persisted object. A future dedicated table could centralize derived state and simplify queries if the feature's scope grows further.&lt;br /&gt;
&lt;br /&gt;
== Mentor ==&lt;br /&gt;
[mailto:kgudipe@ncsu.edu Koushik Gudipelly]&lt;br /&gt;
&lt;br /&gt;
== Members ==&lt;br /&gt;
[mailto:rsgopara@ncsu.edu Ravi Goparaju]&lt;br /&gt;
[mailto:xmi@ncsu.edu Xiangjun Mi]&lt;br /&gt;
&lt;br /&gt;
== Completed by ==&lt;br /&gt;
[mailto:blalu@ncsu.edu Bestin Lalu]&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2602._Reimplement_student_task_view&amp;diff=168231</id>
		<title>CSC/ECE 517 Spring 2026 - E2602. Reimplement student task view</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2602._Reimplement_student_task_view&amp;diff=168231"/>
		<updated>2026-07-02T23:26:24Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Student Task Overview */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= CSC/ECE 517 Spring 2026 - E2602. Reimplement Student Task View =&lt;br /&gt;
&lt;br /&gt;
== About Expertiza ==&lt;br /&gt;
Expertiza is a robust, open-source web platform designed to empower educational environments that emphasize project-based learning. It streamlines collaborative activities including peer reviews, surveys, and team assignments — giving instructors tools to manage assignments and track performance, and giving students a single place to submit work, review peers, and track feedback.&lt;br /&gt;
&lt;br /&gt;
== Introduction ==&lt;br /&gt;
This iteration of E2602 hardens and extends the Student Task View: consolidating the student dashboard and detail pages into single-call REST endpoints, eliminating N+1 queries in the peer-review score heatgrid, and fixing several display/permission bugs surfaced by manual QA against real assignment data.&lt;br /&gt;
&lt;br /&gt;
=== Tech Stack ===&lt;br /&gt;
* '''Backend:''' Ruby on Rails (API-only)&lt;br /&gt;
* '''Frontend:''' React + TypeScript&lt;br /&gt;
* '''Database:''' MySQL&lt;br /&gt;
* '''Backend Testing:''' RSpec&lt;br /&gt;
* '''Frontend Testing:''' Jest, React Testing Library&lt;br /&gt;
&lt;br /&gt;
=== Key Improvements ===&lt;br /&gt;
* Eliminated N+1 queries in the score heatgrid (&amp;lt;code&amp;gt;get_team_scores&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_my_scores_data&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_answer&amp;lt;/code&amp;gt;) via &amp;lt;code&amp;gt;.includes(responses: { scores: :item })&amp;lt;/code&amp;gt; and in-memory filtering instead of re-querying preloaded associations&lt;br /&gt;
* Converted &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt;'s stylesheet to CSS Modules, fixing a Bootstrap class collision and scoping color classes (&amp;lt;code&amp;gt;c1&amp;lt;/code&amp;gt;–&amp;lt;code&amp;gt;c5&amp;lt;/code&amp;gt;) correctly&lt;br /&gt;
* Redesigned the score/feedback heatgrid with sticky &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;/Question columns and a sticky, full-width SectionHeader row that stays fixed during horizontal scroll&lt;br /&gt;
* Added hover tooltips on score cells showing the reviewer's written feedback&lt;br /&gt;
* Rewrote &amp;lt;code&amp;gt;StudentTasks&amp;lt;/code&amp;gt; from a (mis-implemented) topic sign-up sheet into a proper course-grouped assignment dashboard&lt;br /&gt;
* Added participant-level &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; permission checks, hiding &amp;quot;Your work&amp;quot; / &amp;quot;Others' work&amp;quot; task links accordingly&lt;br /&gt;
* Refactored review grade calculation: &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; now weights each round by its &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;; &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; centralizes the weighted-average logic previously duplicated across &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Problem Statement ==&lt;br /&gt;
The previous Student Task View had several concrete defects:&lt;br /&gt;
* The score heatgrid fired one SQL query per response/score across all reviewers and rounds, producing hundreds of queries for a single page load&lt;br /&gt;
* SectionHeader rubric rows (non-scorable dividers) were incorrectly counted in the &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; column numbering and lost their fixed position during horizontal scroll&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTasks.tsx&amp;lt;/code&amp;gt; was previously implemented as a topic sign-up sheet, not a task dashboard, and required a full rewrite&lt;br /&gt;
* The page did not respect participant-level &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; flags — all task links were always shown regardless of permission&lt;br /&gt;
* Review grade calculation treated every reviewer and every round equally, ignoring each rubric's configured &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== User Stories ==&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want to see all my assignments in one place ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; returns all tasks for the logged-in student in one call&lt;br /&gt;
* Tasks are grouped by course on the dashboard&lt;br /&gt;
* Each task shows current stage and days remaining, if applicable&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want to view a single assignment's deadlines and details ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt; returns the task plus a unified &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt; timeline&lt;br /&gt;
* The timeline visually distinguishes completed, current, and pending stages&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/team&amp;lt;/code&amp;gt; returns teammates grouped by course&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want my team's score heatgrid to load fast and stay readable while scrolling ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* The heatgrid loads from a single &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt; call&lt;br /&gt;
* The first two columns (item number &amp;quot;&amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;&amp;quot; and Item) remain fixed during horizontal scroll, along with section headers&lt;br /&gt;
* Hovering over a score with a comment shows the corresponding reviewer's feedback as a tooltip&lt;br /&gt;
&lt;br /&gt;
=== A student who isn't permitted to submit or review will not see those task links ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;quot;Your work&amp;quot; is hidden when &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;quot;Others' work&amp;quot; is hidden when &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== As an instructor, I want calculated review grades to reflect each rubric's configured weight ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; weights each round's normalized score by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt; both compute their averages through the same shared &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
=== Student Task Overview ===&lt;br /&gt;
No dedicated &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; database table exists. A &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; is a plain Ruby object composed from &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; and its associated &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignedUpTeam&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;ProjectTopic&amp;lt;/code&amp;gt;, and permission fields (&amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;permission_granted&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Request / Response Flow ====&lt;br /&gt;
* Student navigates to &amp;lt;code&amp;gt;/student_tasks&amp;lt;/code&amp;gt; → frontend calls &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTask.tasks(user)&amp;lt;/code&amp;gt; preloads &amp;lt;code&amp;gt;assignment: :course&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;:user&amp;lt;/code&amp;gt; associations, then maps each &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; through &amp;lt;code&amp;gt;StudentTask.create_from_participant&amp;lt;/code&amp;gt;&lt;br /&gt;
* Selecting an assignment navigates to the detail route → frontend calls &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTask.get_events_for_assignment&amp;lt;/code&amp;gt; merges due dates with submitted review/feedback activity into one sorted timeline&lt;br /&gt;
* For the score heatgrid: frontend calls &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt; → &amp;lt;code&amp;gt;GradesController#get_team_scores&amp;lt;/code&amp;gt; eager-loads &amp;lt;code&amp;gt;responses: { scores: :item }&amp;lt;/code&amp;gt; and calls &amp;lt;code&amp;gt;insert_section_headers&amp;lt;/code&amp;gt; to splice header sentinels into the response&lt;br /&gt;
&lt;br /&gt;
=== Score Aggregation Flow ===&lt;br /&gt;
* &amp;lt;code&amp;gt;Response#aggregate_questionnaire_score&amp;lt;/code&amp;gt; — item-weighted score for one submitted response&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; — normalizes a map's score per round and weights it by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score(maps)&amp;lt;/code&amp;gt; — weighted average across multiple maps, the single implementation shared by &amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Backend Implementation ==&lt;br /&gt;
&lt;br /&gt;
=== StudentTasksController ===&lt;br /&gt;
* &amp;lt;code&amp;gt;list&amp;lt;/code&amp;gt; — all &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; objects for the logged-in user&lt;br /&gt;
* &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt; — single task by participant ID with the unified timeline&lt;br /&gt;
* &amp;lt;code&amp;gt;team&amp;lt;/code&amp;gt; — teammates grouped by course name (&amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
=== GradesController — N+1 elimination ===&lt;br /&gt;
&amp;lt;code&amp;gt;get_team_scores&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;get_my_scores_data&amp;lt;/code&amp;gt; eager-load &amp;lt;code&amp;gt;responses: { scores: :item }&amp;lt;/code&amp;gt; on the &amp;lt;code&amp;gt;ReviewResponseMap&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;TeammateReviewResponseMap&amp;lt;/code&amp;gt; queries. &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt; filters SectionHeader scores via Ruby &amp;lt;code&amp;gt;.reject&amp;lt;/code&amp;gt; instead of &amp;lt;code&amp;gt;.joins&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;.where&amp;lt;/code&amp;gt; — calling &amp;lt;code&amp;gt;.where&amp;lt;/code&amp;gt; on an already-preloaded association silently bypasses the cache and re-queries the database. &amp;lt;code&amp;gt;get_answer&amp;lt;/code&amp;gt; now accepts the parent &amp;lt;code&amp;gt;response&amp;lt;/code&amp;gt; directly instead of re-fetching it via &amp;lt;code&amp;gt;score.response&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== insert_section_headers ===&lt;br /&gt;
Splices &amp;lt;code&amp;gt;{ type: &amp;quot;header&amp;quot;, txt: &amp;quot;...&amp;quot; }&amp;lt;/code&amp;gt; sentinel hashes into a round's scores array at positions matching each &amp;lt;code&amp;gt;SectionHeader&amp;lt;/code&amp;gt; item in the questionnaire's sequence, so the frontend can render heading rows between score rows without those rows affecting numbering.&lt;br /&gt;
&lt;br /&gt;
=== Review Grade Calculation ===&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; — latest submitted response per round, normalized (&amp;lt;code&amp;gt;score/max&amp;lt;/code&amp;gt;), weighted by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;ResponseMap.reviewer_reputation_for&amp;lt;/code&amp;gt; — shared weighted-average logic; reviewer reputation currently defaults to &amp;lt;code&amp;gt;1.0&amp;lt;/code&amp;gt; (placeholder for a future review-quality grader integration)&lt;br /&gt;
* &amp;lt;code&amp;gt;Questionnaire#total_item_weight&amp;lt;/code&amp;gt; — pre-calculates the sum of scored item weights, excluding &amp;lt;code&amp;gt;SectionHeader&amp;lt;/code&amp;gt; items&lt;br /&gt;
&lt;br /&gt;
=== Comprehensive Testing ===&lt;br /&gt;
* Model specs for &amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;create_from_participant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_events_for_assignment&amp;lt;/code&amp;gt;&lt;br /&gt;
* Request specs for &amp;lt;code&amp;gt;student_tasks/list&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;student_tasks/show/:id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;student_tasks/team&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;grades/:id/view_our_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
* Routing specs verifying the above routes resolve correctly&lt;br /&gt;
&lt;br /&gt;
== Frontend Implementation ==&lt;br /&gt;
[[File:student_view_assignments_page.png|600px|thumb|right|Student View Assignments Page]]&lt;br /&gt;
[[File:student_view_assignment_detail_page.png|600px|thumb|right|Student View Assignment Detail Page]]&lt;br /&gt;
[[File:student_view_assignment_grades_page.png|600px|thumb|right|Student View Assignment View Team Grades]]&lt;br /&gt;
[[File:student_view_assignment_feedback_page.png|600px|thumb|right|Student View Assignment View Feedbacks]]&lt;br /&gt;
=== New/Updated Files ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! File !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTasks.tsx&amp;lt;/code&amp;gt; || Course-grouped assignment dashboard (rewritten from a topic sign-up sheet)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTasksList.tsx&amp;lt;/code&amp;gt; || Sidebar — not-started tasks, revisions, teammates by course&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTaskDetail.tsx&amp;lt;/code&amp;gt; || Per-assignment detail page — timeline, task links, conditional &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; rendering&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ViewTeamGrades.module.scss&amp;lt;/code&amp;gt; || CSS Modules conversion; scoped &amp;lt;code&amp;gt;c1&amp;lt;/code&amp;gt;–&amp;lt;code&amp;gt;c5&amp;lt;/code&amp;gt; color classes, sticky-column and tooltip styles&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewTable.tsx&amp;lt;/code&amp;gt; || Single-call heatgrid; Scores/Feedback toggle; sticky SectionHeader rows&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewTableRow.tsx&amp;lt;/code&amp;gt; || Sticky &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;/Question columns; reduced row height; hover tooltip via &amp;lt;code&amp;gt;data-question&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;FeedbackTable.tsx&amp;lt;/code&amp;gt; || Feedback-mode table mirroring &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt;'s sticky-column layout&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;heatgridUtils.ts&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;RoundRow&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;isHeader()&amp;lt;/code&amp;gt; types; fixed &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; numbering to skip SectionHeader sentinels&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== StudentTasks (List View) ===&lt;br /&gt;
* Single &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; call, grouped by course&lt;br /&gt;
* Fixed: &amp;quot;Assignments&amp;quot; heading was rendering as body text because its class was referenced as a plain string instead of the CSS Modules export (&amp;lt;code&amp;gt;styles['assignments-title']&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
=== StudentTaskDetail (Detail View) ===&lt;br /&gt;
* Visual timeline with progress line and stage nodes (completed/current/pending)&lt;br /&gt;
* Task links list — &amp;quot;Your work&amp;quot; and &amp;quot;Others' work&amp;quot; conditionally rendered based on &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;quot;Send Email To Reviewers&amp;quot; button repositioned to align with the task list rather than floating disconnected at the page top&lt;br /&gt;
&lt;br /&gt;
=== ViewTeamGrades (Heatgrid) ===&lt;br /&gt;
* Consolidated ~6 sequential API calls into one &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
* SectionHeader rows split into a sticky label cell + scrolling spacer cell so they stay fixed during horizontal scroll&lt;br /&gt;
* Score cells show reviewer feedback as a hover tooltip&lt;br /&gt;
* Round headings simplified from &amp;quot;Review (Round: 1 of 2)&amp;quot; to &amp;quot;Round 1&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Design Principles ==&lt;br /&gt;
&lt;br /&gt;
=== Single Responsibility ===&lt;br /&gt;
Score aggregation is layered: &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; owns item-level scoring, &amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt; owns per-map/per-round weighting, &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; owns cross-map averaging — each level only knows about the one below it.&lt;br /&gt;
&lt;br /&gt;
=== Don't Repeat Yourself ===&lt;br /&gt;
The weighted-average logic that was duplicated between &amp;lt;code&amp;gt;AssignmentTeam#aggregate_review_grade&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;ReviewAggregator#compute_average_review_score&amp;lt;/code&amp;gt; was consolidated into a single &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt;, and the now-dead &amp;lt;code&amp;gt;ReviewAggregator&amp;lt;/code&amp;gt; concern was deleted.&lt;br /&gt;
&lt;br /&gt;
=== Performance by Design ===&lt;br /&gt;
Eager loading (&amp;lt;code&amp;gt;.includes&amp;lt;/code&amp;gt;) and in-memory filtering (&amp;lt;code&amp;gt;.reject&amp;lt;/code&amp;gt;) were chosen deliberately over additional database round-trips after profiling showed the heatgrid endpoint firing hundreds of queries under the old implementation.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
&lt;br /&gt;
=== Backend Tests ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! # !! Test !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| 1 || &amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt; groups teammates by course || Returns &amp;lt;code&amp;gt;{course_name =&amp;gt; [fullnames]}&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 2 || &amp;lt;code&amp;gt;all_teammates&amp;lt;/code&amp;gt; excludes calibrated assignments || Calibrated-assignment teammates excluded&lt;br /&gt;
|-&lt;br /&gt;
| 3 || &amp;lt;code&amp;gt;all_teammates&amp;lt;/code&amp;gt; excludes the user themself || Solo team returns empty hash&lt;br /&gt;
|-&lt;br /&gt;
| 4 || &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; returns all tasks for user || 200 OK with task array&lt;br /&gt;
|-&lt;br /&gt;
| 5 || &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt; returns due_dates timeline || 200 OK with &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt; array&lt;br /&gt;
|-&lt;br /&gt;
| 6 || &amp;lt;code&amp;gt;GET /student_tasks/team&amp;lt;/code&amp;gt; groups teammates by course || 200 OK with grouped hash&lt;br /&gt;
|-&lt;br /&gt;
| 7 || Heatgrid &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; column excludes SectionHeaders from numbering || Sequential numbering skips header rows&lt;br /&gt;
|-&lt;br /&gt;
| 8 || &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt; filters SectionHeaders without re-querying || No additional SQL fired against preloaded association&lt;br /&gt;
|-&lt;br /&gt;
| 9 || &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; weight validation || &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt; must be 0 when rubric has no scored questions&lt;br /&gt;
|-&lt;br /&gt;
| 10 || &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; || Weighted average matches Σ(grade × weight) / Σ(weight)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Frontend Tests ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! # !! Test !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| 1 || &amp;lt;code&amp;gt;StudentTasks&amp;lt;/code&amp;gt; renders &amp;quot;Assignments&amp;quot; heading with correct styling || Heading uses module class, renders bold and large&lt;br /&gt;
|-&lt;br /&gt;
| 2 || &amp;lt;code&amp;gt;StudentTaskDetail&amp;lt;/code&amp;gt; hides &amp;quot;Your work&amp;quot; when &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; is false || List item not rendered&lt;br /&gt;
|-&lt;br /&gt;
| 3 || &amp;lt;code&amp;gt;StudentTaskDetail&amp;lt;/code&amp;gt; hides &amp;quot;Others' work&amp;quot; when &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; is false || List item not rendered&lt;br /&gt;
|-&lt;br /&gt;
| 4 || &amp;lt;code&amp;gt;heatgridUtils&amp;lt;/code&amp;gt; itemNumber skips SectionHeader rows || Numbering matches scored items only&lt;br /&gt;
|-&lt;br /&gt;
| 5 || &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt; SectionHeader row stays fixed during horizontal scroll || Header cell &amp;lt;code&amp;gt;position: sticky&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 6 || &amp;lt;code&amp;gt;ReviewTableRow&amp;lt;/code&amp;gt; shows tooltip only when a comment exists || No tooltip box when &amp;lt;code&amp;gt;data-question&amp;lt;/code&amp;gt; is undefined&lt;br /&gt;
|-&lt;br /&gt;
| 7 || Heatgrid column borders render as &amp;lt;code&amp;gt;#ddd&amp;lt;/code&amp;gt; || No black borders after &amp;lt;code&amp;gt;tbl_heat&amp;lt;/code&amp;gt; class applied&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Relevant Links ==&lt;br /&gt;
&lt;br /&gt;
=== Repos ===&lt;br /&gt;
* https://github.com/expertiza/reimplementation-back-end&lt;br /&gt;
* https://github.com/expertiza/reimplementation-front-end&lt;br /&gt;
&lt;br /&gt;
=== Pull Requests ===&lt;br /&gt;
* Backend: https://github.com/expertiza/reimplementation-back-end/pull/352&lt;br /&gt;
* Frontend: https://github.com/expertiza/reimplementation-front-end/pull/182&lt;br /&gt;
&lt;br /&gt;
=== Demo Video ===&lt;br /&gt;
* (add link once recorded)&lt;br /&gt;
&lt;br /&gt;
== Developer Guidance ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Setup &amp;amp; Run (via Docker) ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
docker compose up&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Within the container ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
bundle exec rspec spec/models/student_task_spec.rb&lt;br /&gt;
bundle exec rspec spec/requests/api/v1/student_tasks_controller_spec.rb&lt;br /&gt;
bundle exec rspec spec/requests/api/v1/grades_controller_spec.rb&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
==== Setup &amp;amp; Run ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm install&lt;br /&gt;
npm start&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Running Frontend Tests ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm test -- StudentTaskDetail   # run a single suite&lt;br /&gt;
npm test                        # run full suite&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Future Scope ==&lt;br /&gt;
&lt;br /&gt;
=== Reviewer reputation-weighted grading ===&lt;br /&gt;
Integrate an external review-quality grader (e.g. an LLM-based reviewer grader) so &amp;lt;code&amp;gt;ResponseMap.reviewer_reputation_for&amp;lt;/code&amp;gt; reflects actual review quality instead of the current &amp;lt;code&amp;gt;1.0&amp;lt;/code&amp;gt; placeholder for every reviewer.&lt;br /&gt;
&lt;br /&gt;
=== Email-the-authors integration ===&lt;br /&gt;
Wire &amp;quot;Send Email To Reviewers&amp;quot; to a real backend endpoint instead of the current placeholder route.&lt;br /&gt;
&lt;br /&gt;
=== Dedicated StudentTask abstraction ===&lt;br /&gt;
&amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; remains a composed, non-persisted object. A future dedicated table could centralize derived state and simplify queries if the feature's scope grows further.&lt;br /&gt;
&lt;br /&gt;
== Mentor ==&lt;br /&gt;
[mailto:kgudipe@ncsu.edu Koushik Gudipelly]&lt;br /&gt;
&lt;br /&gt;
== Members ==&lt;br /&gt;
[mailto:rsgopara@ncsu.edu Ravi Goparaju]&lt;br /&gt;
[mailto:xmi@ncsu.edu Xiangjun Mi]&lt;br /&gt;
&lt;br /&gt;
== Completed by ==&lt;br /&gt;
[mailto:blalu@ncsu.edu Bestin Lalu]&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2602._Reimplement_student_task_view&amp;diff=168230</id>
		<title>CSC/ECE 517 Spring 2026 - E2602. Reimplement student task view</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2602._Reimplement_student_task_view&amp;diff=168230"/>
		<updated>2026-07-02T23:24:49Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* As an instructor, I want review grades to reflect each rubric's configured weight */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= CSC/ECE 517 Spring 2026 - E2602. Reimplement Student Task View =&lt;br /&gt;
&lt;br /&gt;
== About Expertiza ==&lt;br /&gt;
Expertiza is a robust, open-source web platform designed to empower educational environments that emphasize project-based learning. It streamlines collaborative activities including peer reviews, surveys, and team assignments — giving instructors tools to manage assignments and track performance, and giving students a single place to submit work, review peers, and track feedback.&lt;br /&gt;
&lt;br /&gt;
== Introduction ==&lt;br /&gt;
This iteration of E2602 hardens and extends the Student Task View: consolidating the student dashboard and detail pages into single-call REST endpoints, eliminating N+1 queries in the peer-review score heatgrid, and fixing several display/permission bugs surfaced by manual QA against real assignment data.&lt;br /&gt;
&lt;br /&gt;
=== Tech Stack ===&lt;br /&gt;
* '''Backend:''' Ruby on Rails (API-only)&lt;br /&gt;
* '''Frontend:''' React + TypeScript&lt;br /&gt;
* '''Database:''' MySQL&lt;br /&gt;
* '''Backend Testing:''' RSpec&lt;br /&gt;
* '''Frontend Testing:''' Jest, React Testing Library&lt;br /&gt;
&lt;br /&gt;
=== Key Improvements ===&lt;br /&gt;
* Eliminated N+1 queries in the score heatgrid (&amp;lt;code&amp;gt;get_team_scores&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_my_scores_data&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_answer&amp;lt;/code&amp;gt;) via &amp;lt;code&amp;gt;.includes(responses: { scores: :item })&amp;lt;/code&amp;gt; and in-memory filtering instead of re-querying preloaded associations&lt;br /&gt;
* Converted &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt;'s stylesheet to CSS Modules, fixing a Bootstrap class collision and scoping color classes (&amp;lt;code&amp;gt;c1&amp;lt;/code&amp;gt;–&amp;lt;code&amp;gt;c5&amp;lt;/code&amp;gt;) correctly&lt;br /&gt;
* Redesigned the score/feedback heatgrid with sticky &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;/Question columns and a sticky, full-width SectionHeader row that stays fixed during horizontal scroll&lt;br /&gt;
* Added hover tooltips on score cells showing the reviewer's written feedback&lt;br /&gt;
* Rewrote &amp;lt;code&amp;gt;StudentTasks&amp;lt;/code&amp;gt; from a (mis-implemented) topic sign-up sheet into a proper course-grouped assignment dashboard&lt;br /&gt;
* Added participant-level &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; permission checks, hiding &amp;quot;Your work&amp;quot; / &amp;quot;Others' work&amp;quot; task links accordingly&lt;br /&gt;
* Refactored review grade calculation: &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; now weights each round by its &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;; &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; centralizes the weighted-average logic previously duplicated across &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Problem Statement ==&lt;br /&gt;
The previous Student Task View had several concrete defects:&lt;br /&gt;
* The score heatgrid fired one SQL query per response/score across all reviewers and rounds, producing hundreds of queries for a single page load&lt;br /&gt;
* SectionHeader rubric rows (non-scorable dividers) were incorrectly counted in the &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; column numbering and lost their fixed position during horizontal scroll&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTasks.tsx&amp;lt;/code&amp;gt; was previously implemented as a topic sign-up sheet, not a task dashboard, and required a full rewrite&lt;br /&gt;
* The page did not respect participant-level &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; flags — all task links were always shown regardless of permission&lt;br /&gt;
* Review grade calculation treated every reviewer and every round equally, ignoring each rubric's configured &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== User Stories ==&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want to see all my assignments in one place ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; returns all tasks for the logged-in student in one call&lt;br /&gt;
* Tasks are grouped by course on the dashboard&lt;br /&gt;
* Each task shows current stage and days remaining, if applicable&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want to view a single assignment's deadlines and details ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt; returns the task plus a unified &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt; timeline&lt;br /&gt;
* The timeline visually distinguishes completed, current, and pending stages&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/team&amp;lt;/code&amp;gt; returns teammates grouped by course&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want my team's score heatgrid to load fast and stay readable while scrolling ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* The heatgrid loads from a single &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt; call&lt;br /&gt;
* The first two columns (item number &amp;quot;&amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;&amp;quot; and Item) remain fixed during horizontal scroll, along with section headers&lt;br /&gt;
* Hovering over a score with a comment shows the corresponding reviewer's feedback as a tooltip&lt;br /&gt;
&lt;br /&gt;
=== A student who isn't permitted to submit or review will not see those task links ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;quot;Your work&amp;quot; is hidden when &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;quot;Others' work&amp;quot; is hidden when &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== As an instructor, I want calculated review grades to reflect each rubric's configured weight ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; weights each round's normalized score by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt; both compute their averages through the same shared &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
=== Student Task Overview ===&lt;br /&gt;
No dedicated &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; database table exists. &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; is a plain Ruby object composed from &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; and its associated &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignedUpTeam&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;ProjectTopic&amp;lt;/code&amp;gt;, and permission fields (&amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;permission_granted&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Request / Response Flow ====&lt;br /&gt;
* Student navigates to &amp;lt;code&amp;gt;/student_tasks&amp;lt;/code&amp;gt; → frontend calls &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTask.tasks(user)&amp;lt;/code&amp;gt; preloads &amp;lt;code&amp;gt;assignment: :course&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;:user&amp;lt;/code&amp;gt; associations, then maps each &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; through &amp;lt;code&amp;gt;StudentTask.create_from_participant&amp;lt;/code&amp;gt;&lt;br /&gt;
* Selecting an assignment navigates to the detail route → frontend calls &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTask.get_events_for_assignment&amp;lt;/code&amp;gt; merges due dates with submitted review/feedback activity into one sorted timeline&lt;br /&gt;
* For the score heatgrid: frontend calls &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt; → &amp;lt;code&amp;gt;GradesController#get_team_scores&amp;lt;/code&amp;gt; eager-loads &amp;lt;code&amp;gt;responses: { scores: :item }&amp;lt;/code&amp;gt; and calls &amp;lt;code&amp;gt;insert_section_headers&amp;lt;/code&amp;gt; to splice header sentinels into the response&lt;br /&gt;
&lt;br /&gt;
=== Score Aggregation Flow ===&lt;br /&gt;
* &amp;lt;code&amp;gt;Response#aggregate_questionnaire_score&amp;lt;/code&amp;gt; — item-weighted score for one submitted response&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; — normalizes a map's score per round and weights it by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score(maps)&amp;lt;/code&amp;gt; — weighted average across multiple maps, the single implementation shared by &amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Backend Implementation ==&lt;br /&gt;
&lt;br /&gt;
=== StudentTasksController ===&lt;br /&gt;
* &amp;lt;code&amp;gt;list&amp;lt;/code&amp;gt; — all &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; objects for the logged-in user&lt;br /&gt;
* &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt; — single task by participant ID with the unified timeline&lt;br /&gt;
* &amp;lt;code&amp;gt;team&amp;lt;/code&amp;gt; — teammates grouped by course name (&amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
=== GradesController — N+1 elimination ===&lt;br /&gt;
&amp;lt;code&amp;gt;get_team_scores&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;get_my_scores_data&amp;lt;/code&amp;gt; eager-load &amp;lt;code&amp;gt;responses: { scores: :item }&amp;lt;/code&amp;gt; on the &amp;lt;code&amp;gt;ReviewResponseMap&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;TeammateReviewResponseMap&amp;lt;/code&amp;gt; queries. &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt; filters SectionHeader scores via Ruby &amp;lt;code&amp;gt;.reject&amp;lt;/code&amp;gt; instead of &amp;lt;code&amp;gt;.joins&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;.where&amp;lt;/code&amp;gt; — calling &amp;lt;code&amp;gt;.where&amp;lt;/code&amp;gt; on an already-preloaded association silently bypasses the cache and re-queries the database. &amp;lt;code&amp;gt;get_answer&amp;lt;/code&amp;gt; now accepts the parent &amp;lt;code&amp;gt;response&amp;lt;/code&amp;gt; directly instead of re-fetching it via &amp;lt;code&amp;gt;score.response&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== insert_section_headers ===&lt;br /&gt;
Splices &amp;lt;code&amp;gt;{ type: &amp;quot;header&amp;quot;, txt: &amp;quot;...&amp;quot; }&amp;lt;/code&amp;gt; sentinel hashes into a round's scores array at positions matching each &amp;lt;code&amp;gt;SectionHeader&amp;lt;/code&amp;gt; item in the questionnaire's sequence, so the frontend can render heading rows between score rows without those rows affecting numbering.&lt;br /&gt;
&lt;br /&gt;
=== Review Grade Calculation ===&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; — latest submitted response per round, normalized (&amp;lt;code&amp;gt;score/max&amp;lt;/code&amp;gt;), weighted by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;ResponseMap.reviewer_reputation_for&amp;lt;/code&amp;gt; — shared weighted-average logic; reviewer reputation currently defaults to &amp;lt;code&amp;gt;1.0&amp;lt;/code&amp;gt; (placeholder for a future review-quality grader integration)&lt;br /&gt;
* &amp;lt;code&amp;gt;Questionnaire#total_item_weight&amp;lt;/code&amp;gt; — pre-calculates the sum of scored item weights, excluding &amp;lt;code&amp;gt;SectionHeader&amp;lt;/code&amp;gt; items&lt;br /&gt;
&lt;br /&gt;
=== Comprehensive Testing ===&lt;br /&gt;
* Model specs for &amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;create_from_participant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_events_for_assignment&amp;lt;/code&amp;gt;&lt;br /&gt;
* Request specs for &amp;lt;code&amp;gt;student_tasks/list&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;student_tasks/show/:id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;student_tasks/team&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;grades/:id/view_our_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
* Routing specs verifying the above routes resolve correctly&lt;br /&gt;
&lt;br /&gt;
== Frontend Implementation ==&lt;br /&gt;
[[File:student_view_assignments_page.png|600px|thumb|right|Student View Assignments Page]]&lt;br /&gt;
[[File:student_view_assignment_detail_page.png|600px|thumb|right|Student View Assignment Detail Page]]&lt;br /&gt;
[[File:student_view_assignment_grades_page.png|600px|thumb|right|Student View Assignment View Team Grades]]&lt;br /&gt;
[[File:student_view_assignment_feedback_page.png|600px|thumb|right|Student View Assignment View Feedbacks]]&lt;br /&gt;
=== New/Updated Files ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! File !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTasks.tsx&amp;lt;/code&amp;gt; || Course-grouped assignment dashboard (rewritten from a topic sign-up sheet)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTasksList.tsx&amp;lt;/code&amp;gt; || Sidebar — not-started tasks, revisions, teammates by course&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTaskDetail.tsx&amp;lt;/code&amp;gt; || Per-assignment detail page — timeline, task links, conditional &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; rendering&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ViewTeamGrades.module.scss&amp;lt;/code&amp;gt; || CSS Modules conversion; scoped &amp;lt;code&amp;gt;c1&amp;lt;/code&amp;gt;–&amp;lt;code&amp;gt;c5&amp;lt;/code&amp;gt; color classes, sticky-column and tooltip styles&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewTable.tsx&amp;lt;/code&amp;gt; || Single-call heatgrid; Scores/Feedback toggle; sticky SectionHeader rows&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewTableRow.tsx&amp;lt;/code&amp;gt; || Sticky &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;/Question columns; reduced row height; hover tooltip via &amp;lt;code&amp;gt;data-question&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;FeedbackTable.tsx&amp;lt;/code&amp;gt; || Feedback-mode table mirroring &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt;'s sticky-column layout&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;heatgridUtils.ts&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;RoundRow&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;isHeader()&amp;lt;/code&amp;gt; types; fixed &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; numbering to skip SectionHeader sentinels&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== StudentTasks (List View) ===&lt;br /&gt;
* Single &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; call, grouped by course&lt;br /&gt;
* Fixed: &amp;quot;Assignments&amp;quot; heading was rendering as body text because its class was referenced as a plain string instead of the CSS Modules export (&amp;lt;code&amp;gt;styles['assignments-title']&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
=== StudentTaskDetail (Detail View) ===&lt;br /&gt;
* Visual timeline with progress line and stage nodes (completed/current/pending)&lt;br /&gt;
* Task links list — &amp;quot;Your work&amp;quot; and &amp;quot;Others' work&amp;quot; conditionally rendered based on &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;quot;Send Email To Reviewers&amp;quot; button repositioned to align with the task list rather than floating disconnected at the page top&lt;br /&gt;
&lt;br /&gt;
=== ViewTeamGrades (Heatgrid) ===&lt;br /&gt;
* Consolidated ~6 sequential API calls into one &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
* SectionHeader rows split into a sticky label cell + scrolling spacer cell so they stay fixed during horizontal scroll&lt;br /&gt;
* Score cells show reviewer feedback as a hover tooltip&lt;br /&gt;
* Round headings simplified from &amp;quot;Review (Round: 1 of 2)&amp;quot; to &amp;quot;Round 1&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Design Principles ==&lt;br /&gt;
&lt;br /&gt;
=== Single Responsibility ===&lt;br /&gt;
Score aggregation is layered: &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; owns item-level scoring, &amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt; owns per-map/per-round weighting, &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; owns cross-map averaging — each level only knows about the one below it.&lt;br /&gt;
&lt;br /&gt;
=== Don't Repeat Yourself ===&lt;br /&gt;
The weighted-average logic that was duplicated between &amp;lt;code&amp;gt;AssignmentTeam#aggregate_review_grade&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;ReviewAggregator#compute_average_review_score&amp;lt;/code&amp;gt; was consolidated into a single &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt;, and the now-dead &amp;lt;code&amp;gt;ReviewAggregator&amp;lt;/code&amp;gt; concern was deleted.&lt;br /&gt;
&lt;br /&gt;
=== Performance by Design ===&lt;br /&gt;
Eager loading (&amp;lt;code&amp;gt;.includes&amp;lt;/code&amp;gt;) and in-memory filtering (&amp;lt;code&amp;gt;.reject&amp;lt;/code&amp;gt;) were chosen deliberately over additional database round-trips after profiling showed the heatgrid endpoint firing hundreds of queries under the old implementation.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
&lt;br /&gt;
=== Backend Tests ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! # !! Test !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| 1 || &amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt; groups teammates by course || Returns &amp;lt;code&amp;gt;{course_name =&amp;gt; [fullnames]}&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 2 || &amp;lt;code&amp;gt;all_teammates&amp;lt;/code&amp;gt; excludes calibrated assignments || Calibrated-assignment teammates excluded&lt;br /&gt;
|-&lt;br /&gt;
| 3 || &amp;lt;code&amp;gt;all_teammates&amp;lt;/code&amp;gt; excludes the user themself || Solo team returns empty hash&lt;br /&gt;
|-&lt;br /&gt;
| 4 || &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; returns all tasks for user || 200 OK with task array&lt;br /&gt;
|-&lt;br /&gt;
| 5 || &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt; returns due_dates timeline || 200 OK with &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt; array&lt;br /&gt;
|-&lt;br /&gt;
| 6 || &amp;lt;code&amp;gt;GET /student_tasks/team&amp;lt;/code&amp;gt; groups teammates by course || 200 OK with grouped hash&lt;br /&gt;
|-&lt;br /&gt;
| 7 || Heatgrid &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; column excludes SectionHeaders from numbering || Sequential numbering skips header rows&lt;br /&gt;
|-&lt;br /&gt;
| 8 || &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt; filters SectionHeaders without re-querying || No additional SQL fired against preloaded association&lt;br /&gt;
|-&lt;br /&gt;
| 9 || &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; weight validation || &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt; must be 0 when rubric has no scored questions&lt;br /&gt;
|-&lt;br /&gt;
| 10 || &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; || Weighted average matches Σ(grade × weight) / Σ(weight)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Frontend Tests ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! # !! Test !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| 1 || &amp;lt;code&amp;gt;StudentTasks&amp;lt;/code&amp;gt; renders &amp;quot;Assignments&amp;quot; heading with correct styling || Heading uses module class, renders bold and large&lt;br /&gt;
|-&lt;br /&gt;
| 2 || &amp;lt;code&amp;gt;StudentTaskDetail&amp;lt;/code&amp;gt; hides &amp;quot;Your work&amp;quot; when &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; is false || List item not rendered&lt;br /&gt;
|-&lt;br /&gt;
| 3 || &amp;lt;code&amp;gt;StudentTaskDetail&amp;lt;/code&amp;gt; hides &amp;quot;Others' work&amp;quot; when &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; is false || List item not rendered&lt;br /&gt;
|-&lt;br /&gt;
| 4 || &amp;lt;code&amp;gt;heatgridUtils&amp;lt;/code&amp;gt; itemNumber skips SectionHeader rows || Numbering matches scored items only&lt;br /&gt;
|-&lt;br /&gt;
| 5 || &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt; SectionHeader row stays fixed during horizontal scroll || Header cell &amp;lt;code&amp;gt;position: sticky&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 6 || &amp;lt;code&amp;gt;ReviewTableRow&amp;lt;/code&amp;gt; shows tooltip only when a comment exists || No tooltip box when &amp;lt;code&amp;gt;data-question&amp;lt;/code&amp;gt; is undefined&lt;br /&gt;
|-&lt;br /&gt;
| 7 || Heatgrid column borders render as &amp;lt;code&amp;gt;#ddd&amp;lt;/code&amp;gt; || No black borders after &amp;lt;code&amp;gt;tbl_heat&amp;lt;/code&amp;gt; class applied&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Relevant Links ==&lt;br /&gt;
&lt;br /&gt;
=== Repos ===&lt;br /&gt;
* https://github.com/expertiza/reimplementation-back-end&lt;br /&gt;
* https://github.com/expertiza/reimplementation-front-end&lt;br /&gt;
&lt;br /&gt;
=== Pull Requests ===&lt;br /&gt;
* Backend: https://github.com/expertiza/reimplementation-back-end/pull/352&lt;br /&gt;
* Frontend: https://github.com/expertiza/reimplementation-front-end/pull/182&lt;br /&gt;
&lt;br /&gt;
=== Demo Video ===&lt;br /&gt;
* (add link once recorded)&lt;br /&gt;
&lt;br /&gt;
== Developer Guidance ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Setup &amp;amp; Run (via Docker) ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
docker compose up&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Within the container ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
bundle exec rspec spec/models/student_task_spec.rb&lt;br /&gt;
bundle exec rspec spec/requests/api/v1/student_tasks_controller_spec.rb&lt;br /&gt;
bundle exec rspec spec/requests/api/v1/grades_controller_spec.rb&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
==== Setup &amp;amp; Run ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm install&lt;br /&gt;
npm start&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Running Frontend Tests ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm test -- StudentTaskDetail   # run a single suite&lt;br /&gt;
npm test                        # run full suite&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Future Scope ==&lt;br /&gt;
&lt;br /&gt;
=== Reviewer reputation-weighted grading ===&lt;br /&gt;
Integrate an external review-quality grader (e.g. an LLM-based reviewer grader) so &amp;lt;code&amp;gt;ResponseMap.reviewer_reputation_for&amp;lt;/code&amp;gt; reflects actual review quality instead of the current &amp;lt;code&amp;gt;1.0&amp;lt;/code&amp;gt; placeholder for every reviewer.&lt;br /&gt;
&lt;br /&gt;
=== Email-the-authors integration ===&lt;br /&gt;
Wire &amp;quot;Send Email To Reviewers&amp;quot; to a real backend endpoint instead of the current placeholder route.&lt;br /&gt;
&lt;br /&gt;
=== Dedicated StudentTask abstraction ===&lt;br /&gt;
&amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; remains a composed, non-persisted object. A future dedicated table could centralize derived state and simplify queries if the feature's scope grows further.&lt;br /&gt;
&lt;br /&gt;
== Mentor ==&lt;br /&gt;
[mailto:kgudipe@ncsu.edu Koushik Gudipelly]&lt;br /&gt;
&lt;br /&gt;
== Members ==&lt;br /&gt;
[mailto:rsgopara@ncsu.edu Ravi Goparaju]&lt;br /&gt;
[mailto:xmi@ncsu.edu Xiangjun Mi]&lt;br /&gt;
&lt;br /&gt;
== Completed by ==&lt;br /&gt;
[mailto:blalu@ncsu.edu Bestin Lalu]&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2602._Reimplement_student_task_view&amp;diff=168229</id>
		<title>CSC/ECE 517 Spring 2026 - E2602. Reimplement student task view</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2602._Reimplement_student_task_view&amp;diff=168229"/>
		<updated>2026-07-02T23:23:18Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* As a student who isn't permitted to submit or review, I should not see those task links */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= CSC/ECE 517 Spring 2026 - E2602. Reimplement Student Task View =&lt;br /&gt;
&lt;br /&gt;
== About Expertiza ==&lt;br /&gt;
Expertiza is a robust, open-source web platform designed to empower educational environments that emphasize project-based learning. It streamlines collaborative activities including peer reviews, surveys, and team assignments — giving instructors tools to manage assignments and track performance, and giving students a single place to submit work, review peers, and track feedback.&lt;br /&gt;
&lt;br /&gt;
== Introduction ==&lt;br /&gt;
This iteration of E2602 hardens and extends the Student Task View: consolidating the student dashboard and detail pages into single-call REST endpoints, eliminating N+1 queries in the peer-review score heatgrid, and fixing several display/permission bugs surfaced by manual QA against real assignment data.&lt;br /&gt;
&lt;br /&gt;
=== Tech Stack ===&lt;br /&gt;
* '''Backend:''' Ruby on Rails (API-only)&lt;br /&gt;
* '''Frontend:''' React + TypeScript&lt;br /&gt;
* '''Database:''' MySQL&lt;br /&gt;
* '''Backend Testing:''' RSpec&lt;br /&gt;
* '''Frontend Testing:''' Jest, React Testing Library&lt;br /&gt;
&lt;br /&gt;
=== Key Improvements ===&lt;br /&gt;
* Eliminated N+1 queries in the score heatgrid (&amp;lt;code&amp;gt;get_team_scores&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_my_scores_data&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_answer&amp;lt;/code&amp;gt;) via &amp;lt;code&amp;gt;.includes(responses: { scores: :item })&amp;lt;/code&amp;gt; and in-memory filtering instead of re-querying preloaded associations&lt;br /&gt;
* Converted &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt;'s stylesheet to CSS Modules, fixing a Bootstrap class collision and scoping color classes (&amp;lt;code&amp;gt;c1&amp;lt;/code&amp;gt;–&amp;lt;code&amp;gt;c5&amp;lt;/code&amp;gt;) correctly&lt;br /&gt;
* Redesigned the score/feedback heatgrid with sticky &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;/Question columns and a sticky, full-width SectionHeader row that stays fixed during horizontal scroll&lt;br /&gt;
* Added hover tooltips on score cells showing the reviewer's written feedback&lt;br /&gt;
* Rewrote &amp;lt;code&amp;gt;StudentTasks&amp;lt;/code&amp;gt; from a (mis-implemented) topic sign-up sheet into a proper course-grouped assignment dashboard&lt;br /&gt;
* Added participant-level &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; permission checks, hiding &amp;quot;Your work&amp;quot; / &amp;quot;Others' work&amp;quot; task links accordingly&lt;br /&gt;
* Refactored review grade calculation: &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; now weights each round by its &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;; &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; centralizes the weighted-average logic previously duplicated across &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Problem Statement ==&lt;br /&gt;
The previous Student Task View had several concrete defects:&lt;br /&gt;
* The score heatgrid fired one SQL query per response/score across all reviewers and rounds, producing hundreds of queries for a single page load&lt;br /&gt;
* SectionHeader rubric rows (non-scorable dividers) were incorrectly counted in the &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; column numbering and lost their fixed position during horizontal scroll&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTasks.tsx&amp;lt;/code&amp;gt; was previously implemented as a topic sign-up sheet, not a task dashboard, and required a full rewrite&lt;br /&gt;
* The page did not respect participant-level &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; flags — all task links were always shown regardless of permission&lt;br /&gt;
* Review grade calculation treated every reviewer and every round equally, ignoring each rubric's configured &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== User Stories ==&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want to see all my assignments in one place ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; returns all tasks for the logged-in student in one call&lt;br /&gt;
* Tasks are grouped by course on the dashboard&lt;br /&gt;
* Each task shows current stage and days remaining, if applicable&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want to view a single assignment's deadlines and details ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt; returns the task plus a unified &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt; timeline&lt;br /&gt;
* The timeline visually distinguishes completed, current, and pending stages&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/team&amp;lt;/code&amp;gt; returns teammates grouped by course&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want my team's score heatgrid to load fast and stay readable while scrolling ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* The heatgrid loads from a single &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt; call&lt;br /&gt;
* The first two columns (item number &amp;quot;&amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;&amp;quot; and Item) remain fixed during horizontal scroll, along with section headers&lt;br /&gt;
* Hovering over a score with a comment shows the corresponding reviewer's feedback as a tooltip&lt;br /&gt;
&lt;br /&gt;
=== A student who isn't permitted to submit or review will not see those task links ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;quot;Your work&amp;quot; is hidden when &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;quot;Others' work&amp;quot; is hidden when &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== As an instructor, I want review grades to reflect each rubric's configured weight ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; weights each round's normalized score by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt; both compute their averages through the same shared &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
=== Student Task Overview ===&lt;br /&gt;
No dedicated &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; database table exists. &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; is a plain Ruby object composed from &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; and its associated &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignedUpTeam&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;ProjectTopic&amp;lt;/code&amp;gt;, and permission fields (&amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;permission_granted&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Request / Response Flow ====&lt;br /&gt;
* Student navigates to &amp;lt;code&amp;gt;/student_tasks&amp;lt;/code&amp;gt; → frontend calls &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTask.tasks(user)&amp;lt;/code&amp;gt; preloads &amp;lt;code&amp;gt;assignment: :course&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;:user&amp;lt;/code&amp;gt; associations, then maps each &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; through &amp;lt;code&amp;gt;StudentTask.create_from_participant&amp;lt;/code&amp;gt;&lt;br /&gt;
* Selecting an assignment navigates to the detail route → frontend calls &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTask.get_events_for_assignment&amp;lt;/code&amp;gt; merges due dates with submitted review/feedback activity into one sorted timeline&lt;br /&gt;
* For the score heatgrid: frontend calls &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt; → &amp;lt;code&amp;gt;GradesController#get_team_scores&amp;lt;/code&amp;gt; eager-loads &amp;lt;code&amp;gt;responses: { scores: :item }&amp;lt;/code&amp;gt; and calls &amp;lt;code&amp;gt;insert_section_headers&amp;lt;/code&amp;gt; to splice header sentinels into the response&lt;br /&gt;
&lt;br /&gt;
=== Score Aggregation Flow ===&lt;br /&gt;
* &amp;lt;code&amp;gt;Response#aggregate_questionnaire_score&amp;lt;/code&amp;gt; — item-weighted score for one submitted response&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; — normalizes a map's score per round and weights it by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score(maps)&amp;lt;/code&amp;gt; — weighted average across multiple maps, the single implementation shared by &amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Backend Implementation ==&lt;br /&gt;
&lt;br /&gt;
=== StudentTasksController ===&lt;br /&gt;
* &amp;lt;code&amp;gt;list&amp;lt;/code&amp;gt; — all &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; objects for the logged-in user&lt;br /&gt;
* &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt; — single task by participant ID with the unified timeline&lt;br /&gt;
* &amp;lt;code&amp;gt;team&amp;lt;/code&amp;gt; — teammates grouped by course name (&amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
=== GradesController — N+1 elimination ===&lt;br /&gt;
&amp;lt;code&amp;gt;get_team_scores&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;get_my_scores_data&amp;lt;/code&amp;gt; eager-load &amp;lt;code&amp;gt;responses: { scores: :item }&amp;lt;/code&amp;gt; on the &amp;lt;code&amp;gt;ReviewResponseMap&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;TeammateReviewResponseMap&amp;lt;/code&amp;gt; queries. &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt; filters SectionHeader scores via Ruby &amp;lt;code&amp;gt;.reject&amp;lt;/code&amp;gt; instead of &amp;lt;code&amp;gt;.joins&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;.where&amp;lt;/code&amp;gt; — calling &amp;lt;code&amp;gt;.where&amp;lt;/code&amp;gt; on an already-preloaded association silently bypasses the cache and re-queries the database. &amp;lt;code&amp;gt;get_answer&amp;lt;/code&amp;gt; now accepts the parent &amp;lt;code&amp;gt;response&amp;lt;/code&amp;gt; directly instead of re-fetching it via &amp;lt;code&amp;gt;score.response&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== insert_section_headers ===&lt;br /&gt;
Splices &amp;lt;code&amp;gt;{ type: &amp;quot;header&amp;quot;, txt: &amp;quot;...&amp;quot; }&amp;lt;/code&amp;gt; sentinel hashes into a round's scores array at positions matching each &amp;lt;code&amp;gt;SectionHeader&amp;lt;/code&amp;gt; item in the questionnaire's sequence, so the frontend can render heading rows between score rows without those rows affecting numbering.&lt;br /&gt;
&lt;br /&gt;
=== Review Grade Calculation ===&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; — latest submitted response per round, normalized (&amp;lt;code&amp;gt;score/max&amp;lt;/code&amp;gt;), weighted by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;ResponseMap.reviewer_reputation_for&amp;lt;/code&amp;gt; — shared weighted-average logic; reviewer reputation currently defaults to &amp;lt;code&amp;gt;1.0&amp;lt;/code&amp;gt; (placeholder for a future review-quality grader integration)&lt;br /&gt;
* &amp;lt;code&amp;gt;Questionnaire#total_item_weight&amp;lt;/code&amp;gt; — pre-calculates the sum of scored item weights, excluding &amp;lt;code&amp;gt;SectionHeader&amp;lt;/code&amp;gt; items&lt;br /&gt;
&lt;br /&gt;
=== Comprehensive Testing ===&lt;br /&gt;
* Model specs for &amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;create_from_participant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_events_for_assignment&amp;lt;/code&amp;gt;&lt;br /&gt;
* Request specs for &amp;lt;code&amp;gt;student_tasks/list&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;student_tasks/show/:id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;student_tasks/team&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;grades/:id/view_our_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
* Routing specs verifying the above routes resolve correctly&lt;br /&gt;
&lt;br /&gt;
== Frontend Implementation ==&lt;br /&gt;
[[File:student_view_assignments_page.png|600px|thumb|right|Student View Assignments Page]]&lt;br /&gt;
[[File:student_view_assignment_detail_page.png|600px|thumb|right|Student View Assignment Detail Page]]&lt;br /&gt;
[[File:student_view_assignment_grades_page.png|600px|thumb|right|Student View Assignment View Team Grades]]&lt;br /&gt;
[[File:student_view_assignment_feedback_page.png|600px|thumb|right|Student View Assignment View Feedbacks]]&lt;br /&gt;
=== New/Updated Files ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! File !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTasks.tsx&amp;lt;/code&amp;gt; || Course-grouped assignment dashboard (rewritten from a topic sign-up sheet)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTasksList.tsx&amp;lt;/code&amp;gt; || Sidebar — not-started tasks, revisions, teammates by course&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTaskDetail.tsx&amp;lt;/code&amp;gt; || Per-assignment detail page — timeline, task links, conditional &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; rendering&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ViewTeamGrades.module.scss&amp;lt;/code&amp;gt; || CSS Modules conversion; scoped &amp;lt;code&amp;gt;c1&amp;lt;/code&amp;gt;–&amp;lt;code&amp;gt;c5&amp;lt;/code&amp;gt; color classes, sticky-column and tooltip styles&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewTable.tsx&amp;lt;/code&amp;gt; || Single-call heatgrid; Scores/Feedback toggle; sticky SectionHeader rows&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewTableRow.tsx&amp;lt;/code&amp;gt; || Sticky &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;/Question columns; reduced row height; hover tooltip via &amp;lt;code&amp;gt;data-question&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;FeedbackTable.tsx&amp;lt;/code&amp;gt; || Feedback-mode table mirroring &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt;'s sticky-column layout&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;heatgridUtils.ts&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;RoundRow&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;isHeader()&amp;lt;/code&amp;gt; types; fixed &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; numbering to skip SectionHeader sentinels&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== StudentTasks (List View) ===&lt;br /&gt;
* Single &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; call, grouped by course&lt;br /&gt;
* Fixed: &amp;quot;Assignments&amp;quot; heading was rendering as body text because its class was referenced as a plain string instead of the CSS Modules export (&amp;lt;code&amp;gt;styles['assignments-title']&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
=== StudentTaskDetail (Detail View) ===&lt;br /&gt;
* Visual timeline with progress line and stage nodes (completed/current/pending)&lt;br /&gt;
* Task links list — &amp;quot;Your work&amp;quot; and &amp;quot;Others' work&amp;quot; conditionally rendered based on &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;quot;Send Email To Reviewers&amp;quot; button repositioned to align with the task list rather than floating disconnected at the page top&lt;br /&gt;
&lt;br /&gt;
=== ViewTeamGrades (Heatgrid) ===&lt;br /&gt;
* Consolidated ~6 sequential API calls into one &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
* SectionHeader rows split into a sticky label cell + scrolling spacer cell so they stay fixed during horizontal scroll&lt;br /&gt;
* Score cells show reviewer feedback as a hover tooltip&lt;br /&gt;
* Round headings simplified from &amp;quot;Review (Round: 1 of 2)&amp;quot; to &amp;quot;Round 1&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Design Principles ==&lt;br /&gt;
&lt;br /&gt;
=== Single Responsibility ===&lt;br /&gt;
Score aggregation is layered: &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; owns item-level scoring, &amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt; owns per-map/per-round weighting, &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; owns cross-map averaging — each level only knows about the one below it.&lt;br /&gt;
&lt;br /&gt;
=== Don't Repeat Yourself ===&lt;br /&gt;
The weighted-average logic that was duplicated between &amp;lt;code&amp;gt;AssignmentTeam#aggregate_review_grade&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;ReviewAggregator#compute_average_review_score&amp;lt;/code&amp;gt; was consolidated into a single &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt;, and the now-dead &amp;lt;code&amp;gt;ReviewAggregator&amp;lt;/code&amp;gt; concern was deleted.&lt;br /&gt;
&lt;br /&gt;
=== Performance by Design ===&lt;br /&gt;
Eager loading (&amp;lt;code&amp;gt;.includes&amp;lt;/code&amp;gt;) and in-memory filtering (&amp;lt;code&amp;gt;.reject&amp;lt;/code&amp;gt;) were chosen deliberately over additional database round-trips after profiling showed the heatgrid endpoint firing hundreds of queries under the old implementation.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
&lt;br /&gt;
=== Backend Tests ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! # !! Test !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| 1 || &amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt; groups teammates by course || Returns &amp;lt;code&amp;gt;{course_name =&amp;gt; [fullnames]}&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 2 || &amp;lt;code&amp;gt;all_teammates&amp;lt;/code&amp;gt; excludes calibrated assignments || Calibrated-assignment teammates excluded&lt;br /&gt;
|-&lt;br /&gt;
| 3 || &amp;lt;code&amp;gt;all_teammates&amp;lt;/code&amp;gt; excludes the user themself || Solo team returns empty hash&lt;br /&gt;
|-&lt;br /&gt;
| 4 || &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; returns all tasks for user || 200 OK with task array&lt;br /&gt;
|-&lt;br /&gt;
| 5 || &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt; returns due_dates timeline || 200 OK with &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt; array&lt;br /&gt;
|-&lt;br /&gt;
| 6 || &amp;lt;code&amp;gt;GET /student_tasks/team&amp;lt;/code&amp;gt; groups teammates by course || 200 OK with grouped hash&lt;br /&gt;
|-&lt;br /&gt;
| 7 || Heatgrid &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; column excludes SectionHeaders from numbering || Sequential numbering skips header rows&lt;br /&gt;
|-&lt;br /&gt;
| 8 || &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt; filters SectionHeaders without re-querying || No additional SQL fired against preloaded association&lt;br /&gt;
|-&lt;br /&gt;
| 9 || &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; weight validation || &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt; must be 0 when rubric has no scored questions&lt;br /&gt;
|-&lt;br /&gt;
| 10 || &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; || Weighted average matches Σ(grade × weight) / Σ(weight)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Frontend Tests ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! # !! Test !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| 1 || &amp;lt;code&amp;gt;StudentTasks&amp;lt;/code&amp;gt; renders &amp;quot;Assignments&amp;quot; heading with correct styling || Heading uses module class, renders bold and large&lt;br /&gt;
|-&lt;br /&gt;
| 2 || &amp;lt;code&amp;gt;StudentTaskDetail&amp;lt;/code&amp;gt; hides &amp;quot;Your work&amp;quot; when &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; is false || List item not rendered&lt;br /&gt;
|-&lt;br /&gt;
| 3 || &amp;lt;code&amp;gt;StudentTaskDetail&amp;lt;/code&amp;gt; hides &amp;quot;Others' work&amp;quot; when &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; is false || List item not rendered&lt;br /&gt;
|-&lt;br /&gt;
| 4 || &amp;lt;code&amp;gt;heatgridUtils&amp;lt;/code&amp;gt; itemNumber skips SectionHeader rows || Numbering matches scored items only&lt;br /&gt;
|-&lt;br /&gt;
| 5 || &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt; SectionHeader row stays fixed during horizontal scroll || Header cell &amp;lt;code&amp;gt;position: sticky&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 6 || &amp;lt;code&amp;gt;ReviewTableRow&amp;lt;/code&amp;gt; shows tooltip only when a comment exists || No tooltip box when &amp;lt;code&amp;gt;data-question&amp;lt;/code&amp;gt; is undefined&lt;br /&gt;
|-&lt;br /&gt;
| 7 || Heatgrid column borders render as &amp;lt;code&amp;gt;#ddd&amp;lt;/code&amp;gt; || No black borders after &amp;lt;code&amp;gt;tbl_heat&amp;lt;/code&amp;gt; class applied&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Relevant Links ==&lt;br /&gt;
&lt;br /&gt;
=== Repos ===&lt;br /&gt;
* https://github.com/expertiza/reimplementation-back-end&lt;br /&gt;
* https://github.com/expertiza/reimplementation-front-end&lt;br /&gt;
&lt;br /&gt;
=== Pull Requests ===&lt;br /&gt;
* Backend: https://github.com/expertiza/reimplementation-back-end/pull/352&lt;br /&gt;
* Frontend: https://github.com/expertiza/reimplementation-front-end/pull/182&lt;br /&gt;
&lt;br /&gt;
=== Demo Video ===&lt;br /&gt;
* (add link once recorded)&lt;br /&gt;
&lt;br /&gt;
== Developer Guidance ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Setup &amp;amp; Run (via Docker) ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
docker compose up&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Within the container ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
bundle exec rspec spec/models/student_task_spec.rb&lt;br /&gt;
bundle exec rspec spec/requests/api/v1/student_tasks_controller_spec.rb&lt;br /&gt;
bundle exec rspec spec/requests/api/v1/grades_controller_spec.rb&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
==== Setup &amp;amp; Run ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm install&lt;br /&gt;
npm start&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Running Frontend Tests ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm test -- StudentTaskDetail   # run a single suite&lt;br /&gt;
npm test                        # run full suite&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Future Scope ==&lt;br /&gt;
&lt;br /&gt;
=== Reviewer reputation-weighted grading ===&lt;br /&gt;
Integrate an external review-quality grader (e.g. an LLM-based reviewer grader) so &amp;lt;code&amp;gt;ResponseMap.reviewer_reputation_for&amp;lt;/code&amp;gt; reflects actual review quality instead of the current &amp;lt;code&amp;gt;1.0&amp;lt;/code&amp;gt; placeholder for every reviewer.&lt;br /&gt;
&lt;br /&gt;
=== Email-the-authors integration ===&lt;br /&gt;
Wire &amp;quot;Send Email To Reviewers&amp;quot; to a real backend endpoint instead of the current placeholder route.&lt;br /&gt;
&lt;br /&gt;
=== Dedicated StudentTask abstraction ===&lt;br /&gt;
&amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; remains a composed, non-persisted object. A future dedicated table could centralize derived state and simplify queries if the feature's scope grows further.&lt;br /&gt;
&lt;br /&gt;
== Mentor ==&lt;br /&gt;
[mailto:kgudipe@ncsu.edu Koushik Gudipelly]&lt;br /&gt;
&lt;br /&gt;
== Members ==&lt;br /&gt;
[mailto:rsgopara@ncsu.edu Ravi Goparaju]&lt;br /&gt;
[mailto:xmi@ncsu.edu Xiangjun Mi]&lt;br /&gt;
&lt;br /&gt;
== Completed by ==&lt;br /&gt;
[mailto:blalu@ncsu.edu Bestin Lalu]&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2602._Reimplement_student_task_view&amp;diff=168228</id>
		<title>CSC/ECE 517 Spring 2026 - E2602. Reimplement student task view</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2602._Reimplement_student_task_view&amp;diff=168228"/>
		<updated>2026-07-02T23:22:28Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* As a student, I want my team's score heatgrid to load fast and stay readable while scrolling */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= CSC/ECE 517 Spring 2026 - E2602. Reimplement Student Task View =&lt;br /&gt;
&lt;br /&gt;
== About Expertiza ==&lt;br /&gt;
Expertiza is a robust, open-source web platform designed to empower educational environments that emphasize project-based learning. It streamlines collaborative activities including peer reviews, surveys, and team assignments — giving instructors tools to manage assignments and track performance, and giving students a single place to submit work, review peers, and track feedback.&lt;br /&gt;
&lt;br /&gt;
== Introduction ==&lt;br /&gt;
This iteration of E2602 hardens and extends the Student Task View: consolidating the student dashboard and detail pages into single-call REST endpoints, eliminating N+1 queries in the peer-review score heatgrid, and fixing several display/permission bugs surfaced by manual QA against real assignment data.&lt;br /&gt;
&lt;br /&gt;
=== Tech Stack ===&lt;br /&gt;
* '''Backend:''' Ruby on Rails (API-only)&lt;br /&gt;
* '''Frontend:''' React + TypeScript&lt;br /&gt;
* '''Database:''' MySQL&lt;br /&gt;
* '''Backend Testing:''' RSpec&lt;br /&gt;
* '''Frontend Testing:''' Jest, React Testing Library&lt;br /&gt;
&lt;br /&gt;
=== Key Improvements ===&lt;br /&gt;
* Eliminated N+1 queries in the score heatgrid (&amp;lt;code&amp;gt;get_team_scores&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_my_scores_data&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_answer&amp;lt;/code&amp;gt;) via &amp;lt;code&amp;gt;.includes(responses: { scores: :item })&amp;lt;/code&amp;gt; and in-memory filtering instead of re-querying preloaded associations&lt;br /&gt;
* Converted &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt;'s stylesheet to CSS Modules, fixing a Bootstrap class collision and scoping color classes (&amp;lt;code&amp;gt;c1&amp;lt;/code&amp;gt;–&amp;lt;code&amp;gt;c5&amp;lt;/code&amp;gt;) correctly&lt;br /&gt;
* Redesigned the score/feedback heatgrid with sticky &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;/Question columns and a sticky, full-width SectionHeader row that stays fixed during horizontal scroll&lt;br /&gt;
* Added hover tooltips on score cells showing the reviewer's written feedback&lt;br /&gt;
* Rewrote &amp;lt;code&amp;gt;StudentTasks&amp;lt;/code&amp;gt; from a (mis-implemented) topic sign-up sheet into a proper course-grouped assignment dashboard&lt;br /&gt;
* Added participant-level &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; permission checks, hiding &amp;quot;Your work&amp;quot; / &amp;quot;Others' work&amp;quot; task links accordingly&lt;br /&gt;
* Refactored review grade calculation: &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; now weights each round by its &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;; &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; centralizes the weighted-average logic previously duplicated across &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Problem Statement ==&lt;br /&gt;
The previous Student Task View had several concrete defects:&lt;br /&gt;
* The score heatgrid fired one SQL query per response/score across all reviewers and rounds, producing hundreds of queries for a single page load&lt;br /&gt;
* SectionHeader rubric rows (non-scorable dividers) were incorrectly counted in the &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; column numbering and lost their fixed position during horizontal scroll&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTasks.tsx&amp;lt;/code&amp;gt; was previously implemented as a topic sign-up sheet, not a task dashboard, and required a full rewrite&lt;br /&gt;
* The page did not respect participant-level &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; flags — all task links were always shown regardless of permission&lt;br /&gt;
* Review grade calculation treated every reviewer and every round equally, ignoring each rubric's configured &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== User Stories ==&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want to see all my assignments in one place ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; returns all tasks for the logged-in student in one call&lt;br /&gt;
* Tasks are grouped by course on the dashboard&lt;br /&gt;
* Each task shows current stage and days remaining, if applicable&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want to view a single assignment's deadlines and details ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt; returns the task plus a unified &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt; timeline&lt;br /&gt;
* The timeline visually distinguishes completed, current, and pending stages&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/team&amp;lt;/code&amp;gt; returns teammates grouped by course&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want my team's score heatgrid to load fast and stay readable while scrolling ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* The heatgrid loads from a single &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt; call&lt;br /&gt;
* The first two columns (item number &amp;quot;&amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;&amp;quot; and Item) remain fixed during horizontal scroll, along with section headers&lt;br /&gt;
* Hovering over a score with a comment shows the corresponding reviewer's feedback as a tooltip&lt;br /&gt;
&lt;br /&gt;
=== As a student who isn't permitted to submit or review, I should not see those task links ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;quot;Your work&amp;quot; is hidden when &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;quot;Others' work&amp;quot; is hidden when &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== As an instructor, I want review grades to reflect each rubric's configured weight ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; weights each round's normalized score by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt; both compute their averages through the same shared &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
=== Student Task Overview ===&lt;br /&gt;
No dedicated &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; database table exists. &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; is a plain Ruby object composed from &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; and its associated &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignedUpTeam&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;ProjectTopic&amp;lt;/code&amp;gt;, and permission fields (&amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;permission_granted&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Request / Response Flow ====&lt;br /&gt;
* Student navigates to &amp;lt;code&amp;gt;/student_tasks&amp;lt;/code&amp;gt; → frontend calls &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTask.tasks(user)&amp;lt;/code&amp;gt; preloads &amp;lt;code&amp;gt;assignment: :course&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;:user&amp;lt;/code&amp;gt; associations, then maps each &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; through &amp;lt;code&amp;gt;StudentTask.create_from_participant&amp;lt;/code&amp;gt;&lt;br /&gt;
* Selecting an assignment navigates to the detail route → frontend calls &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTask.get_events_for_assignment&amp;lt;/code&amp;gt; merges due dates with submitted review/feedback activity into one sorted timeline&lt;br /&gt;
* For the score heatgrid: frontend calls &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt; → &amp;lt;code&amp;gt;GradesController#get_team_scores&amp;lt;/code&amp;gt; eager-loads &amp;lt;code&amp;gt;responses: { scores: :item }&amp;lt;/code&amp;gt; and calls &amp;lt;code&amp;gt;insert_section_headers&amp;lt;/code&amp;gt; to splice header sentinels into the response&lt;br /&gt;
&lt;br /&gt;
=== Score Aggregation Flow ===&lt;br /&gt;
* &amp;lt;code&amp;gt;Response#aggregate_questionnaire_score&amp;lt;/code&amp;gt; — item-weighted score for one submitted response&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; — normalizes a map's score per round and weights it by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score(maps)&amp;lt;/code&amp;gt; — weighted average across multiple maps, the single implementation shared by &amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Backend Implementation ==&lt;br /&gt;
&lt;br /&gt;
=== StudentTasksController ===&lt;br /&gt;
* &amp;lt;code&amp;gt;list&amp;lt;/code&amp;gt; — all &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; objects for the logged-in user&lt;br /&gt;
* &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt; — single task by participant ID with the unified timeline&lt;br /&gt;
* &amp;lt;code&amp;gt;team&amp;lt;/code&amp;gt; — teammates grouped by course name (&amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
=== GradesController — N+1 elimination ===&lt;br /&gt;
&amp;lt;code&amp;gt;get_team_scores&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;get_my_scores_data&amp;lt;/code&amp;gt; eager-load &amp;lt;code&amp;gt;responses: { scores: :item }&amp;lt;/code&amp;gt; on the &amp;lt;code&amp;gt;ReviewResponseMap&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;TeammateReviewResponseMap&amp;lt;/code&amp;gt; queries. &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt; filters SectionHeader scores via Ruby &amp;lt;code&amp;gt;.reject&amp;lt;/code&amp;gt; instead of &amp;lt;code&amp;gt;.joins&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;.where&amp;lt;/code&amp;gt; — calling &amp;lt;code&amp;gt;.where&amp;lt;/code&amp;gt; on an already-preloaded association silently bypasses the cache and re-queries the database. &amp;lt;code&amp;gt;get_answer&amp;lt;/code&amp;gt; now accepts the parent &amp;lt;code&amp;gt;response&amp;lt;/code&amp;gt; directly instead of re-fetching it via &amp;lt;code&amp;gt;score.response&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== insert_section_headers ===&lt;br /&gt;
Splices &amp;lt;code&amp;gt;{ type: &amp;quot;header&amp;quot;, txt: &amp;quot;...&amp;quot; }&amp;lt;/code&amp;gt; sentinel hashes into a round's scores array at positions matching each &amp;lt;code&amp;gt;SectionHeader&amp;lt;/code&amp;gt; item in the questionnaire's sequence, so the frontend can render heading rows between score rows without those rows affecting numbering.&lt;br /&gt;
&lt;br /&gt;
=== Review Grade Calculation ===&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; — latest submitted response per round, normalized (&amp;lt;code&amp;gt;score/max&amp;lt;/code&amp;gt;), weighted by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;ResponseMap.reviewer_reputation_for&amp;lt;/code&amp;gt; — shared weighted-average logic; reviewer reputation currently defaults to &amp;lt;code&amp;gt;1.0&amp;lt;/code&amp;gt; (placeholder for a future review-quality grader integration)&lt;br /&gt;
* &amp;lt;code&amp;gt;Questionnaire#total_item_weight&amp;lt;/code&amp;gt; — pre-calculates the sum of scored item weights, excluding &amp;lt;code&amp;gt;SectionHeader&amp;lt;/code&amp;gt; items&lt;br /&gt;
&lt;br /&gt;
=== Comprehensive Testing ===&lt;br /&gt;
* Model specs for &amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;create_from_participant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_events_for_assignment&amp;lt;/code&amp;gt;&lt;br /&gt;
* Request specs for &amp;lt;code&amp;gt;student_tasks/list&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;student_tasks/show/:id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;student_tasks/team&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;grades/:id/view_our_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
* Routing specs verifying the above routes resolve correctly&lt;br /&gt;
&lt;br /&gt;
== Frontend Implementation ==&lt;br /&gt;
[[File:student_view_assignments_page.png|600px|thumb|right|Student View Assignments Page]]&lt;br /&gt;
[[File:student_view_assignment_detail_page.png|600px|thumb|right|Student View Assignment Detail Page]]&lt;br /&gt;
[[File:student_view_assignment_grades_page.png|600px|thumb|right|Student View Assignment View Team Grades]]&lt;br /&gt;
[[File:student_view_assignment_feedback_page.png|600px|thumb|right|Student View Assignment View Feedbacks]]&lt;br /&gt;
=== New/Updated Files ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! File !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTasks.tsx&amp;lt;/code&amp;gt; || Course-grouped assignment dashboard (rewritten from a topic sign-up sheet)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTasksList.tsx&amp;lt;/code&amp;gt; || Sidebar — not-started tasks, revisions, teammates by course&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTaskDetail.tsx&amp;lt;/code&amp;gt; || Per-assignment detail page — timeline, task links, conditional &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; rendering&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ViewTeamGrades.module.scss&amp;lt;/code&amp;gt; || CSS Modules conversion; scoped &amp;lt;code&amp;gt;c1&amp;lt;/code&amp;gt;–&amp;lt;code&amp;gt;c5&amp;lt;/code&amp;gt; color classes, sticky-column and tooltip styles&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewTable.tsx&amp;lt;/code&amp;gt; || Single-call heatgrid; Scores/Feedback toggle; sticky SectionHeader rows&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewTableRow.tsx&amp;lt;/code&amp;gt; || Sticky &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;/Question columns; reduced row height; hover tooltip via &amp;lt;code&amp;gt;data-question&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;FeedbackTable.tsx&amp;lt;/code&amp;gt; || Feedback-mode table mirroring &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt;'s sticky-column layout&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;heatgridUtils.ts&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;RoundRow&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;isHeader()&amp;lt;/code&amp;gt; types; fixed &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; numbering to skip SectionHeader sentinels&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== StudentTasks (List View) ===&lt;br /&gt;
* Single &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; call, grouped by course&lt;br /&gt;
* Fixed: &amp;quot;Assignments&amp;quot; heading was rendering as body text because its class was referenced as a plain string instead of the CSS Modules export (&amp;lt;code&amp;gt;styles['assignments-title']&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
=== StudentTaskDetail (Detail View) ===&lt;br /&gt;
* Visual timeline with progress line and stage nodes (completed/current/pending)&lt;br /&gt;
* Task links list — &amp;quot;Your work&amp;quot; and &amp;quot;Others' work&amp;quot; conditionally rendered based on &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;quot;Send Email To Reviewers&amp;quot; button repositioned to align with the task list rather than floating disconnected at the page top&lt;br /&gt;
&lt;br /&gt;
=== ViewTeamGrades (Heatgrid) ===&lt;br /&gt;
* Consolidated ~6 sequential API calls into one &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
* SectionHeader rows split into a sticky label cell + scrolling spacer cell so they stay fixed during horizontal scroll&lt;br /&gt;
* Score cells show reviewer feedback as a hover tooltip&lt;br /&gt;
* Round headings simplified from &amp;quot;Review (Round: 1 of 2)&amp;quot; to &amp;quot;Round 1&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Design Principles ==&lt;br /&gt;
&lt;br /&gt;
=== Single Responsibility ===&lt;br /&gt;
Score aggregation is layered: &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; owns item-level scoring, &amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt; owns per-map/per-round weighting, &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; owns cross-map averaging — each level only knows about the one below it.&lt;br /&gt;
&lt;br /&gt;
=== Don't Repeat Yourself ===&lt;br /&gt;
The weighted-average logic that was duplicated between &amp;lt;code&amp;gt;AssignmentTeam#aggregate_review_grade&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;ReviewAggregator#compute_average_review_score&amp;lt;/code&amp;gt; was consolidated into a single &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt;, and the now-dead &amp;lt;code&amp;gt;ReviewAggregator&amp;lt;/code&amp;gt; concern was deleted.&lt;br /&gt;
&lt;br /&gt;
=== Performance by Design ===&lt;br /&gt;
Eager loading (&amp;lt;code&amp;gt;.includes&amp;lt;/code&amp;gt;) and in-memory filtering (&amp;lt;code&amp;gt;.reject&amp;lt;/code&amp;gt;) were chosen deliberately over additional database round-trips after profiling showed the heatgrid endpoint firing hundreds of queries under the old implementation.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
&lt;br /&gt;
=== Backend Tests ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! # !! Test !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| 1 || &amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt; groups teammates by course || Returns &amp;lt;code&amp;gt;{course_name =&amp;gt; [fullnames]}&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 2 || &amp;lt;code&amp;gt;all_teammates&amp;lt;/code&amp;gt; excludes calibrated assignments || Calibrated-assignment teammates excluded&lt;br /&gt;
|-&lt;br /&gt;
| 3 || &amp;lt;code&amp;gt;all_teammates&amp;lt;/code&amp;gt; excludes the user themself || Solo team returns empty hash&lt;br /&gt;
|-&lt;br /&gt;
| 4 || &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; returns all tasks for user || 200 OK with task array&lt;br /&gt;
|-&lt;br /&gt;
| 5 || &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt; returns due_dates timeline || 200 OK with &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt; array&lt;br /&gt;
|-&lt;br /&gt;
| 6 || &amp;lt;code&amp;gt;GET /student_tasks/team&amp;lt;/code&amp;gt; groups teammates by course || 200 OK with grouped hash&lt;br /&gt;
|-&lt;br /&gt;
| 7 || Heatgrid &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; column excludes SectionHeaders from numbering || Sequential numbering skips header rows&lt;br /&gt;
|-&lt;br /&gt;
| 8 || &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt; filters SectionHeaders without re-querying || No additional SQL fired against preloaded association&lt;br /&gt;
|-&lt;br /&gt;
| 9 || &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; weight validation || &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt; must be 0 when rubric has no scored questions&lt;br /&gt;
|-&lt;br /&gt;
| 10 || &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; || Weighted average matches Σ(grade × weight) / Σ(weight)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Frontend Tests ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! # !! Test !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| 1 || &amp;lt;code&amp;gt;StudentTasks&amp;lt;/code&amp;gt; renders &amp;quot;Assignments&amp;quot; heading with correct styling || Heading uses module class, renders bold and large&lt;br /&gt;
|-&lt;br /&gt;
| 2 || &amp;lt;code&amp;gt;StudentTaskDetail&amp;lt;/code&amp;gt; hides &amp;quot;Your work&amp;quot; when &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; is false || List item not rendered&lt;br /&gt;
|-&lt;br /&gt;
| 3 || &amp;lt;code&amp;gt;StudentTaskDetail&amp;lt;/code&amp;gt; hides &amp;quot;Others' work&amp;quot; when &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; is false || List item not rendered&lt;br /&gt;
|-&lt;br /&gt;
| 4 || &amp;lt;code&amp;gt;heatgridUtils&amp;lt;/code&amp;gt; itemNumber skips SectionHeader rows || Numbering matches scored items only&lt;br /&gt;
|-&lt;br /&gt;
| 5 || &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt; SectionHeader row stays fixed during horizontal scroll || Header cell &amp;lt;code&amp;gt;position: sticky&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 6 || &amp;lt;code&amp;gt;ReviewTableRow&amp;lt;/code&amp;gt; shows tooltip only when a comment exists || No tooltip box when &amp;lt;code&amp;gt;data-question&amp;lt;/code&amp;gt; is undefined&lt;br /&gt;
|-&lt;br /&gt;
| 7 || Heatgrid column borders render as &amp;lt;code&amp;gt;#ddd&amp;lt;/code&amp;gt; || No black borders after &amp;lt;code&amp;gt;tbl_heat&amp;lt;/code&amp;gt; class applied&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Relevant Links ==&lt;br /&gt;
&lt;br /&gt;
=== Repos ===&lt;br /&gt;
* https://github.com/expertiza/reimplementation-back-end&lt;br /&gt;
* https://github.com/expertiza/reimplementation-front-end&lt;br /&gt;
&lt;br /&gt;
=== Pull Requests ===&lt;br /&gt;
* Backend: https://github.com/expertiza/reimplementation-back-end/pull/352&lt;br /&gt;
* Frontend: https://github.com/expertiza/reimplementation-front-end/pull/182&lt;br /&gt;
&lt;br /&gt;
=== Demo Video ===&lt;br /&gt;
* (add link once recorded)&lt;br /&gt;
&lt;br /&gt;
== Developer Guidance ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Setup &amp;amp; Run (via Docker) ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
docker compose up&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Within the container ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
bundle exec rspec spec/models/student_task_spec.rb&lt;br /&gt;
bundle exec rspec spec/requests/api/v1/student_tasks_controller_spec.rb&lt;br /&gt;
bundle exec rspec spec/requests/api/v1/grades_controller_spec.rb&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
==== Setup &amp;amp; Run ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm install&lt;br /&gt;
npm start&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Running Frontend Tests ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm test -- StudentTaskDetail   # run a single suite&lt;br /&gt;
npm test                        # run full suite&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Future Scope ==&lt;br /&gt;
&lt;br /&gt;
=== Reviewer reputation-weighted grading ===&lt;br /&gt;
Integrate an external review-quality grader (e.g. an LLM-based reviewer grader) so &amp;lt;code&amp;gt;ResponseMap.reviewer_reputation_for&amp;lt;/code&amp;gt; reflects actual review quality instead of the current &amp;lt;code&amp;gt;1.0&amp;lt;/code&amp;gt; placeholder for every reviewer.&lt;br /&gt;
&lt;br /&gt;
=== Email-the-authors integration ===&lt;br /&gt;
Wire &amp;quot;Send Email To Reviewers&amp;quot; to a real backend endpoint instead of the current placeholder route.&lt;br /&gt;
&lt;br /&gt;
=== Dedicated StudentTask abstraction ===&lt;br /&gt;
&amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; remains a composed, non-persisted object. A future dedicated table could centralize derived state and simplify queries if the feature's scope grows further.&lt;br /&gt;
&lt;br /&gt;
== Mentor ==&lt;br /&gt;
[mailto:kgudipe@ncsu.edu Koushik Gudipelly]&lt;br /&gt;
&lt;br /&gt;
== Members ==&lt;br /&gt;
[mailto:rsgopara@ncsu.edu Ravi Goparaju]&lt;br /&gt;
[mailto:xmi@ncsu.edu Xiangjun Mi]&lt;br /&gt;
&lt;br /&gt;
== Completed by ==&lt;br /&gt;
[mailto:blalu@ncsu.edu Bestin Lalu]&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2602._Reimplement_student_task_view&amp;diff=168227</id>
		<title>CSC/ECE 517 Spring 2026 - E2602. Reimplement student task view</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2602._Reimplement_student_task_view&amp;diff=168227"/>
		<updated>2026-07-02T23:22:10Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* As a student, I want my team's score heatgrid to load fast and stay readable while scrolling */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= CSC/ECE 517 Spring 2026 - E2602. Reimplement Student Task View =&lt;br /&gt;
&lt;br /&gt;
== About Expertiza ==&lt;br /&gt;
Expertiza is a robust, open-source web platform designed to empower educational environments that emphasize project-based learning. It streamlines collaborative activities including peer reviews, surveys, and team assignments — giving instructors tools to manage assignments and track performance, and giving students a single place to submit work, review peers, and track feedback.&lt;br /&gt;
&lt;br /&gt;
== Introduction ==&lt;br /&gt;
This iteration of E2602 hardens and extends the Student Task View: consolidating the student dashboard and detail pages into single-call REST endpoints, eliminating N+1 queries in the peer-review score heatgrid, and fixing several display/permission bugs surfaced by manual QA against real assignment data.&lt;br /&gt;
&lt;br /&gt;
=== Tech Stack ===&lt;br /&gt;
* '''Backend:''' Ruby on Rails (API-only)&lt;br /&gt;
* '''Frontend:''' React + TypeScript&lt;br /&gt;
* '''Database:''' MySQL&lt;br /&gt;
* '''Backend Testing:''' RSpec&lt;br /&gt;
* '''Frontend Testing:''' Jest, React Testing Library&lt;br /&gt;
&lt;br /&gt;
=== Key Improvements ===&lt;br /&gt;
* Eliminated N+1 queries in the score heatgrid (&amp;lt;code&amp;gt;get_team_scores&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_my_scores_data&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_answer&amp;lt;/code&amp;gt;) via &amp;lt;code&amp;gt;.includes(responses: { scores: :item })&amp;lt;/code&amp;gt; and in-memory filtering instead of re-querying preloaded associations&lt;br /&gt;
* Converted &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt;'s stylesheet to CSS Modules, fixing a Bootstrap class collision and scoping color classes (&amp;lt;code&amp;gt;c1&amp;lt;/code&amp;gt;–&amp;lt;code&amp;gt;c5&amp;lt;/code&amp;gt;) correctly&lt;br /&gt;
* Redesigned the score/feedback heatgrid with sticky &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;/Question columns and a sticky, full-width SectionHeader row that stays fixed during horizontal scroll&lt;br /&gt;
* Added hover tooltips on score cells showing the reviewer's written feedback&lt;br /&gt;
* Rewrote &amp;lt;code&amp;gt;StudentTasks&amp;lt;/code&amp;gt; from a (mis-implemented) topic sign-up sheet into a proper course-grouped assignment dashboard&lt;br /&gt;
* Added participant-level &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; permission checks, hiding &amp;quot;Your work&amp;quot; / &amp;quot;Others' work&amp;quot; task links accordingly&lt;br /&gt;
* Refactored review grade calculation: &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; now weights each round by its &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;; &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; centralizes the weighted-average logic previously duplicated across &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Problem Statement ==&lt;br /&gt;
The previous Student Task View had several concrete defects:&lt;br /&gt;
* The score heatgrid fired one SQL query per response/score across all reviewers and rounds, producing hundreds of queries for a single page load&lt;br /&gt;
* SectionHeader rubric rows (non-scorable dividers) were incorrectly counted in the &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; column numbering and lost their fixed position during horizontal scroll&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTasks.tsx&amp;lt;/code&amp;gt; was previously implemented as a topic sign-up sheet, not a task dashboard, and required a full rewrite&lt;br /&gt;
* The page did not respect participant-level &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; flags — all task links were always shown regardless of permission&lt;br /&gt;
* Review grade calculation treated every reviewer and every round equally, ignoring each rubric's configured &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== User Stories ==&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want to see all my assignments in one place ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; returns all tasks for the logged-in student in one call&lt;br /&gt;
* Tasks are grouped by course on the dashboard&lt;br /&gt;
* Each task shows current stage and days remaining, if applicable&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want to view a single assignment's deadlines and details ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt; returns the task plus a unified &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt; timeline&lt;br /&gt;
* The timeline visually distinguishes completed, current, and pending stages&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/team&amp;lt;/code&amp;gt; returns teammates grouped by course&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want my team's score heatgrid to load fast and stay readable while scrolling ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* The heatgrid loads from a single &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt; call&lt;br /&gt;
* The first two columns (item number &amp;quot;&amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;&amp;quot; and Item remain fixed during horizontal scroll, along with section headers&lt;br /&gt;
* Hovering over a score with a comment shows the corresponding reviewer's feedback as a tooltip&lt;br /&gt;
&lt;br /&gt;
=== As a student who isn't permitted to submit or review, I should not see those task links ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;quot;Your work&amp;quot; is hidden when &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;quot;Others' work&amp;quot; is hidden when &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== As an instructor, I want review grades to reflect each rubric's configured weight ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; weights each round's normalized score by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt; both compute their averages through the same shared &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
=== Student Task Overview ===&lt;br /&gt;
No dedicated &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; database table exists. &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; is a plain Ruby object composed from &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; and its associated &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignedUpTeam&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;ProjectTopic&amp;lt;/code&amp;gt;, and permission fields (&amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;permission_granted&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Request / Response Flow ====&lt;br /&gt;
* Student navigates to &amp;lt;code&amp;gt;/student_tasks&amp;lt;/code&amp;gt; → frontend calls &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTask.tasks(user)&amp;lt;/code&amp;gt; preloads &amp;lt;code&amp;gt;assignment: :course&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;:user&amp;lt;/code&amp;gt; associations, then maps each &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; through &amp;lt;code&amp;gt;StudentTask.create_from_participant&amp;lt;/code&amp;gt;&lt;br /&gt;
* Selecting an assignment navigates to the detail route → frontend calls &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTask.get_events_for_assignment&amp;lt;/code&amp;gt; merges due dates with submitted review/feedback activity into one sorted timeline&lt;br /&gt;
* For the score heatgrid: frontend calls &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt; → &amp;lt;code&amp;gt;GradesController#get_team_scores&amp;lt;/code&amp;gt; eager-loads &amp;lt;code&amp;gt;responses: { scores: :item }&amp;lt;/code&amp;gt; and calls &amp;lt;code&amp;gt;insert_section_headers&amp;lt;/code&amp;gt; to splice header sentinels into the response&lt;br /&gt;
&lt;br /&gt;
=== Score Aggregation Flow ===&lt;br /&gt;
* &amp;lt;code&amp;gt;Response#aggregate_questionnaire_score&amp;lt;/code&amp;gt; — item-weighted score for one submitted response&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; — normalizes a map's score per round and weights it by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score(maps)&amp;lt;/code&amp;gt; — weighted average across multiple maps, the single implementation shared by &amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Backend Implementation ==&lt;br /&gt;
&lt;br /&gt;
=== StudentTasksController ===&lt;br /&gt;
* &amp;lt;code&amp;gt;list&amp;lt;/code&amp;gt; — all &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; objects for the logged-in user&lt;br /&gt;
* &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt; — single task by participant ID with the unified timeline&lt;br /&gt;
* &amp;lt;code&amp;gt;team&amp;lt;/code&amp;gt; — teammates grouped by course name (&amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
=== GradesController — N+1 elimination ===&lt;br /&gt;
&amp;lt;code&amp;gt;get_team_scores&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;get_my_scores_data&amp;lt;/code&amp;gt; eager-load &amp;lt;code&amp;gt;responses: { scores: :item }&amp;lt;/code&amp;gt; on the &amp;lt;code&amp;gt;ReviewResponseMap&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;TeammateReviewResponseMap&amp;lt;/code&amp;gt; queries. &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt; filters SectionHeader scores via Ruby &amp;lt;code&amp;gt;.reject&amp;lt;/code&amp;gt; instead of &amp;lt;code&amp;gt;.joins&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;.where&amp;lt;/code&amp;gt; — calling &amp;lt;code&amp;gt;.where&amp;lt;/code&amp;gt; on an already-preloaded association silently bypasses the cache and re-queries the database. &amp;lt;code&amp;gt;get_answer&amp;lt;/code&amp;gt; now accepts the parent &amp;lt;code&amp;gt;response&amp;lt;/code&amp;gt; directly instead of re-fetching it via &amp;lt;code&amp;gt;score.response&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== insert_section_headers ===&lt;br /&gt;
Splices &amp;lt;code&amp;gt;{ type: &amp;quot;header&amp;quot;, txt: &amp;quot;...&amp;quot; }&amp;lt;/code&amp;gt; sentinel hashes into a round's scores array at positions matching each &amp;lt;code&amp;gt;SectionHeader&amp;lt;/code&amp;gt; item in the questionnaire's sequence, so the frontend can render heading rows between score rows without those rows affecting numbering.&lt;br /&gt;
&lt;br /&gt;
=== Review Grade Calculation ===&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; — latest submitted response per round, normalized (&amp;lt;code&amp;gt;score/max&amp;lt;/code&amp;gt;), weighted by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;ResponseMap.reviewer_reputation_for&amp;lt;/code&amp;gt; — shared weighted-average logic; reviewer reputation currently defaults to &amp;lt;code&amp;gt;1.0&amp;lt;/code&amp;gt; (placeholder for a future review-quality grader integration)&lt;br /&gt;
* &amp;lt;code&amp;gt;Questionnaire#total_item_weight&amp;lt;/code&amp;gt; — pre-calculates the sum of scored item weights, excluding &amp;lt;code&amp;gt;SectionHeader&amp;lt;/code&amp;gt; items&lt;br /&gt;
&lt;br /&gt;
=== Comprehensive Testing ===&lt;br /&gt;
* Model specs for &amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;create_from_participant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_events_for_assignment&amp;lt;/code&amp;gt;&lt;br /&gt;
* Request specs for &amp;lt;code&amp;gt;student_tasks/list&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;student_tasks/show/:id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;student_tasks/team&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;grades/:id/view_our_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
* Routing specs verifying the above routes resolve correctly&lt;br /&gt;
&lt;br /&gt;
== Frontend Implementation ==&lt;br /&gt;
[[File:student_view_assignments_page.png|600px|thumb|right|Student View Assignments Page]]&lt;br /&gt;
[[File:student_view_assignment_detail_page.png|600px|thumb|right|Student View Assignment Detail Page]]&lt;br /&gt;
[[File:student_view_assignment_grades_page.png|600px|thumb|right|Student View Assignment View Team Grades]]&lt;br /&gt;
[[File:student_view_assignment_feedback_page.png|600px|thumb|right|Student View Assignment View Feedbacks]]&lt;br /&gt;
=== New/Updated Files ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! File !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTasks.tsx&amp;lt;/code&amp;gt; || Course-grouped assignment dashboard (rewritten from a topic sign-up sheet)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTasksList.tsx&amp;lt;/code&amp;gt; || Sidebar — not-started tasks, revisions, teammates by course&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTaskDetail.tsx&amp;lt;/code&amp;gt; || Per-assignment detail page — timeline, task links, conditional &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; rendering&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ViewTeamGrades.module.scss&amp;lt;/code&amp;gt; || CSS Modules conversion; scoped &amp;lt;code&amp;gt;c1&amp;lt;/code&amp;gt;–&amp;lt;code&amp;gt;c5&amp;lt;/code&amp;gt; color classes, sticky-column and tooltip styles&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewTable.tsx&amp;lt;/code&amp;gt; || Single-call heatgrid; Scores/Feedback toggle; sticky SectionHeader rows&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewTableRow.tsx&amp;lt;/code&amp;gt; || Sticky &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;/Question columns; reduced row height; hover tooltip via &amp;lt;code&amp;gt;data-question&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;FeedbackTable.tsx&amp;lt;/code&amp;gt; || Feedback-mode table mirroring &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt;'s sticky-column layout&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;heatgridUtils.ts&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;RoundRow&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;isHeader()&amp;lt;/code&amp;gt; types; fixed &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; numbering to skip SectionHeader sentinels&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== StudentTasks (List View) ===&lt;br /&gt;
* Single &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; call, grouped by course&lt;br /&gt;
* Fixed: &amp;quot;Assignments&amp;quot; heading was rendering as body text because its class was referenced as a plain string instead of the CSS Modules export (&amp;lt;code&amp;gt;styles['assignments-title']&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
=== StudentTaskDetail (Detail View) ===&lt;br /&gt;
* Visual timeline with progress line and stage nodes (completed/current/pending)&lt;br /&gt;
* Task links list — &amp;quot;Your work&amp;quot; and &amp;quot;Others' work&amp;quot; conditionally rendered based on &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;quot;Send Email To Reviewers&amp;quot; button repositioned to align with the task list rather than floating disconnected at the page top&lt;br /&gt;
&lt;br /&gt;
=== ViewTeamGrades (Heatgrid) ===&lt;br /&gt;
* Consolidated ~6 sequential API calls into one &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
* SectionHeader rows split into a sticky label cell + scrolling spacer cell so they stay fixed during horizontal scroll&lt;br /&gt;
* Score cells show reviewer feedback as a hover tooltip&lt;br /&gt;
* Round headings simplified from &amp;quot;Review (Round: 1 of 2)&amp;quot; to &amp;quot;Round 1&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Design Principles ==&lt;br /&gt;
&lt;br /&gt;
=== Single Responsibility ===&lt;br /&gt;
Score aggregation is layered: &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; owns item-level scoring, &amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt; owns per-map/per-round weighting, &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; owns cross-map averaging — each level only knows about the one below it.&lt;br /&gt;
&lt;br /&gt;
=== Don't Repeat Yourself ===&lt;br /&gt;
The weighted-average logic that was duplicated between &amp;lt;code&amp;gt;AssignmentTeam#aggregate_review_grade&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;ReviewAggregator#compute_average_review_score&amp;lt;/code&amp;gt; was consolidated into a single &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt;, and the now-dead &amp;lt;code&amp;gt;ReviewAggregator&amp;lt;/code&amp;gt; concern was deleted.&lt;br /&gt;
&lt;br /&gt;
=== Performance by Design ===&lt;br /&gt;
Eager loading (&amp;lt;code&amp;gt;.includes&amp;lt;/code&amp;gt;) and in-memory filtering (&amp;lt;code&amp;gt;.reject&amp;lt;/code&amp;gt;) were chosen deliberately over additional database round-trips after profiling showed the heatgrid endpoint firing hundreds of queries under the old implementation.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
&lt;br /&gt;
=== Backend Tests ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! # !! Test !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| 1 || &amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt; groups teammates by course || Returns &amp;lt;code&amp;gt;{course_name =&amp;gt; [fullnames]}&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 2 || &amp;lt;code&amp;gt;all_teammates&amp;lt;/code&amp;gt; excludes calibrated assignments || Calibrated-assignment teammates excluded&lt;br /&gt;
|-&lt;br /&gt;
| 3 || &amp;lt;code&amp;gt;all_teammates&amp;lt;/code&amp;gt; excludes the user themself || Solo team returns empty hash&lt;br /&gt;
|-&lt;br /&gt;
| 4 || &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; returns all tasks for user || 200 OK with task array&lt;br /&gt;
|-&lt;br /&gt;
| 5 || &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt; returns due_dates timeline || 200 OK with &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt; array&lt;br /&gt;
|-&lt;br /&gt;
| 6 || &amp;lt;code&amp;gt;GET /student_tasks/team&amp;lt;/code&amp;gt; groups teammates by course || 200 OK with grouped hash&lt;br /&gt;
|-&lt;br /&gt;
| 7 || Heatgrid &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; column excludes SectionHeaders from numbering || Sequential numbering skips header rows&lt;br /&gt;
|-&lt;br /&gt;
| 8 || &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt; filters SectionHeaders without re-querying || No additional SQL fired against preloaded association&lt;br /&gt;
|-&lt;br /&gt;
| 9 || &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; weight validation || &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt; must be 0 when rubric has no scored questions&lt;br /&gt;
|-&lt;br /&gt;
| 10 || &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; || Weighted average matches Σ(grade × weight) / Σ(weight)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Frontend Tests ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! # !! Test !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| 1 || &amp;lt;code&amp;gt;StudentTasks&amp;lt;/code&amp;gt; renders &amp;quot;Assignments&amp;quot; heading with correct styling || Heading uses module class, renders bold and large&lt;br /&gt;
|-&lt;br /&gt;
| 2 || &amp;lt;code&amp;gt;StudentTaskDetail&amp;lt;/code&amp;gt; hides &amp;quot;Your work&amp;quot; when &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; is false || List item not rendered&lt;br /&gt;
|-&lt;br /&gt;
| 3 || &amp;lt;code&amp;gt;StudentTaskDetail&amp;lt;/code&amp;gt; hides &amp;quot;Others' work&amp;quot; when &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; is false || List item not rendered&lt;br /&gt;
|-&lt;br /&gt;
| 4 || &amp;lt;code&amp;gt;heatgridUtils&amp;lt;/code&amp;gt; itemNumber skips SectionHeader rows || Numbering matches scored items only&lt;br /&gt;
|-&lt;br /&gt;
| 5 || &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt; SectionHeader row stays fixed during horizontal scroll || Header cell &amp;lt;code&amp;gt;position: sticky&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 6 || &amp;lt;code&amp;gt;ReviewTableRow&amp;lt;/code&amp;gt; shows tooltip only when a comment exists || No tooltip box when &amp;lt;code&amp;gt;data-question&amp;lt;/code&amp;gt; is undefined&lt;br /&gt;
|-&lt;br /&gt;
| 7 || Heatgrid column borders render as &amp;lt;code&amp;gt;#ddd&amp;lt;/code&amp;gt; || No black borders after &amp;lt;code&amp;gt;tbl_heat&amp;lt;/code&amp;gt; class applied&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Relevant Links ==&lt;br /&gt;
&lt;br /&gt;
=== Repos ===&lt;br /&gt;
* https://github.com/expertiza/reimplementation-back-end&lt;br /&gt;
* https://github.com/expertiza/reimplementation-front-end&lt;br /&gt;
&lt;br /&gt;
=== Pull Requests ===&lt;br /&gt;
* Backend: https://github.com/expertiza/reimplementation-back-end/pull/352&lt;br /&gt;
* Frontend: https://github.com/expertiza/reimplementation-front-end/pull/182&lt;br /&gt;
&lt;br /&gt;
=== Demo Video ===&lt;br /&gt;
* (add link once recorded)&lt;br /&gt;
&lt;br /&gt;
== Developer Guidance ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Setup &amp;amp; Run (via Docker) ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
docker compose up&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Within the container ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
bundle exec rspec spec/models/student_task_spec.rb&lt;br /&gt;
bundle exec rspec spec/requests/api/v1/student_tasks_controller_spec.rb&lt;br /&gt;
bundle exec rspec spec/requests/api/v1/grades_controller_spec.rb&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
==== Setup &amp;amp; Run ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm install&lt;br /&gt;
npm start&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Running Frontend Tests ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm test -- StudentTaskDetail   # run a single suite&lt;br /&gt;
npm test                        # run full suite&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Future Scope ==&lt;br /&gt;
&lt;br /&gt;
=== Reviewer reputation-weighted grading ===&lt;br /&gt;
Integrate an external review-quality grader (e.g. an LLM-based reviewer grader) so &amp;lt;code&amp;gt;ResponseMap.reviewer_reputation_for&amp;lt;/code&amp;gt; reflects actual review quality instead of the current &amp;lt;code&amp;gt;1.0&amp;lt;/code&amp;gt; placeholder for every reviewer.&lt;br /&gt;
&lt;br /&gt;
=== Email-the-authors integration ===&lt;br /&gt;
Wire &amp;quot;Send Email To Reviewers&amp;quot; to a real backend endpoint instead of the current placeholder route.&lt;br /&gt;
&lt;br /&gt;
=== Dedicated StudentTask abstraction ===&lt;br /&gt;
&amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; remains a composed, non-persisted object. A future dedicated table could centralize derived state and simplify queries if the feature's scope grows further.&lt;br /&gt;
&lt;br /&gt;
== Mentor ==&lt;br /&gt;
[mailto:kgudipe@ncsu.edu Koushik Gudipelly]&lt;br /&gt;
&lt;br /&gt;
== Members ==&lt;br /&gt;
[mailto:rsgopara@ncsu.edu Ravi Goparaju]&lt;br /&gt;
[mailto:xmi@ncsu.edu Xiangjun Mi]&lt;br /&gt;
&lt;br /&gt;
== Completed by ==&lt;br /&gt;
[mailto:blalu@ncsu.edu Bestin Lalu]&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2602._Reimplement_student_task_view&amp;diff=168226</id>
		<title>CSC/ECE 517 Spring 2026 - E2602. Reimplement student task view</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2602._Reimplement_student_task_view&amp;diff=168226"/>
		<updated>2026-07-02T23:10:17Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* User Stories */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= CSC/ECE 517 Spring 2026 - E2602. Reimplement Student Task View =&lt;br /&gt;
&lt;br /&gt;
== About Expertiza ==&lt;br /&gt;
Expertiza is a robust, open-source web platform designed to empower educational environments that emphasize project-based learning. It streamlines collaborative activities including peer reviews, surveys, and team assignments — giving instructors tools to manage assignments and track performance, and giving students a single place to submit work, review peers, and track feedback.&lt;br /&gt;
&lt;br /&gt;
== Introduction ==&lt;br /&gt;
This iteration of E2602 hardens and extends the Student Task View: consolidating the student dashboard and detail pages into single-call REST endpoints, eliminating N+1 queries in the peer-review score heatgrid, and fixing several display/permission bugs surfaced by manual QA against real assignment data.&lt;br /&gt;
&lt;br /&gt;
=== Tech Stack ===&lt;br /&gt;
* '''Backend:''' Ruby on Rails (API-only)&lt;br /&gt;
* '''Frontend:''' React + TypeScript&lt;br /&gt;
* '''Database:''' MySQL&lt;br /&gt;
* '''Backend Testing:''' RSpec&lt;br /&gt;
* '''Frontend Testing:''' Jest, React Testing Library&lt;br /&gt;
&lt;br /&gt;
=== Key Improvements ===&lt;br /&gt;
* Eliminated N+1 queries in the score heatgrid (&amp;lt;code&amp;gt;get_team_scores&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_my_scores_data&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_answer&amp;lt;/code&amp;gt;) via &amp;lt;code&amp;gt;.includes(responses: { scores: :item })&amp;lt;/code&amp;gt; and in-memory filtering instead of re-querying preloaded associations&lt;br /&gt;
* Converted &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt;'s stylesheet to CSS Modules, fixing a Bootstrap class collision and scoping color classes (&amp;lt;code&amp;gt;c1&amp;lt;/code&amp;gt;–&amp;lt;code&amp;gt;c5&amp;lt;/code&amp;gt;) correctly&lt;br /&gt;
* Redesigned the score/feedback heatgrid with sticky &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;/Question columns and a sticky, full-width SectionHeader row that stays fixed during horizontal scroll&lt;br /&gt;
* Added hover tooltips on score cells showing the reviewer's written feedback&lt;br /&gt;
* Rewrote &amp;lt;code&amp;gt;StudentTasks&amp;lt;/code&amp;gt; from a (mis-implemented) topic sign-up sheet into a proper course-grouped assignment dashboard&lt;br /&gt;
* Added participant-level &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; permission checks, hiding &amp;quot;Your work&amp;quot; / &amp;quot;Others' work&amp;quot; task links accordingly&lt;br /&gt;
* Refactored review grade calculation: &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; now weights each round by its &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;; &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; centralizes the weighted-average logic previously duplicated across &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Problem Statement ==&lt;br /&gt;
The previous Student Task View had several concrete defects:&lt;br /&gt;
* The score heatgrid fired one SQL query per response/score across all reviewers and rounds, producing hundreds of queries for a single page load&lt;br /&gt;
* SectionHeader rubric rows (non-scorable dividers) were incorrectly counted in the &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; column numbering and lost their fixed position during horizontal scroll&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTasks.tsx&amp;lt;/code&amp;gt; was previously implemented as a topic sign-up sheet, not a task dashboard, and required a full rewrite&lt;br /&gt;
* The page did not respect participant-level &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; flags — all task links were always shown regardless of permission&lt;br /&gt;
* Review grade calculation treated every reviewer and every round equally, ignoring each rubric's configured &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== User Stories ==&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want to see all my assignments in one place ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; returns all tasks for the logged-in student in one call&lt;br /&gt;
* Tasks are grouped by course on the dashboard&lt;br /&gt;
* Each task shows current stage and days remaining, if applicable&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want to view a single assignment's deadlines and details ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt; returns the task plus a unified &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt; timeline&lt;br /&gt;
* The timeline visually distinguishes completed, current, and pending stages&lt;br /&gt;
* &amp;lt;code&amp;gt;GET /student_tasks/team&amp;lt;/code&amp;gt; returns teammates grouped by course&lt;br /&gt;
&lt;br /&gt;
=== As a student, I want my team's score heatgrid to load fast and stay readable while scrolling ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* The heatgrid loads from a single &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt; call&lt;br /&gt;
* SectionHeaders are excluded from the &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; column numbering&lt;br /&gt;
* SectionHeader rows and the &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;/Question columns stay fixed during horizontal scroll&lt;br /&gt;
* Hovering a score with a comment shows the reviewer's feedback as a tooltip&lt;br /&gt;
&lt;br /&gt;
=== As a student who isn't permitted to submit or review, I should not see those task links ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;quot;Your work&amp;quot; is hidden when &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;quot;Others' work&amp;quot; is hidden when &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; is &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== As an instructor, I want review grades to reflect each rubric's configured weight ===&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; weights each round's normalized score by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt; both compute their averages through the same shared &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
=== Student Task Overview ===&lt;br /&gt;
No dedicated &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; database table exists. &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; is a plain Ruby object composed from &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; and its associated &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Course&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;DueDate&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;SignedUpTeam&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;ProjectTopic&amp;lt;/code&amp;gt;, and permission fields (&amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;permission_granted&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==== Request / Response Flow ====&lt;br /&gt;
* Student navigates to &amp;lt;code&amp;gt;/student_tasks&amp;lt;/code&amp;gt; → frontend calls &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTask.tasks(user)&amp;lt;/code&amp;gt; preloads &amp;lt;code&amp;gt;assignment: :course&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;:user&amp;lt;/code&amp;gt; associations, then maps each &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; through &amp;lt;code&amp;gt;StudentTask.create_from_participant&amp;lt;/code&amp;gt;&lt;br /&gt;
* Selecting an assignment navigates to the detail route → frontend calls &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;StudentTask.get_events_for_assignment&amp;lt;/code&amp;gt; merges due dates with submitted review/feedback activity into one sorted timeline&lt;br /&gt;
* For the score heatgrid: frontend calls &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt; → &amp;lt;code&amp;gt;GradesController#get_team_scores&amp;lt;/code&amp;gt; eager-loads &amp;lt;code&amp;gt;responses: { scores: :item }&amp;lt;/code&amp;gt; and calls &amp;lt;code&amp;gt;insert_section_headers&amp;lt;/code&amp;gt; to splice header sentinels into the response&lt;br /&gt;
&lt;br /&gt;
=== Score Aggregation Flow ===&lt;br /&gt;
* &amp;lt;code&amp;gt;Response#aggregate_questionnaire_score&amp;lt;/code&amp;gt; — item-weighted score for one submitted response&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; — normalizes a map's score per round and weights it by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score(maps)&amp;lt;/code&amp;gt; — weighted average across multiple maps, the single implementation shared by &amp;lt;code&amp;gt;AssignmentTeam#aggregate_reviewer_score&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;AssignmentParticipant#aggregate_teammate_review_grade&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Backend Implementation ==&lt;br /&gt;
&lt;br /&gt;
=== StudentTasksController ===&lt;br /&gt;
* &amp;lt;code&amp;gt;list&amp;lt;/code&amp;gt; — all &amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; objects for the logged-in user&lt;br /&gt;
* &amp;lt;code&amp;gt;show&amp;lt;/code&amp;gt; — single task by participant ID with the unified timeline&lt;br /&gt;
* &amp;lt;code&amp;gt;team&amp;lt;/code&amp;gt; — teammates grouped by course name (&amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
=== GradesController — N+1 elimination ===&lt;br /&gt;
&amp;lt;code&amp;gt;get_team_scores&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;get_my_scores_data&amp;lt;/code&amp;gt; eager-load &amp;lt;code&amp;gt;responses: { scores: :item }&amp;lt;/code&amp;gt; on the &amp;lt;code&amp;gt;ReviewResponseMap&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;TeammateReviewResponseMap&amp;lt;/code&amp;gt; queries. &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt; filters SectionHeader scores via Ruby &amp;lt;code&amp;gt;.reject&amp;lt;/code&amp;gt; instead of &amp;lt;code&amp;gt;.joins&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;.where&amp;lt;/code&amp;gt; — calling &amp;lt;code&amp;gt;.where&amp;lt;/code&amp;gt; on an already-preloaded association silently bypasses the cache and re-queries the database. &amp;lt;code&amp;gt;get_answer&amp;lt;/code&amp;gt; now accepts the parent &amp;lt;code&amp;gt;response&amp;lt;/code&amp;gt; directly instead of re-fetching it via &amp;lt;code&amp;gt;score.response&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== insert_section_headers ===&lt;br /&gt;
Splices &amp;lt;code&amp;gt;{ type: &amp;quot;header&amp;quot;, txt: &amp;quot;...&amp;quot; }&amp;lt;/code&amp;gt; sentinel hashes into a round's scores array at positions matching each &amp;lt;code&amp;gt;SectionHeader&amp;lt;/code&amp;gt; item in the questionnaire's sequence, so the frontend can render heading rows between score rows without those rows affecting numbering.&lt;br /&gt;
&lt;br /&gt;
=== Review Grade Calculation ===&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap#review_grade&amp;lt;/code&amp;gt; — latest submitted response per round, normalized (&amp;lt;code&amp;gt;score/max&amp;lt;/code&amp;gt;), weighted by that round's &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;ResponseMap.reviewer_reputation_for&amp;lt;/code&amp;gt; — shared weighted-average logic; reviewer reputation currently defaults to &amp;lt;code&amp;gt;1.0&amp;lt;/code&amp;gt; (placeholder for a future review-quality grader integration)&lt;br /&gt;
* &amp;lt;code&amp;gt;Questionnaire#total_item_weight&amp;lt;/code&amp;gt; — pre-calculates the sum of scored item weights, excluding &amp;lt;code&amp;gt;SectionHeader&amp;lt;/code&amp;gt; items&lt;br /&gt;
&lt;br /&gt;
=== Comprehensive Testing ===&lt;br /&gt;
* Model specs for &amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;create_from_participant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;get_events_for_assignment&amp;lt;/code&amp;gt;&lt;br /&gt;
* Request specs for &amp;lt;code&amp;gt;student_tasks/list&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;student_tasks/show/:id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;student_tasks/team&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;grades/:id/view_our_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
* Routing specs verifying the above routes resolve correctly&lt;br /&gt;
&lt;br /&gt;
== Frontend Implementation ==&lt;br /&gt;
[[File:student_view_assignments_page.png|600px|thumb|right|Student View Assignments Page]]&lt;br /&gt;
[[File:student_view_assignment_detail_page.png|600px|thumb|right|Student View Assignment Detail Page]]&lt;br /&gt;
[[File:student_view_assignment_grades_page.png|600px|thumb|right|Student View Assignment View Team Grades]]&lt;br /&gt;
[[File:student_view_assignment_feedback_page.png|600px|thumb|right|Student View Assignment View Feedbacks]]&lt;br /&gt;
=== New/Updated Files ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! File !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTasks.tsx&amp;lt;/code&amp;gt; || Course-grouped assignment dashboard (rewritten from a topic sign-up sheet)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTasksList.tsx&amp;lt;/code&amp;gt; || Sidebar — not-started tasks, revisions, teammates by course&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;StudentTaskDetail.tsx&amp;lt;/code&amp;gt; || Per-assignment detail page — timeline, task links, conditional &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; rendering&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ViewTeamGrades.module.scss&amp;lt;/code&amp;gt; || CSS Modules conversion; scoped &amp;lt;code&amp;gt;c1&amp;lt;/code&amp;gt;–&amp;lt;code&amp;gt;c5&amp;lt;/code&amp;gt; color classes, sticky-column and tooltip styles&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewTable.tsx&amp;lt;/code&amp;gt; || Single-call heatgrid; Scores/Feedback toggle; sticky SectionHeader rows&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewTableRow.tsx&amp;lt;/code&amp;gt; || Sticky &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt;/Question columns; reduced row height; hover tooltip via &amp;lt;code&amp;gt;data-question&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;FeedbackTable.tsx&amp;lt;/code&amp;gt; || Feedback-mode table mirroring &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt;'s sticky-column layout&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;heatgridUtils.ts&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;RoundRow&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;isHeader()&amp;lt;/code&amp;gt; types; fixed &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; numbering to skip SectionHeader sentinels&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== StudentTasks (List View) ===&lt;br /&gt;
* Single &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; call, grouped by course&lt;br /&gt;
* Fixed: &amp;quot;Assignments&amp;quot; heading was rendering as body text because its class was referenced as a plain string instead of the CSS Modules export (&amp;lt;code&amp;gt;styles['assignments-title']&amp;lt;/code&amp;gt;)&lt;br /&gt;
&lt;br /&gt;
=== StudentTaskDetail (Detail View) ===&lt;br /&gt;
* Visual timeline with progress line and stage nodes (completed/current/pending)&lt;br /&gt;
* Task links list — &amp;quot;Your work&amp;quot; and &amp;quot;Others' work&amp;quot; conditionally rendered based on &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;quot;Send Email To Reviewers&amp;quot; button repositioned to align with the task list rather than floating disconnected at the page top&lt;br /&gt;
&lt;br /&gt;
=== ViewTeamGrades (Heatgrid) ===&lt;br /&gt;
* Consolidated ~6 sequential API calls into one &amp;lt;code&amp;gt;GET /grades/:id/view_our_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
* SectionHeader rows split into a sticky label cell + scrolling spacer cell so they stay fixed during horizontal scroll&lt;br /&gt;
* Score cells show reviewer feedback as a hover tooltip&lt;br /&gt;
* Round headings simplified from &amp;quot;Review (Round: 1 of 2)&amp;quot; to &amp;quot;Round 1&amp;quot;&lt;br /&gt;
&lt;br /&gt;
== Design Principles ==&lt;br /&gt;
&lt;br /&gt;
=== Single Responsibility ===&lt;br /&gt;
Score aggregation is layered: &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; owns item-level scoring, &amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt; owns per-map/per-round weighting, &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; owns cross-map averaging — each level only knows about the one below it.&lt;br /&gt;
&lt;br /&gt;
=== Don't Repeat Yourself ===&lt;br /&gt;
The weighted-average logic that was duplicated between &amp;lt;code&amp;gt;AssignmentTeam#aggregate_review_grade&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;ReviewAggregator#compute_average_review_score&amp;lt;/code&amp;gt; was consolidated into a single &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt;, and the now-dead &amp;lt;code&amp;gt;ReviewAggregator&amp;lt;/code&amp;gt; concern was deleted.&lt;br /&gt;
&lt;br /&gt;
=== Performance by Design ===&lt;br /&gt;
Eager loading (&amp;lt;code&amp;gt;.includes&amp;lt;/code&amp;gt;) and in-memory filtering (&amp;lt;code&amp;gt;.reject&amp;lt;/code&amp;gt;) were chosen deliberately over additional database round-trips after profiling showed the heatgrid endpoint firing hundreds of queries under the old implementation.&lt;br /&gt;
&lt;br /&gt;
== Test Plan ==&lt;br /&gt;
&lt;br /&gt;
=== Backend Tests ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! # !! Test !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| 1 || &amp;lt;code&amp;gt;StudentTask.all_teammates&amp;lt;/code&amp;gt; groups teammates by course || Returns &amp;lt;code&amp;gt;{course_name =&amp;gt; [fullnames]}&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 2 || &amp;lt;code&amp;gt;all_teammates&amp;lt;/code&amp;gt; excludes calibrated assignments || Calibrated-assignment teammates excluded&lt;br /&gt;
|-&lt;br /&gt;
| 3 || &amp;lt;code&amp;gt;all_teammates&amp;lt;/code&amp;gt; excludes the user themself || Solo team returns empty hash&lt;br /&gt;
|-&lt;br /&gt;
| 4 || &amp;lt;code&amp;gt;GET /student_tasks/list&amp;lt;/code&amp;gt; returns all tasks for user || 200 OK with task array&lt;br /&gt;
|-&lt;br /&gt;
| 5 || &amp;lt;code&amp;gt;GET /student_tasks/show/:id&amp;lt;/code&amp;gt; returns due_dates timeline || 200 OK with &amp;lt;code&amp;gt;due_dates&amp;lt;/code&amp;gt; array&lt;br /&gt;
|-&lt;br /&gt;
| 6 || &amp;lt;code&amp;gt;GET /student_tasks/team&amp;lt;/code&amp;gt; groups teammates by course || 200 OK with grouped hash&lt;br /&gt;
|-&lt;br /&gt;
| 7 || Heatgrid &amp;lt;code&amp;gt;#&amp;lt;/code&amp;gt; column excludes SectionHeaders from numbering || Sequential numbering skips header rows&lt;br /&gt;
|-&lt;br /&gt;
| 8 || &amp;lt;code&amp;gt;accumulate_round_scores&amp;lt;/code&amp;gt; filters SectionHeaders without re-querying || No additional SQL fired against preloaded association&lt;br /&gt;
|-&lt;br /&gt;
| 9 || &amp;lt;code&amp;gt;AssignmentQuestionnaire&amp;lt;/code&amp;gt; weight validation || &amp;lt;code&amp;gt;questionnaire_weight&amp;lt;/code&amp;gt; must be 0 when rubric has no scored questions&lt;br /&gt;
|-&lt;br /&gt;
| 10 || &amp;lt;code&amp;gt;ResponseMap.compute_average_reviewer_score&amp;lt;/code&amp;gt; || Weighted average matches Σ(grade × weight) / Σ(weight)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Frontend Tests ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! # !! Test !! Expected Result&lt;br /&gt;
|-&lt;br /&gt;
| 1 || &amp;lt;code&amp;gt;StudentTasks&amp;lt;/code&amp;gt; renders &amp;quot;Assignments&amp;quot; heading with correct styling || Heading uses module class, renders bold and large&lt;br /&gt;
|-&lt;br /&gt;
| 2 || &amp;lt;code&amp;gt;StudentTaskDetail&amp;lt;/code&amp;gt; hides &amp;quot;Your work&amp;quot; when &amp;lt;code&amp;gt;can_submit&amp;lt;/code&amp;gt; is false || List item not rendered&lt;br /&gt;
|-&lt;br /&gt;
| 3 || &amp;lt;code&amp;gt;StudentTaskDetail&amp;lt;/code&amp;gt; hides &amp;quot;Others' work&amp;quot; when &amp;lt;code&amp;gt;can_review&amp;lt;/code&amp;gt; is false || List item not rendered&lt;br /&gt;
|-&lt;br /&gt;
| 4 || &amp;lt;code&amp;gt;heatgridUtils&amp;lt;/code&amp;gt; itemNumber skips SectionHeader rows || Numbering matches scored items only&lt;br /&gt;
|-&lt;br /&gt;
| 5 || &amp;lt;code&amp;gt;ReviewTable&amp;lt;/code&amp;gt; SectionHeader row stays fixed during horizontal scroll || Header cell &amp;lt;code&amp;gt;position: sticky&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| 6 || &amp;lt;code&amp;gt;ReviewTableRow&amp;lt;/code&amp;gt; shows tooltip only when a comment exists || No tooltip box when &amp;lt;code&amp;gt;data-question&amp;lt;/code&amp;gt; is undefined&lt;br /&gt;
|-&lt;br /&gt;
| 7 || Heatgrid column borders render as &amp;lt;code&amp;gt;#ddd&amp;lt;/code&amp;gt; || No black borders after &amp;lt;code&amp;gt;tbl_heat&amp;lt;/code&amp;gt; class applied&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Relevant Links ==&lt;br /&gt;
&lt;br /&gt;
=== Repos ===&lt;br /&gt;
* https://github.com/expertiza/reimplementation-back-end&lt;br /&gt;
* https://github.com/expertiza/reimplementation-front-end&lt;br /&gt;
&lt;br /&gt;
=== Pull Requests ===&lt;br /&gt;
* Backend: https://github.com/expertiza/reimplementation-back-end/pull/352&lt;br /&gt;
* Frontend: https://github.com/expertiza/reimplementation-front-end/pull/182&lt;br /&gt;
&lt;br /&gt;
=== Demo Video ===&lt;br /&gt;
* (add link once recorded)&lt;br /&gt;
&lt;br /&gt;
== Developer Guidance ==&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
==== Setup &amp;amp; Run (via Docker) ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
docker compose up&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Within the container ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
bundle exec rspec spec/models/student_task_spec.rb&lt;br /&gt;
bundle exec rspec spec/requests/api/v1/student_tasks_controller_spec.rb&lt;br /&gt;
bundle exec rspec spec/requests/api/v1/grades_controller_spec.rb&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
==== Setup &amp;amp; Run ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm install&lt;br /&gt;
npm start&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Running Frontend Tests ====&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
npm test -- StudentTaskDetail   # run a single suite&lt;br /&gt;
npm test                        # run full suite&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Future Scope ==&lt;br /&gt;
&lt;br /&gt;
=== Reviewer reputation-weighted grading ===&lt;br /&gt;
Integrate an external review-quality grader (e.g. an LLM-based reviewer grader) so &amp;lt;code&amp;gt;ResponseMap.reviewer_reputation_for&amp;lt;/code&amp;gt; reflects actual review quality instead of the current &amp;lt;code&amp;gt;1.0&amp;lt;/code&amp;gt; placeholder for every reviewer.&lt;br /&gt;
&lt;br /&gt;
=== Email-the-authors integration ===&lt;br /&gt;
Wire &amp;quot;Send Email To Reviewers&amp;quot; to a real backend endpoint instead of the current placeholder route.&lt;br /&gt;
&lt;br /&gt;
=== Dedicated StudentTask abstraction ===&lt;br /&gt;
&amp;lt;code&amp;gt;StudentTask&amp;lt;/code&amp;gt; remains a composed, non-persisted object. A future dedicated table could centralize derived state and simplify queries if the feature's scope grows further.&lt;br /&gt;
&lt;br /&gt;
== Mentor ==&lt;br /&gt;
[mailto:kgudipe@ncsu.edu Koushik Gudipelly]&lt;br /&gt;
&lt;br /&gt;
== Members ==&lt;br /&gt;
[mailto:rsgopara@ncsu.edu Ravi Goparaju]&lt;br /&gt;
[mailto:xmi@ncsu.edu Xiangjun Mi]&lt;br /&gt;
&lt;br /&gt;
== Completed by ==&lt;br /&gt;
[mailto:blalu@ncsu.edu Bestin Lalu]&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2609._Review_calibration&amp;diff=167976</id>
		<title>CSC/ECE 517 Spring 2026 - E2609. Review calibration</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2609._Review_calibration&amp;diff=167976"/>
		<updated>2026-04-17T00:49:54Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Problem statements */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Project overview ==&lt;br /&gt;
&lt;br /&gt;
Expertiza supports course assignments with peer review. The reimplementation exposes a '''Rails JSON API''' and a '''React''' single-page application.&lt;br /&gt;
&lt;br /&gt;
||| This should describe the motivation for calibration. It's a way to make sure that the reviewer knows what they're doing.  The instructor evaluates a &amp;quot;calibration submission&amp;quot; before the students do, and then the system compares how close the student's review is to the instructor's review. If it's close, the student can be deemed competent. ||| This project implements a '''calibration assignment''' in the reimplementation: instructors use the '''Calibration''' tab to register '''calibration participants''' by username, use '''Begin''' to open the same rubric UI students see, and '''enter the calibration review''' as an instructor &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; on a &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; &amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt;. A '''comparison report''' summarizes how each student’s calibration review compares to the '''instructor’s review''' of the same submission, per rubric item. As in the course description, students review work that '''course staff''' have already reviewed; the system stores reviews and rubric scores for training and reporting.&lt;br /&gt;
&lt;br /&gt;
== Problem statements ==&lt;br /&gt;
&lt;br /&gt;
# '''Data model.''' Normal peer review and calibration must be distinguishable. The implementation adds &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; on &amp;lt;code&amp;gt;response_maps&amp;lt;/code&amp;gt; ||| response maps are the way that Expertiza knows who's reviewing whom.||| so the system can select the right maps for listing, saving the instructor calibration review at the &amp;lt;code&amp;gt;instructor_response&amp;lt;/code&amp;gt; endpoint, and report logic.&lt;br /&gt;
# '''Instructor workflow.''' Teaching staff add calibration participants by username, open the review via '''Begin''', and save drafts or submit the instructor calibration review. Submit must lock that review so comparison reports stay stable.&lt;br /&gt;
# '''Reporting.''' For each calibration submission the instructor needs a view that compares many student calibration responses to one instructor response on the same rubric, with per-item summaries suitable for charts.&lt;br /&gt;
# '''Authorization''' Only instructor or teaching-assistant roles for the assignment may use the calibration instructor APIs.&lt;br /&gt;
# '''Controller boundaries''' Instructor calibration maps, instructor-review persistence, '''and''' the comparison-report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' are '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''' responsibilities. One '''new student review controller''' covers '''student'''-scoped HTTP: review listing, calibration links, and comparison affordances. Submitted content uses the '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;''' stack.&lt;br /&gt;
# '''Student workflow ''' Students see their work as reviewers through a '''student review''' API: list the '''&amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt;s / reviews''' relevant to them. Where &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; applies, provide a '''link to the calibration comparison''' for instructor versus class scores and enough '''score/feedback''' context that the student can see '''how they compare''' to the instructor on that calibration submission. Teaching-staff map management remains on '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''; the student API lists maps and comparison links and reuses the same report JSON where appropriate.&lt;br /&gt;
&lt;br /&gt;
== Design goals ==&lt;br /&gt;
&lt;br /&gt;
* '''Clarity''' — A reader can follow the instructor path from configuration through '''Begin''' and the instructor calibration review to the comparison report, '''and''' the student path from their review list to calibration comparison where applicable.&lt;br /&gt;
* '''Separation''' — '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''' owns every teaching-staff calibration endpoint: the map list &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;, add participant, '''Begin''', instructor response, map delete, and the comparison-report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;'''. A '''dedicated student review controller''' for student-facing HTTP lists student reviews / maps and exposes calibration comparison entry points, delegating instructor tab behavior and report aggregation to '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''.&lt;br /&gt;
* '''DRY''' — One rubric &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;Answer&amp;lt;/code&amp;gt; persistence path using a '''shared rubric writer''' for instructor calibration and other reviews where applicable. One submitted-content path via '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;''' for list rows and report payloads.&lt;br /&gt;
* '''Minimal footprint''' — Ship what the course feature needs: an instructor path to enter calibration reviews and a student path to compare their calibration work to the instructor’s. Prefer small methods, clear names, focused PRs, and plain comments on the calibration surface.&lt;br /&gt;
* '''Testability''' — Prefer factories and request specs for repeatable scenarios.&lt;br /&gt;
* '''Documentation''' — Controllers are described in prose with routes in the appendix; screenshots pair UI with the backing API.&lt;br /&gt;
&lt;br /&gt;
== Database design ==&lt;br /&gt;
&lt;br /&gt;
Column: &amp;lt;code&amp;gt;response_maps.for_calibration&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Type: boolean, default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;NOT NULL&amp;lt;/code&amp;gt;.&lt;br /&gt;
* When &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the row is part of the calibration workflow for that assignment: either the instructor-to-calibration-participant map or a student calibrator map that shares the same &amp;lt;code&amp;gt;reviewee_id&amp;lt;/code&amp;gt; as other calibration maps for that participant.&lt;br /&gt;
* When &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;, the row behaves as a normal peer-review map.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Solution approach ==&lt;br /&gt;
&lt;br /&gt;
=== Phase 1 — Calibration participants and response maps ===&lt;br /&gt;
&lt;br /&gt;
Instructors register calibration participants on the assignment and obtain a &amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt; that backs the instructor &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; for that calibration submission. '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''', extended for calibration, validates assignment and teaching-staff role, ensures the target user is an &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; when required, sets &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; on the map, and returns list rows with display name, submitted artifact summary using the same helpers as '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;''', and review status. Adding a participant by username is idempotent.&lt;br /&gt;
&lt;br /&gt;
=== Phase 2 — Reporting and comparison ===&lt;br /&gt;
&lt;br /&gt;
The comparison-report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' returns the JSON consumed by the Calibration report React page and is implemented on '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''. It returns one payload for the stacked chart and rubric detail views. The server resolves the instructor calibration map, identifies the reviewee as the calibration participant, loads rubric &amp;lt;code&amp;gt;Item&amp;lt;/code&amp;gt;s, loads instructor and student &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;Answer&amp;lt;/code&amp;gt; data for the same reviewed work. Per map, aggregation uses '''submitted''' student responses, taking the latest row by &amp;lt;code&amp;gt;updated_at&amp;lt;/code&amp;gt; among submitted responses, then runs &amp;lt;code&amp;gt;CalibrationPerItemSummary&amp;lt;/code&amp;gt; for per-item buckets.&lt;br /&gt;
&lt;br /&gt;
=== Phase 3 — Instructor calibration review UI ===&lt;br /&gt;
&lt;br /&gt;
The instructor uses the same rubric experience as students: draft and submit, then the submitted rubric is locked for further edits. That matches the course description of a normal-looking rubric review after '''Begin''' and establishes a stable instructor review for downstream comparison reports.&lt;br /&gt;
&lt;br /&gt;
=== High-level architecture ===&lt;br /&gt;
&lt;br /&gt;
All '''teaching-staff''' calibration JSON — the calibration map '''list''', mutating map and review actions including add participant, '''Begin''', instructor response, and delete, '''and''' the comparison-report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' — goes through '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''. Student-facing review HTTP lives in '''one new student review controller''' whose Ruby class and path names follow &amp;lt;code&amp;gt;config/routes.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* '''Review mappings — JSON, teaching staff.''' Extended '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''' serves the Calibration tab: list calibration maps, add a participant and the corresponding &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; map, '''Begin''' metadata, save or submit the instructor calibration review, optional map delete, '''and''' the comparison-report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;'''. Implement the report action as a thin entry point that delegates to POROs or private methods for assembly. &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;Answer&amp;lt;/code&amp;gt; persistence for saves uses the same shared rubric writer as elsewhere where possible.&lt;br /&gt;
&lt;br /&gt;
* '''Report — JSON, teaching staff.''' The report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' is a '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''' action; declare it as a member or collection route in &amp;lt;code&amp;gt;config/routes.rb&amp;lt;/code&amp;gt; next to the action implementation. It returns JSON: rubric items, instructor and student &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; data for the reviewee, &amp;lt;code&amp;gt;per_item_summary&amp;lt;/code&amp;gt;, reviewee hyperlinks/files via '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;'''.&lt;br /&gt;
&lt;br /&gt;
* '''Student reviews — JSON.''' The '''student review controller''' lists '''&amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt;s / reviews''' for the student; for '''&amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt;''' rows it exposes a '''link''' to the teaching-staff comparison report JSON and UI to compare '''student vs instructor''' scores and feedback. Thin layer over existing review models; instructor map listing and report aggregation stay in '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''.&lt;br /&gt;
&lt;br /&gt;
* '''React SPA.''' Assignment editor → Calibration tab → '''Begin''' → instructor rubric page → report page '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;'''.&lt;br /&gt;
&lt;br /&gt;
* '''Server-side helpers.''' &amp;lt;code&amp;gt;CalibrationPerItemSummary&amp;lt;/code&amp;gt; builds per-item chart buckets. Submitted hyperlinks/files always go through '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;'''; details are under '''Implementation''' and '''Backend'''.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== UML diagram ==&lt;br /&gt;
&lt;br /&gt;
[[File:E2609-calibration-domain-class-uml.png|720px|center]]&lt;br /&gt;
&lt;br /&gt;
Instructors and students are &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt;s. The instructor’s calibration map is a &amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt; with &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; true whose reviewer is the instructor participant and whose reviewee is the calibration participant. Student calibration maps share the same &amp;lt;code&amp;gt;reviewee_id&amp;lt;/code&amp;gt;, the calibration author, but use other &amp;lt;code&amp;gt;reviewer_id&amp;lt;/code&amp;gt; values. &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;Answer&amp;lt;/code&amp;gt; store scores. List and report use '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;''' for hyperlinks/files; &amp;lt;code&amp;gt;CalibrationPerItemSummary&amp;lt;/code&amp;gt; supports report aggregation.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Flow: Review calibration ==&lt;br /&gt;
&lt;br /&gt;
'''Figure 1a — Calibration setup, overview.''' Student lane: review list and submit via the student review stack. Staff lane: Calibration tab and rubric send teaching-staff calibration traffic to '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''. The two lanes can happen in any order; details and routes are in '''Implementation''' and the '''Appendix'''.&lt;br /&gt;
&lt;br /&gt;
[[File:E2609-calibration-pipelines-flow.png|820px|center]]&lt;br /&gt;
&lt;br /&gt;
'''Figure 1b — Calibration report, overview.''' Teaching staff open the report in React; the page issues one &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; to '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''' and receives either &amp;lt;code&amp;gt;403&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;200&amp;lt;/code&amp;gt; report JSON. Assembly of rubric, scores, &amp;lt;code&amp;gt;CalibrationPerItemSummary&amp;lt;/code&amp;gt;, and the &amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt; stack is specified in code and in '''Implementation''' and '''Backend'''; the diagram highlights the browser–Rails exchange.&lt;br /&gt;
&lt;br /&gt;
[[File:E2609-calibration-flow-report.png|720px|center]]&lt;br /&gt;
&lt;br /&gt;
Figure 1b: maps for one calibration run share a &amp;lt;code&amp;gt;reviewee_id&amp;lt;/code&amp;gt;; &amp;lt;code&amp;gt;per_item_summary&amp;lt;/code&amp;gt; aggregates '''submitted''' student &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; rows per map using the latest &amp;lt;code&amp;gt;updated_at&amp;lt;/code&amp;gt; among submitted rows.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Implementation ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Area&lt;br /&gt;
! Implementation &lt;br /&gt;
! Verify&lt;br /&gt;
|-&lt;br /&gt;
| Teaching-staff HTTP&lt;br /&gt;
| Implement list, create, begin, instructor response save, map delete, '''and''' comparison-report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' on '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''', optionally split into a Rails concern mixed into that controller, scoped with &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; and teaching-staff authorization. Add '''one new student review controller''': list student '''&amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt;s / reviews'''; for &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; items, '''link''' to the comparison report and expose '''student vs instructor''' score/feedback comparison. Keep the student controller thin: delegate to existing review models and reuse the same report JSON payload shape where appropriate.&lt;br /&gt;
| Request specs for all &amp;lt;code&amp;gt;review_mappings&amp;lt;/code&amp;gt; calibration actions; request specs for student review list, calibration link, and comparison paths; coverage lives on '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''' for teaching-staff calibration HTTP.&lt;br /&gt;
|-&lt;br /&gt;
| Report &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;&lt;br /&gt;
| Report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' on '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''': thin entry + PORO/private steps; stable JSON for the React report page. Uses &amp;lt;code&amp;gt;CalibrationPerItemSummary&amp;lt;/code&amp;gt; and the '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;''' stack for reviewee hyperlinks/files.&lt;br /&gt;
| Request spec for report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' documents required JSON keys and error cases.&lt;br /&gt;
|-&lt;br /&gt;
| Instructor review write&lt;br /&gt;
| Instructor calibration draft/submit on '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''. Delegate &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;Answer&amp;lt;/code&amp;gt; persistence to a '''shared rubric writer'''; action handles guards, validation, and JSON only.&lt;br /&gt;
| Request specs: draft, submit, 422, invalid &amp;lt;code&amp;gt;answers&amp;lt;/code&amp;gt; payload.&lt;br /&gt;
|-&lt;br /&gt;
| Submitted content&lt;br /&gt;
| Build list and report &amp;lt;code&amp;gt;{ hyperlinks, files }&amp;lt;/code&amp;gt; through '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;''' or the same service object that controller already delegates to.&lt;br /&gt;
| Specs assert list and report payloads match submitted-content for the same participant.&lt;br /&gt;
|-&lt;br /&gt;
| Team + participant bootstrap&lt;br /&gt;
| Implement solo-team and related bootstrap logic in a '''team/participant service''' called from map '''create''', with '''&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;''' updates limited to what map create and listing need. Keep &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; hyperlink behavior correct for those paths.&lt;br /&gt;
| Service/unit tests + create path; hyperlink regressions if any.&lt;br /&gt;
|-&lt;br /&gt;
| Scope hygiene&lt;br /&gt;
| Scope edits to calibration controllers, services, routes, and React surfaces named in this document, plus the smallest shared-model touches that map create and listing require.&lt;br /&gt;
| Code review checklist; small, focused diffs.&lt;br /&gt;
|-&lt;br /&gt;
| Report UI&lt;br /&gt;
| &amp;lt;code&amp;gt;CalibrationReview&amp;lt;/code&amp;gt; and related report components match the mockup while consuming the report JSON shape from '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''.&lt;br /&gt;
| Vitest; refresh '''Figure 2''' left pane when the shipped UI changes.&lt;br /&gt;
|-&lt;br /&gt;
| E2E · repo · docs&lt;br /&gt;
| Browser E2E over &amp;lt;code&amp;gt;review_mappings&amp;lt;/code&amp;gt; calibration routes and report tab. Record generated-asset rules for this repo in the PR checklist or team wiki so commits stay consistent.&lt;br /&gt;
| CI or &amp;lt;code&amp;gt;test:e2e&amp;lt;/code&amp;gt;; PR checklist.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Backend — controller behavior ==&lt;br /&gt;
&lt;br /&gt;
Teaching-staff APIs use auth, '''&amp;lt;code&amp;gt;authorize&amp;lt;/code&amp;gt;''', and staff checks consistent with the assignment. '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''', extended for calibration, hosts all teaching-staff calibration endpoints: it lists calibration maps where the current user is the instructor reviewer and &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; is true; each row includes map/reviewee fields, '''&amp;lt;code&amp;gt;participant_name&amp;lt;/code&amp;gt;''', '''&amp;lt;code&amp;gt;review_status&amp;lt;/code&amp;gt;''' from the latest '''&amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt;''', and '''&amp;lt;code&amp;gt;submitted_content&amp;lt;/code&amp;gt;''' built with the '''same service/helpers as &amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;'''. '''Create''' resolves username to '''&amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt;''', runs team bootstrap via a '''dedicated service''', '''&amp;lt;code&amp;gt;find_or_create_by!&amp;lt;/code&amp;gt;''' the instructor→reviewee map with &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; true, returns '''&amp;lt;code&amp;gt;201&amp;lt;/code&amp;gt;'''. '''Begin''' returns '''&amp;lt;code&amp;gt;map_id&amp;lt;/code&amp;gt;''', optional '''&amp;lt;code&amp;gt;response_id&amp;lt;/code&amp;gt;''', '''&amp;lt;code&amp;gt;redirect_to&amp;lt;/code&amp;gt;'''. '''Instructor response save''' validates input, enforces the submitted-state rules, delegates persistence to a '''shared writer''', returns '''&amp;lt;code&amp;gt;200&amp;lt;/code&amp;gt;''' JSON. '''Destroy''' returns '''&amp;lt;code&amp;gt;204&amp;lt;/code&amp;gt;''' after ownership checks. '''Comparison-report &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' for the instructor’s calibration map id on the same controller returns rubric items, '''&amp;lt;code&amp;gt;instructor_response&amp;lt;/code&amp;gt;''', submitted student '''&amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt;s''', '''&amp;lt;code&amp;gt;CalibrationPerItemSummary.build&amp;lt;/code&amp;gt;''', reviewee hyperlinks/files via the '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt; stack''', '''&amp;lt;code&amp;gt;200&amp;lt;/code&amp;gt;''' JSON, or '''&amp;lt;code&amp;gt;404&amp;lt;/code&amp;gt;''' / '''&amp;lt;code&amp;gt;422&amp;lt;/code&amp;gt;''' when assignment, map, or rubric preconditions fail.&lt;br /&gt;
&lt;br /&gt;
'''Student review controller — new, thin layer''' — Lists '''&amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt;s / reviews''' for the logged-in student as reviewer; for '''&amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt;''' maps, provides a '''link''' to the calibration comparison using the instructor-facing report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' or a student-safe JSON projection, plus fields and UI so the student can see '''how their scores and feedback compare''' to the instructor’s. Delegates to existing review / &amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt; models; instructor Calibration tab CRUD, rubric save, and server-side report aggregation live in '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Appendix — API routes ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Method&lt;br /&gt;
! Path pattern&lt;br /&gt;
! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;/assignments/:assignment_id/review_mappings/…&amp;lt;/code&amp;gt;&lt;br /&gt;
| '''Calibration list.''' List calibration &amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt; rows for teaching staff; exact path under &amp;lt;code&amp;gt;review_mappings&amp;lt;/code&amp;gt; is defined in &amp;lt;code&amp;gt;config/routes.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;/assignments/:assignment_id/review_mappings/…&amp;lt;/code&amp;gt;&lt;br /&gt;
| '''Calibration create.''' Add calibration participant; request includes &amp;lt;code&amp;gt;username&amp;lt;/code&amp;gt;. Exact path in &amp;lt;code&amp;gt;config/routes.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;DELETE&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;/assignments/:assignment_id/review_mappings/…/:id&amp;lt;/code&amp;gt;&lt;br /&gt;
| '''Calibration map delete.''' Remove one calibration map by id; exact path in &amp;lt;code&amp;gt;config/routes.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;/assignments/:assignment_id/review_mappings/…/:id/…&amp;lt;/code&amp;gt;&lt;br /&gt;
| '''Begin.''' Return navigation JSON for instructor calibration rubric SPA; exact member path in &amp;lt;code&amp;gt;config/routes.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;/assignments/:assignment_id/review_mappings/…/:id/…&amp;lt;/code&amp;gt;&lt;br /&gt;
| '''Instructor response.''' Save draft or submit instructor calibration review; exact member path in &amp;lt;code&amp;gt;config/routes.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;/assignments/:assignment_id/review_mappings/…&amp;lt;/code&amp;gt;&lt;br /&gt;
| '''Calibration report.''' Return comparison report JSON for the instructor calibration map id; exact path in &amp;lt;code&amp;gt;config/routes.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;/assignments/:assignment_id/…&amp;lt;/code&amp;gt;&lt;br /&gt;
| '''Student review controller.''' Student review list, calibration report links, and comparison to instructor; verbs and paths declared in &amp;lt;code&amp;gt;config/routes.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Frontend — data expectations and user flow ==&lt;br /&gt;
&lt;br /&gt;
'''Calibration tab table:''' Each row shows map id, display name, submitted links and files summary, and status: '''Not started''', '''Draft''', or '''Submitted'''. Data comes from the teaching-staff calibration '''list''' &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; on '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''' under &amp;lt;code&amp;gt;review_mappings&amp;lt;/code&amp;gt; routes, with submitted fields from the same pipeline as '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;'''.&lt;br /&gt;
&lt;br /&gt;
'''Report page:''' Rubric items in order, instructor scores and comments per item, each student’s scores, and per-item buckets from the report JSON.&lt;br /&gt;
&lt;br /&gt;
'''User flow:'''&lt;br /&gt;
&lt;br /&gt;
# Open Assignment editor, then Calibration tab.&lt;br /&gt;
# Enter username; system ensures participant and &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; map.&lt;br /&gt;
# &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; calibration list on &amp;lt;code&amp;gt;review_mappings&amp;lt;/code&amp;gt;; table refreshes.&lt;br /&gt;
# Begin; &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; begin; SPA opens the instructor calibration review page.&lt;br /&gt;
# Save draft or submit; &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; instructor response on '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''.&lt;br /&gt;
# Open report; &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; comparison JSON from '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''; charts and rubric detail render.&lt;br /&gt;
&lt;br /&gt;
'''Student review list: ''' The student review controller lists the student’s reviews / '''&amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt;s'''. Calibration rows show a '''link''' to open the comparison for instructor versus class scores and, where the UI implements it, the student’s position relative to the instructor.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Assignment editor — Calibration tab ===&lt;br /&gt;
&lt;br /&gt;
[[File:E2609-calibration-assignment-tab.png|480px|center]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;text-align:center; margin-top:0.2em;&amp;quot;&amp;gt;'''Figure 1.''' Reference UI — assignment editor Calibration tab: add participants, open report or '''Begin''' the instructor calibration review, and see submitted items. Each row is filled from the calibration '''list''' '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' on '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''' under &amp;lt;code&amp;gt;review_mappings&amp;lt;/code&amp;gt;, with &amp;lt;code&amp;gt;participant_name&amp;lt;/code&amp;gt;, submitted content from the '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;''' pipeline, and &amp;lt;code&amp;gt;review_status&amp;lt;/code&amp;gt;. Adding the same username again is idempotent.&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Calibration report: comparison UI ===&lt;br /&gt;
&lt;br /&gt;
{| style=&amp;quot;width:100%; max-width:980px; margin:0.8em auto; border-collapse:separate; border-spacing:10px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;vertical-align:top; text-align:center; width:50%;&amp;quot; | '''Reference UI — Class comparison, stacked chart tab'''&lt;br /&gt;
| style=&amp;quot;vertical-align:top; text-align:center; width:50%;&amp;quot; | '''Mockup chart layout; same report &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; JSON from &amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;vertical-align:top; text-align:center;&amp;quot; | [[File:E2609-calibration-report-stacked.png|440px]]&lt;br /&gt;
| style=&amp;quot;vertical-align:top; text-align:center;&amp;quot; | [[File:E2609-calibration-stacked-chart-improved-mockup.png|440px]]&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;text-align:center; margin-top:0.35em;&amp;quot;&amp;gt;'''Figure 2.''' Side by side: reference stacked-chart tab versus the intended layout. The mockup adds per-criterion axis labels, framing, legend, and full criterion text in tooltips in &amp;lt;code&amp;gt;CalibrationReview&amp;lt;/code&amp;gt;. Same report JSON from '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''.&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[File:E2609-calibration-report-rubric-detail.png|480px|center]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;text-align:center; margin-top:0.2em;&amp;quot;&amp;gt;'''Figure 3.''' Reference UI — rubric detail: instructor review versus class summary and reviewer selections, using the comparison-report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' JSON from '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''.&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Testing and Verification ==&lt;br /&gt;
&lt;br /&gt;
=== Test Plan ===&lt;br /&gt;
&lt;br /&gt;
==== Automated Testing — RSpec ====&lt;br /&gt;
&lt;br /&gt;
* '''Request specs''' for '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''' calibration: list, create participant/map, begin, instructor response for draft, submit, and error paths, destroy, and comparison-report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;'''. Co-locate spec files under &amp;lt;code&amp;gt;spec/requests/&amp;lt;/code&amp;gt; with the route names in &amp;lt;code&amp;gt;config/routes.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
* '''Request specs''' for the '''student review controller''': list reviews/maps; '''&amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt;''' rows include correct report link or comparison payload; responses match the logged-in student reviewer.&lt;br /&gt;
* Service specs for aggregation logic, for example &amp;lt;code&amp;gt;spec/services/calibration_per_item_summary_spec.rb&amp;lt;/code&amp;gt; next to &amp;lt;code&amp;gt;CalibrationPerItemSummary&amp;lt;/code&amp;gt;.&lt;br /&gt;
* '''Factories / fixtures''' for &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; maps, participants, and submitted responses as needed for the above.&lt;br /&gt;
&lt;br /&gt;
==== Frontend Testing — Vitest ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;src/pages/Assignments/__tests__/CalibrationReview.test.tsx&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;src/pages/Assignments/__tests__/CalibrationInstructorReview.test.tsx&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;src/pages/Assignments/__tests__/calibrationReportNormalize.test.ts&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;src/pages/Assignments/__tests__/calibrationSummary.test.ts&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;src/pages/Assignments/__tests__/calibrationReportDemo.test.ts&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== End-to-end — E2E ====&lt;br /&gt;
&lt;br /&gt;
# Teaching staff: assignment editor → Calibration tab → calibration map list &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; on &amp;lt;code&amp;gt;review_mappings&amp;lt;/code&amp;gt; → table or empty state renders cleanly in the browser.&lt;br /&gt;
# Teaching staff: comparison report &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; on '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''' → “Class comparison (stacked)” and “Rubric detail” render; chart region populated when &amp;lt;code&amp;gt;per_item_summary&amp;lt;/code&amp;gt; is non-empty.&lt;br /&gt;
# Teaching staff: '''Begin''', including opening the calibration review by direct URL if supported → draft save → submit → UI matches post-submit lock rules.&lt;br /&gt;
# Student: open student review list → calibration row shows '''link''' to comparison for instructor versus class view → student can see '''how their review compares''' to the instructor’s when that panel is implemented.&lt;br /&gt;
&lt;br /&gt;
==== Manual UI Testing ====&lt;br /&gt;
&lt;br /&gt;
# Login as instructor/TA.&lt;br /&gt;
# Open assignment editor → Calibration tab.&lt;br /&gt;
# Add a calibration participant by username.&lt;br /&gt;
# Click '''View report''' and verify stacked and rubric detail tabs.&lt;br /&gt;
# Enter/edit instructor review: save draft, submit, and confirm the form locks.&lt;br /&gt;
# Login as student calibrator: open student review list; for a calibration map, follow the report/comparison link and confirm scores or feedback align with expectations versus the instructor view.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Future Scope ==&lt;br /&gt;
&lt;br /&gt;
* Add an overall calibration score and trend visualization per participant.&lt;br /&gt;
* Provide additional endpoints for:&lt;br /&gt;
** calibration summary per map,&lt;br /&gt;
** detailed agreement breakdown per item across all rounds.&lt;br /&gt;
* Improve diagnostics, such as highlighting criteria with the largest score spread across reviewers.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=Main_Page Expertiza wiki main page]&lt;br /&gt;
* Front-end pull request: [https://github.com/expertiza/reimplementation-front-end/pull/164 expertiza/reimplementation-front-end#164]&lt;br /&gt;
* Back-end pull request: [https://github.com/expertiza/reimplementation-back-end/pull/323 expertiza/reimplementation-back-end#323]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Team ==&lt;br /&gt;
&lt;br /&gt;
* Mentor: Dr. Ed Gehringer&lt;br /&gt;
* Team members: Xiangjun Mi, Rujuta Palimkar, Emma Hassler&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2609._Review_calibration&amp;diff=167975</id>
		<title>CSC/ECE 517 Spring 2026 - E2609. Review calibration</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2609._Review_calibration&amp;diff=167975"/>
		<updated>2026-04-17T00:46:53Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Project overview */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
== Project overview ==&lt;br /&gt;
&lt;br /&gt;
Expertiza supports course assignments with peer review. The reimplementation exposes a '''Rails JSON API''' and a '''React''' single-page application.&lt;br /&gt;
&lt;br /&gt;
||| This should describe the motivation for calibration. It's a way to make sure that the reviewer knows what they're doing.  The instructor evaluates a &amp;quot;calibration submission&amp;quot; before the students do, and then the system compares how close the student's review is to the instructor's review. If it's close, the student can be deemed competent. ||| This project implements a '''calibration assignment''' in the reimplementation: instructors use the '''Calibration''' tab to register '''calibration participants''' by username, use '''Begin''' to open the same rubric UI students see, and '''enter the calibration review''' as an instructor &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; on a &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; &amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt;. A '''comparison report''' summarizes how each student’s calibration review compares to the '''instructor’s review''' of the same submission, per rubric item. As in the course description, students review work that '''course staff''' have already reviewed; the system stores reviews and rubric scores for training and reporting.&lt;br /&gt;
&lt;br /&gt;
== Problem statements ==&lt;br /&gt;
&lt;br /&gt;
# '''Data model.''' Normal peer review and calibration must be distinguishable. The implementation adds &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; on &amp;lt;code&amp;gt;response_maps&amp;lt;/code&amp;gt; so the system can select the right maps for listing, saving the instructor calibration review at the &amp;lt;code&amp;gt;instructor_response&amp;lt;/code&amp;gt; endpoint, and report logic.&lt;br /&gt;
# '''Instructor workflow.''' Teaching staff add calibration participants by username, open the review via '''Begin''', and save drafts or submit the instructor calibration review. Submit must lock that review so comparison reports stay stable.&lt;br /&gt;
# '''Reporting.''' For each calibration submission the instructor needs a view that compares many student calibration responses to one instructor response on the same rubric, with per-item summaries suitable for charts.&lt;br /&gt;
# '''Authorization''' Only instructor or teaching-assistant roles for the assignment may use the calibration instructor APIs.&lt;br /&gt;
# '''Controller boundaries''' Instructor calibration maps, instructor-review persistence, '''and''' the comparison-report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' are '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''' responsibilities. One '''new student review controller''' covers '''student'''-scoped HTTP: review listing, calibration links, and comparison affordances. Submitted content uses the '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;''' stack.&lt;br /&gt;
# '''Student workflow ''' Students see their work as reviewers through a '''student review''' API: list the '''&amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt;s / reviews''' relevant to them. Where &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; applies, provide a '''link to the calibration comparison''' for instructor versus class scores and enough '''score/feedback''' context that the student can see '''how they compare''' to the instructor on that calibration submission. Teaching-staff map management remains on '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''; the student API lists maps and comparison links and reuses the same report JSON where appropriate.&lt;br /&gt;
&lt;br /&gt;
== Design goals ==&lt;br /&gt;
&lt;br /&gt;
* '''Clarity''' — A reader can follow the instructor path from configuration through '''Begin''' and the instructor calibration review to the comparison report, '''and''' the student path from their review list to calibration comparison where applicable.&lt;br /&gt;
* '''Separation''' — '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''' owns every teaching-staff calibration endpoint: the map list &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;, add participant, '''Begin''', instructor response, map delete, and the comparison-report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;'''. A '''dedicated student review controller''' for student-facing HTTP lists student reviews / maps and exposes calibration comparison entry points, delegating instructor tab behavior and report aggregation to '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''.&lt;br /&gt;
* '''DRY''' — One rubric &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;Answer&amp;lt;/code&amp;gt; persistence path using a '''shared rubric writer''' for instructor calibration and other reviews where applicable. One submitted-content path via '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;''' for list rows and report payloads.&lt;br /&gt;
* '''Minimal footprint''' — Ship what the course feature needs: an instructor path to enter calibration reviews and a student path to compare their calibration work to the instructor’s. Prefer small methods, clear names, focused PRs, and plain comments on the calibration surface.&lt;br /&gt;
* '''Testability''' — Prefer factories and request specs for repeatable scenarios.&lt;br /&gt;
* '''Documentation''' — Controllers are described in prose with routes in the appendix; screenshots pair UI with the backing API.&lt;br /&gt;
&lt;br /&gt;
== Database design ==&lt;br /&gt;
&lt;br /&gt;
Column: &amp;lt;code&amp;gt;response_maps.for_calibration&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Type: boolean, default &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;NOT NULL&amp;lt;/code&amp;gt;.&lt;br /&gt;
* When &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the row is part of the calibration workflow for that assignment: either the instructor-to-calibration-participant map or a student calibrator map that shares the same &amp;lt;code&amp;gt;reviewee_id&amp;lt;/code&amp;gt; as other calibration maps for that participant.&lt;br /&gt;
* When &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;, the row behaves as a normal peer-review map.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Solution approach ==&lt;br /&gt;
&lt;br /&gt;
=== Phase 1 — Calibration participants and response maps ===&lt;br /&gt;
&lt;br /&gt;
Instructors register calibration participants on the assignment and obtain a &amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt; that backs the instructor &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; for that calibration submission. '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''', extended for calibration, validates assignment and teaching-staff role, ensures the target user is an &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt; when required, sets &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; on the map, and returns list rows with display name, submitted artifact summary using the same helpers as '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;''', and review status. Adding a participant by username is idempotent.&lt;br /&gt;
&lt;br /&gt;
=== Phase 2 — Reporting and comparison ===&lt;br /&gt;
&lt;br /&gt;
The comparison-report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' returns the JSON consumed by the Calibration report React page and is implemented on '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''. It returns one payload for the stacked chart and rubric detail views. The server resolves the instructor calibration map, identifies the reviewee as the calibration participant, loads rubric &amp;lt;code&amp;gt;Item&amp;lt;/code&amp;gt;s, loads instructor and student &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;Answer&amp;lt;/code&amp;gt; data for the same reviewed work. Per map, aggregation uses '''submitted''' student responses, taking the latest row by &amp;lt;code&amp;gt;updated_at&amp;lt;/code&amp;gt; among submitted responses, then runs &amp;lt;code&amp;gt;CalibrationPerItemSummary&amp;lt;/code&amp;gt; for per-item buckets.&lt;br /&gt;
&lt;br /&gt;
=== Phase 3 — Instructor calibration review UI ===&lt;br /&gt;
&lt;br /&gt;
The instructor uses the same rubric experience as students: draft and submit, then the submitted rubric is locked for further edits. That matches the course description of a normal-looking rubric review after '''Begin''' and establishes a stable instructor review for downstream comparison reports.&lt;br /&gt;
&lt;br /&gt;
=== High-level architecture ===&lt;br /&gt;
&lt;br /&gt;
All '''teaching-staff''' calibration JSON — the calibration map '''list''', mutating map and review actions including add participant, '''Begin''', instructor response, and delete, '''and''' the comparison-report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' — goes through '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''. Student-facing review HTTP lives in '''one new student review controller''' whose Ruby class and path names follow &amp;lt;code&amp;gt;config/routes.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* '''Review mappings — JSON, teaching staff.''' Extended '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''' serves the Calibration tab: list calibration maps, add a participant and the corresponding &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; map, '''Begin''' metadata, save or submit the instructor calibration review, optional map delete, '''and''' the comparison-report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;'''. Implement the report action as a thin entry point that delegates to POROs or private methods for assembly. &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;Answer&amp;lt;/code&amp;gt; persistence for saves uses the same shared rubric writer as elsewhere where possible.&lt;br /&gt;
&lt;br /&gt;
* '''Report — JSON, teaching staff.''' The report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' is a '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''' action; declare it as a member or collection route in &amp;lt;code&amp;gt;config/routes.rb&amp;lt;/code&amp;gt; next to the action implementation. It returns JSON: rubric items, instructor and student &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; data for the reviewee, &amp;lt;code&amp;gt;per_item_summary&amp;lt;/code&amp;gt;, reviewee hyperlinks/files via '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;'''.&lt;br /&gt;
&lt;br /&gt;
* '''Student reviews — JSON.''' The '''student review controller''' lists '''&amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt;s / reviews''' for the student; for '''&amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt;''' rows it exposes a '''link''' to the teaching-staff comparison report JSON and UI to compare '''student vs instructor''' scores and feedback. Thin layer over existing review models; instructor map listing and report aggregation stay in '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''.&lt;br /&gt;
&lt;br /&gt;
* '''React SPA.''' Assignment editor → Calibration tab → '''Begin''' → instructor rubric page → report page '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;'''.&lt;br /&gt;
&lt;br /&gt;
* '''Server-side helpers.''' &amp;lt;code&amp;gt;CalibrationPerItemSummary&amp;lt;/code&amp;gt; builds per-item chart buckets. Submitted hyperlinks/files always go through '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;'''; details are under '''Implementation''' and '''Backend'''.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== UML diagram ==&lt;br /&gt;
&lt;br /&gt;
[[File:E2609-calibration-domain-class-uml.png|720px|center]]&lt;br /&gt;
&lt;br /&gt;
Instructors and students are &amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt;s. The instructor’s calibration map is a &amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt; with &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; true whose reviewer is the instructor participant and whose reviewee is the calibration participant. Student calibration maps share the same &amp;lt;code&amp;gt;reviewee_id&amp;lt;/code&amp;gt;, the calibration author, but use other &amp;lt;code&amp;gt;reviewer_id&amp;lt;/code&amp;gt; values. &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;Answer&amp;lt;/code&amp;gt; store scores. List and report use '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;''' for hyperlinks/files; &amp;lt;code&amp;gt;CalibrationPerItemSummary&amp;lt;/code&amp;gt; supports report aggregation.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Flow: Review calibration ==&lt;br /&gt;
&lt;br /&gt;
'''Figure 1a — Calibration setup, overview.''' Student lane: review list and submit via the student review stack. Staff lane: Calibration tab and rubric send teaching-staff calibration traffic to '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''. The two lanes can happen in any order; details and routes are in '''Implementation''' and the '''Appendix'''.&lt;br /&gt;
&lt;br /&gt;
[[File:E2609-calibration-pipelines-flow.png|820px|center]]&lt;br /&gt;
&lt;br /&gt;
'''Figure 1b — Calibration report, overview.''' Teaching staff open the report in React; the page issues one &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; to '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''' and receives either &amp;lt;code&amp;gt;403&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;200&amp;lt;/code&amp;gt; report JSON. Assembly of rubric, scores, &amp;lt;code&amp;gt;CalibrationPerItemSummary&amp;lt;/code&amp;gt;, and the &amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt; stack is specified in code and in '''Implementation''' and '''Backend'''; the diagram highlights the browser–Rails exchange.&lt;br /&gt;
&lt;br /&gt;
[[File:E2609-calibration-flow-report.png|720px|center]]&lt;br /&gt;
&lt;br /&gt;
Figure 1b: maps for one calibration run share a &amp;lt;code&amp;gt;reviewee_id&amp;lt;/code&amp;gt;; &amp;lt;code&amp;gt;per_item_summary&amp;lt;/code&amp;gt; aggregates '''submitted''' student &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; rows per map using the latest &amp;lt;code&amp;gt;updated_at&amp;lt;/code&amp;gt; among submitted rows.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Implementation ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Area&lt;br /&gt;
! Implementation &lt;br /&gt;
! Verify&lt;br /&gt;
|-&lt;br /&gt;
| Teaching-staff HTTP&lt;br /&gt;
| Implement list, create, begin, instructor response save, map delete, '''and''' comparison-report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' on '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''', optionally split into a Rails concern mixed into that controller, scoped with &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; and teaching-staff authorization. Add '''one new student review controller''': list student '''&amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt;s / reviews'''; for &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; items, '''link''' to the comparison report and expose '''student vs instructor''' score/feedback comparison. Keep the student controller thin: delegate to existing review models and reuse the same report JSON payload shape where appropriate.&lt;br /&gt;
| Request specs for all &amp;lt;code&amp;gt;review_mappings&amp;lt;/code&amp;gt; calibration actions; request specs for student review list, calibration link, and comparison paths; coverage lives on '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''' for teaching-staff calibration HTTP.&lt;br /&gt;
|-&lt;br /&gt;
| Report &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;&lt;br /&gt;
| Report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' on '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''': thin entry + PORO/private steps; stable JSON for the React report page. Uses &amp;lt;code&amp;gt;CalibrationPerItemSummary&amp;lt;/code&amp;gt; and the '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;''' stack for reviewee hyperlinks/files.&lt;br /&gt;
| Request spec for report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' documents required JSON keys and error cases.&lt;br /&gt;
|-&lt;br /&gt;
| Instructor review write&lt;br /&gt;
| Instructor calibration draft/submit on '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''. Delegate &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt;/&amp;lt;code&amp;gt;Answer&amp;lt;/code&amp;gt; persistence to a '''shared rubric writer'''; action handles guards, validation, and JSON only.&lt;br /&gt;
| Request specs: draft, submit, 422, invalid &amp;lt;code&amp;gt;answers&amp;lt;/code&amp;gt; payload.&lt;br /&gt;
|-&lt;br /&gt;
| Submitted content&lt;br /&gt;
| Build list and report &amp;lt;code&amp;gt;{ hyperlinks, files }&amp;lt;/code&amp;gt; through '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;''' or the same service object that controller already delegates to.&lt;br /&gt;
| Specs assert list and report payloads match submitted-content for the same participant.&lt;br /&gt;
|-&lt;br /&gt;
| Team + participant bootstrap&lt;br /&gt;
| Implement solo-team and related bootstrap logic in a '''team/participant service''' called from map '''create''', with '''&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;''' updates limited to what map create and listing need. Keep &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; hyperlink behavior correct for those paths.&lt;br /&gt;
| Service/unit tests + create path; hyperlink regressions if any.&lt;br /&gt;
|-&lt;br /&gt;
| Scope hygiene&lt;br /&gt;
| Scope edits to calibration controllers, services, routes, and React surfaces named in this document, plus the smallest shared-model touches that map create and listing require.&lt;br /&gt;
| Code review checklist; small, focused diffs.&lt;br /&gt;
|-&lt;br /&gt;
| Report UI&lt;br /&gt;
| &amp;lt;code&amp;gt;CalibrationReview&amp;lt;/code&amp;gt; and related report components match the mockup while consuming the report JSON shape from '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''.&lt;br /&gt;
| Vitest; refresh '''Figure 2''' left pane when the shipped UI changes.&lt;br /&gt;
|-&lt;br /&gt;
| E2E · repo · docs&lt;br /&gt;
| Browser E2E over &amp;lt;code&amp;gt;review_mappings&amp;lt;/code&amp;gt; calibration routes and report tab. Record generated-asset rules for this repo in the PR checklist or team wiki so commits stay consistent.&lt;br /&gt;
| CI or &amp;lt;code&amp;gt;test:e2e&amp;lt;/code&amp;gt;; PR checklist.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Backend — controller behavior ==&lt;br /&gt;
&lt;br /&gt;
Teaching-staff APIs use auth, '''&amp;lt;code&amp;gt;authorize&amp;lt;/code&amp;gt;''', and staff checks consistent with the assignment. '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''', extended for calibration, hosts all teaching-staff calibration endpoints: it lists calibration maps where the current user is the instructor reviewer and &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; is true; each row includes map/reviewee fields, '''&amp;lt;code&amp;gt;participant_name&amp;lt;/code&amp;gt;''', '''&amp;lt;code&amp;gt;review_status&amp;lt;/code&amp;gt;''' from the latest '''&amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt;''', and '''&amp;lt;code&amp;gt;submitted_content&amp;lt;/code&amp;gt;''' built with the '''same service/helpers as &amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;'''. '''Create''' resolves username to '''&amp;lt;code&amp;gt;AssignmentParticipant&amp;lt;/code&amp;gt;''', runs team bootstrap via a '''dedicated service''', '''&amp;lt;code&amp;gt;find_or_create_by!&amp;lt;/code&amp;gt;''' the instructor→reviewee map with &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; true, returns '''&amp;lt;code&amp;gt;201&amp;lt;/code&amp;gt;'''. '''Begin''' returns '''&amp;lt;code&amp;gt;map_id&amp;lt;/code&amp;gt;''', optional '''&amp;lt;code&amp;gt;response_id&amp;lt;/code&amp;gt;''', '''&amp;lt;code&amp;gt;redirect_to&amp;lt;/code&amp;gt;'''. '''Instructor response save''' validates input, enforces the submitted-state rules, delegates persistence to a '''shared writer''', returns '''&amp;lt;code&amp;gt;200&amp;lt;/code&amp;gt;''' JSON. '''Destroy''' returns '''&amp;lt;code&amp;gt;204&amp;lt;/code&amp;gt;''' after ownership checks. '''Comparison-report &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' for the instructor’s calibration map id on the same controller returns rubric items, '''&amp;lt;code&amp;gt;instructor_response&amp;lt;/code&amp;gt;''', submitted student '''&amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt;s''', '''&amp;lt;code&amp;gt;CalibrationPerItemSummary.build&amp;lt;/code&amp;gt;''', reviewee hyperlinks/files via the '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt; stack''', '''&amp;lt;code&amp;gt;200&amp;lt;/code&amp;gt;''' JSON, or '''&amp;lt;code&amp;gt;404&amp;lt;/code&amp;gt;''' / '''&amp;lt;code&amp;gt;422&amp;lt;/code&amp;gt;''' when assignment, map, or rubric preconditions fail.&lt;br /&gt;
&lt;br /&gt;
'''Student review controller — new, thin layer''' — Lists '''&amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt;s / reviews''' for the logged-in student as reviewer; for '''&amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt;''' maps, provides a '''link''' to the calibration comparison using the instructor-facing report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' or a student-safe JSON projection, plus fields and UI so the student can see '''how their scores and feedback compare''' to the instructor’s. Delegates to existing review / &amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt; models; instructor Calibration tab CRUD, rubric save, and server-side report aggregation live in '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Appendix — API routes ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Method&lt;br /&gt;
! Path pattern&lt;br /&gt;
! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;/assignments/:assignment_id/review_mappings/…&amp;lt;/code&amp;gt;&lt;br /&gt;
| '''Calibration list.''' List calibration &amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt; rows for teaching staff; exact path under &amp;lt;code&amp;gt;review_mappings&amp;lt;/code&amp;gt; is defined in &amp;lt;code&amp;gt;config/routes.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;/assignments/:assignment_id/review_mappings/…&amp;lt;/code&amp;gt;&lt;br /&gt;
| '''Calibration create.''' Add calibration participant; request includes &amp;lt;code&amp;gt;username&amp;lt;/code&amp;gt;. Exact path in &amp;lt;code&amp;gt;config/routes.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;DELETE&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;/assignments/:assignment_id/review_mappings/…/:id&amp;lt;/code&amp;gt;&lt;br /&gt;
| '''Calibration map delete.''' Remove one calibration map by id; exact path in &amp;lt;code&amp;gt;config/routes.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;/assignments/:assignment_id/review_mappings/…/:id/…&amp;lt;/code&amp;gt;&lt;br /&gt;
| '''Begin.''' Return navigation JSON for instructor calibration rubric SPA; exact member path in &amp;lt;code&amp;gt;config/routes.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;/assignments/:assignment_id/review_mappings/…/:id/…&amp;lt;/code&amp;gt;&lt;br /&gt;
| '''Instructor response.''' Save draft or submit instructor calibration review; exact member path in &amp;lt;code&amp;gt;config/routes.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;/assignments/:assignment_id/review_mappings/…&amp;lt;/code&amp;gt;&lt;br /&gt;
| '''Calibration report.''' Return comparison report JSON for the instructor calibration map id; exact path in &amp;lt;code&amp;gt;config/routes.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;/assignments/:assignment_id/…&amp;lt;/code&amp;gt;&lt;br /&gt;
| '''Student review controller.''' Student review list, calibration report links, and comparison to instructor; verbs and paths declared in &amp;lt;code&amp;gt;config/routes.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Frontend — data expectations and user flow ==&lt;br /&gt;
&lt;br /&gt;
'''Calibration tab table:''' Each row shows map id, display name, submitted links and files summary, and status: '''Not started''', '''Draft''', or '''Submitted'''. Data comes from the teaching-staff calibration '''list''' &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; on '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''' under &amp;lt;code&amp;gt;review_mappings&amp;lt;/code&amp;gt; routes, with submitted fields from the same pipeline as '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;'''.&lt;br /&gt;
&lt;br /&gt;
'''Report page:''' Rubric items in order, instructor scores and comments per item, each student’s scores, and per-item buckets from the report JSON.&lt;br /&gt;
&lt;br /&gt;
'''User flow:'''&lt;br /&gt;
&lt;br /&gt;
# Open Assignment editor, then Calibration tab.&lt;br /&gt;
# Enter username; system ensures participant and &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; map.&lt;br /&gt;
# &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; calibration list on &amp;lt;code&amp;gt;review_mappings&amp;lt;/code&amp;gt;; table refreshes.&lt;br /&gt;
# Begin; &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; begin; SPA opens the instructor calibration review page.&lt;br /&gt;
# Save draft or submit; &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; instructor response on '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''.&lt;br /&gt;
# Open report; &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; comparison JSON from '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''; charts and rubric detail render.&lt;br /&gt;
&lt;br /&gt;
'''Student review list: ''' The student review controller lists the student’s reviews / '''&amp;lt;code&amp;gt;ResponseMap&amp;lt;/code&amp;gt;s'''. Calibration rows show a '''link''' to open the comparison for instructor versus class scores and, where the UI implements it, the student’s position relative to the instructor.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Assignment editor — Calibration tab ===&lt;br /&gt;
&lt;br /&gt;
[[File:E2609-calibration-assignment-tab.png|480px|center]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;text-align:center; margin-top:0.2em;&amp;quot;&amp;gt;'''Figure 1.''' Reference UI — assignment editor Calibration tab: add participants, open report or '''Begin''' the instructor calibration review, and see submitted items. Each row is filled from the calibration '''list''' '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' on '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''' under &amp;lt;code&amp;gt;review_mappings&amp;lt;/code&amp;gt;, with &amp;lt;code&amp;gt;participant_name&amp;lt;/code&amp;gt;, submitted content from the '''&amp;lt;code&amp;gt;SubmittedContentController&amp;lt;/code&amp;gt;''' pipeline, and &amp;lt;code&amp;gt;review_status&amp;lt;/code&amp;gt;. Adding the same username again is idempotent.&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Calibration report: comparison UI ===&lt;br /&gt;
&lt;br /&gt;
{| style=&amp;quot;width:100%; max-width:980px; margin:0.8em auto; border-collapse:separate; border-spacing:10px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;vertical-align:top; text-align:center; width:50%;&amp;quot; | '''Reference UI — Class comparison, stacked chart tab'''&lt;br /&gt;
| style=&amp;quot;vertical-align:top; text-align:center; width:50%;&amp;quot; | '''Mockup chart layout; same report &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; JSON from &amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''&lt;br /&gt;
|-&lt;br /&gt;
| style=&amp;quot;vertical-align:top; text-align:center;&amp;quot; | [[File:E2609-calibration-report-stacked.png|440px]]&lt;br /&gt;
| style=&amp;quot;vertical-align:top; text-align:center;&amp;quot; | [[File:E2609-calibration-stacked-chart-improved-mockup.png|440px]]&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;text-align:center; margin-top:0.35em;&amp;quot;&amp;gt;'''Figure 2.''' Side by side: reference stacked-chart tab versus the intended layout. The mockup adds per-criterion axis labels, framing, legend, and full criterion text in tooltips in &amp;lt;code&amp;gt;CalibrationReview&amp;lt;/code&amp;gt;. Same report JSON from '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''.&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[File:E2609-calibration-report-rubric-detail.png|480px|center]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div style=&amp;quot;text-align:center; margin-top:0.2em;&amp;quot;&amp;gt;'''Figure 3.''' Reference UI — rubric detail: instructor review versus class summary and reviewer selections, using the comparison-report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;''' JSON from '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;'''.&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Testing and Verification ==&lt;br /&gt;
&lt;br /&gt;
=== Test Plan ===&lt;br /&gt;
&lt;br /&gt;
==== Automated Testing — RSpec ====&lt;br /&gt;
&lt;br /&gt;
* '''Request specs''' for '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''' calibration: list, create participant/map, begin, instructor response for draft, submit, and error paths, destroy, and comparison-report '''&amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt;'''. Co-locate spec files under &amp;lt;code&amp;gt;spec/requests/&amp;lt;/code&amp;gt; with the route names in &amp;lt;code&amp;gt;config/routes.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
* '''Request specs''' for the '''student review controller''': list reviews/maps; '''&amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt;''' rows include correct report link or comparison payload; responses match the logged-in student reviewer.&lt;br /&gt;
* Service specs for aggregation logic, for example &amp;lt;code&amp;gt;spec/services/calibration_per_item_summary_spec.rb&amp;lt;/code&amp;gt; next to &amp;lt;code&amp;gt;CalibrationPerItemSummary&amp;lt;/code&amp;gt;.&lt;br /&gt;
* '''Factories / fixtures''' for &amp;lt;code&amp;gt;for_calibration&amp;lt;/code&amp;gt; maps, participants, and submitted responses as needed for the above.&lt;br /&gt;
&lt;br /&gt;
==== Frontend Testing — Vitest ====&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;src/pages/Assignments/__tests__/CalibrationReview.test.tsx&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;src/pages/Assignments/__tests__/CalibrationInstructorReview.test.tsx&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;src/pages/Assignments/__tests__/calibrationReportNormalize.test.ts&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;src/pages/Assignments/__tests__/calibrationSummary.test.ts&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;src/pages/Assignments/__tests__/calibrationReportDemo.test.ts&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== End-to-end — E2E ====&lt;br /&gt;
&lt;br /&gt;
# Teaching staff: assignment editor → Calibration tab → calibration map list &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; on &amp;lt;code&amp;gt;review_mappings&amp;lt;/code&amp;gt; → table or empty state renders cleanly in the browser.&lt;br /&gt;
# Teaching staff: comparison report &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; on '''&amp;lt;code&amp;gt;ReviewMappingsController&amp;lt;/code&amp;gt;''' → “Class comparison (stacked)” and “Rubric detail” render; chart region populated when &amp;lt;code&amp;gt;per_item_summary&amp;lt;/code&amp;gt; is non-empty.&lt;br /&gt;
# Teaching staff: '''Begin''', including opening the calibration review by direct URL if supported → draft save → submit → UI matches post-submit lock rules.&lt;br /&gt;
# Student: open student review list → calibration row shows '''link''' to comparison for instructor versus class view → student can see '''how their review compares''' to the instructor’s when that panel is implemented.&lt;br /&gt;
&lt;br /&gt;
==== Manual UI Testing ====&lt;br /&gt;
&lt;br /&gt;
# Login as instructor/TA.&lt;br /&gt;
# Open assignment editor → Calibration tab.&lt;br /&gt;
# Add a calibration participant by username.&lt;br /&gt;
# Click '''View report''' and verify stacked and rubric detail tabs.&lt;br /&gt;
# Enter/edit instructor review: save draft, submit, and confirm the form locks.&lt;br /&gt;
# Login as student calibrator: open student review list; for a calibration map, follow the report/comparison link and confirm scores or feedback align with expectations versus the instructor view.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Future Scope ==&lt;br /&gt;
&lt;br /&gt;
* Add an overall calibration score and trend visualization per participant.&lt;br /&gt;
* Provide additional endpoints for:&lt;br /&gt;
** calibration summary per map,&lt;br /&gt;
** detailed agreement breakdown per item across all rounds.&lt;br /&gt;
* Improve diagnostics, such as highlighting criteria with the largest score spread across reviewers.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
&lt;br /&gt;
* [https://wiki.expertiza.ncsu.edu/index.php?title=Main_Page Expertiza wiki main page]&lt;br /&gt;
* Front-end pull request: [https://github.com/expertiza/reimplementation-front-end/pull/164 expertiza/reimplementation-front-end#164]&lt;br /&gt;
* Back-end pull request: [https://github.com/expertiza/reimplementation-back-end/pull/323 expertiza/reimplementation-back-end#323]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Team ==&lt;br /&gt;
&lt;br /&gt;
* Mentor: Dr. Ed Gehringer&lt;br /&gt;
* Team members: Xiangjun Mi, Rujuta Palimkar, Emma Hassler&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2618._Support_OIDC_Logins&amp;diff=167958</id>
		<title>CSC/ECE 517 Spring 2026 - E2618. Support OIDC Logins</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2618._Support_OIDC_Logins&amp;diff=167958"/>
		<updated>2026-04-14T21:45:09Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Purpose */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Purpose ==&lt;br /&gt;
Expertiza currently authenticates users with its own login page, implemented by the Expertiza application.  Expertiza has been used at many campuses, however, and each has their own SSO (single signon) protocol that students and staff use to log into other applications. It is more secure for applications to use the standard approach at sites where they are in use, and it also frees Expertiza from managing passwords, and thus removes the risk of compromise. By integrating [https://openid.net/developers/how-connect-works/ OIDC] login, users can authenticate using their existing university credentials, providing a familiar and streamlined login experience. Traditional username and password login will continue to be supported alongside OIDC, allowing users to choose their preferred authentication method.&lt;br /&gt;
&lt;br /&gt;
== Requirements ==&lt;br /&gt;
=== Authentication Flow ===&lt;br /&gt;
Users select a provider from a dropdown on the login page that redirects them to the school's OIDC provider, authenticates them, and redirects back to the application with a valid session. The frontend fetches available providers from the backend via &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; and renders them dynamically in a dropdown. On provider selection, the frontend posts to the backend, which returns an authorization URL. After the user completes the OIDC flow, the IdP redirects back to the frontend callback, which posts the authorization code and state to the backend to complete login.&lt;br /&gt;
&lt;br /&gt;
=== Session Management ===&lt;br /&gt;
Issue and maintain a local application session (JWT) after successful OIDC authentication, using the same &amp;lt;code&amp;gt;JsonWebToken&amp;lt;/code&amp;gt; class and payload structure as the existing password login. Refresh token grant flow will not be considered at this time (since session is managed by the application).&lt;br /&gt;
&lt;br /&gt;
=== Account Linking ===&lt;br /&gt;
Match the authenticated user's email from the ID token to an existing local account. No dedicated account linking table or just-in-time account creation will be built at this time. If no matching local account is found, an error is returned.&lt;br /&gt;
&lt;br /&gt;
=== Configuration ===&lt;br /&gt;
OIDC provider configurations (display name, scopes, endpoints) are defined in a YAML config file (&amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt;). Client credentials (client ID, client secret) are stored in environment variables and injected via ERB. Providers that support OIDC discovery have their endpoints and JWKS keys fetched automatically. The system supports multiple OIDC provider configurations simultaneously. The configuration is validated at boot via an initializer, surfacing missing values immediately on deploy rather than at login time.&lt;br /&gt;
&lt;br /&gt;
=== State Management ===&lt;br /&gt;
OIDC state, nonce, and PKCE code verifier are stored server-side in an &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; database table (via ActiveRecord) rather than in session cookies. This avoids cross-origin cookie issues between the separate frontend and backend. Rows are expired after 5 minutes and deleted after use. This table is named generically to support future federated auth protocols such as SAML. Note that many OIDC libraries (including &amp;lt;code&amp;gt;omniauth_openid_connect&amp;lt;/code&amp;gt;) use cookies to track state; due to SameSite restrictions on cross-origin requests, this approach leads to instability with a separated frontend and backend and should be avoided.&lt;br /&gt;
&lt;br /&gt;
=== Logout ===&lt;br /&gt;
Logout will not be impacted.&lt;br /&gt;
&lt;br /&gt;
=== Error Handling ===&lt;br /&gt;
Gracefully handle failures such as no local account matching the authenticated email, expired or invalid state parameters, token exchange errors, ID token verification errors, and user-denied consent at the IdP.&lt;br /&gt;
&lt;br /&gt;
=== Security ===&lt;br /&gt;
Use the Authorization Code flow with the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; Ruby gem (by nov). Validate the ID token signature and claims via JWKS keys from the provider's discovery document. Enforce a &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; parameter to prevent CSRF and a &amp;lt;code&amp;gt;nonce&amp;lt;/code&amp;gt; to prevent replay attacks. PKCE (code verifier and code challenge) is always included in the authorization request and token exchange; providers that support it will enforce it, and providers that do not will ignore the extra parameters. The backend is a confidential client and always authenticates with both client secret and PKCE.&lt;br /&gt;
&lt;br /&gt;
=== Testing ===&lt;br /&gt;
Backend and frontend are tested independently. Backend request specs use WebMock to stub the identity provider's discovery, token, and JWKS endpoints, allowing the full controller logic (state management, token exchange, ID token verification, user matching) to be tested without external dependencies. Frontend component tests mock axios calls to verify rendering, dropdown behavior, callback handling, and error display. End-to-end testing across both systems with a live identity provider is not planned at this time, as it would require standing up a mock IdP server (e.g. Keycloak or mock-oauth2-server), which is beyond the scope of the existing test infrastructure. The full OIDC login flow will be manually verified against Google's OIDC provider in a local development environment and demonstrated as needed.&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
[[File:OIDC Provider-2026-04-06-223511.png|1000px]]&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
* '''Boot (Step 0):''' Load provider configurations from &amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt; with secrets injected from environment variables via ERB. Each provider entry defines a display name, scopes, issuer, client credentials, and redirect URI. The &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; class validates that all required keys are present at boot via &amp;lt;code&amp;gt;config/initializers/oidc.rb&amp;lt;/code&amp;gt;. For providers with &amp;lt;code&amp;gt;discovery: true&amp;lt;/code&amp;gt;, the &amp;lt;code&amp;gt;.well-known/openid-configuration&amp;lt;/code&amp;gt; document is fetched using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem to resolve the authorization endpoint, token endpoint, userinfo endpoint, and JWKS keys. Discovery results are not aggressively cached to allow for key rotation; on signature verification failure, keys are re-fetched and verification is retried once.&lt;br /&gt;
* '''Provider List (Step 1):''' Expose a &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; endpoint that returns a JSON array of &amp;lt;code&amp;gt;{ id, name }&amp;lt;/code&amp;gt; from &amp;lt;code&amp;gt;OidcConfig.public_list&amp;lt;/code&amp;gt;. No secrets or endpoint details are included in this response.&lt;br /&gt;
* '''Client Select (Step 2):''' Expose a &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; endpoint that accepts a provider id. Generate a cryptographically random state and nonce via &amp;lt;code&amp;gt;SecureRandom.hex(32)&amp;lt;/code&amp;gt;, and a PKCE code verifier via &amp;lt;code&amp;gt;SecureRandom.urlsafe_base64(64)&amp;lt;/code&amp;gt; with a SHA256 code challenge. Insert a row into the &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; table containing the state, nonce, code verifier, provider id, and creation timestamp. Construct the authorization URL using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem's &amp;lt;code&amp;gt;authorization_uri&amp;lt;/code&amp;gt; method and return it to the frontend.&lt;br /&gt;
* '''Callback (Step 4):''' Expose a &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt; endpoint (and a temporary &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; for direct IdP redirect during backend-only testing) that accepts the authorization code and state. Look up the matching &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; row by state, rejecting the request if no row is found or if the row is older than 5 minutes. Delete the row to prevent reuse. Using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem, exchange the authorization code for tokens via &amp;lt;code&amp;gt;access_token!&amp;lt;/code&amp;gt; with the stored code verifier. Decode the ID token using &amp;lt;code&amp;gt;OpenIDConnect::ResponseObject::IdToken.decode&amp;lt;/code&amp;gt; against the provider's JWKS keys, and verify the issuer, client_id, and nonce via &amp;lt;code&amp;gt;id_token.verify!&amp;lt;/code&amp;gt;. Extract the user's email from the ID token claims and look up a matching local user. If a match is found, issue a session JWT using the same &amp;lt;code&amp;gt;JsonWebToken.encode&amp;lt;/code&amp;gt; method and payload structure as the existing &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; action. If no match is found, return a 404 error indicating no local account exists for that email.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
* '''Login Page (Step 1):''' On page load, the &amp;lt;code&amp;gt;OidcLogin&amp;lt;/code&amp;gt; component calls &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; and renders a dropdown (&amp;lt;code&amp;gt;Form.Select&amp;lt;/code&amp;gt;) for each configured provider below the existing username and password form. If the request fails or returns empty, the component renders nothing and the standard login form remains available and unaffected. No loading state is shown to avoid visual disruption when no providers are configured.&lt;br /&gt;
* '''Initiate Login (Step 2):''' When the user selects a provider from the dropdown, &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;/auth/client-select&amp;lt;/code&amp;gt; with the selected provider id. On success, redirect the browser to the returned authorization URL via &amp;lt;code&amp;gt;window.location.href&amp;lt;/code&amp;gt;. The user then authenticates with the identity provider and is redirected back to the frontend callback route.&lt;br /&gt;
* '''Callback (Step 4):''' The &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt; page component handles the redirect back from the identity provider at &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt;. It extracts the authorization code and state from the query parameters and &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt;s them to &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt;. If the query parameters contain an &amp;lt;code&amp;gt;error&amp;lt;/code&amp;gt; parameter instead of a code (e.g. the user denied consent), the error is displayed without calling the backend and the user is redirected to the login page.&lt;br /&gt;
* '''Login Complete (Step 5):''' On a successful callback response, store the session JWT via &amp;lt;code&amp;gt;setAuthToken&amp;lt;/code&amp;gt;, update the Redux auth state via &amp;lt;code&amp;gt;authenticationActions.setAuthentication&amp;lt;/code&amp;gt;, persist the session to localStorage, and redirect the user to the dashboard. This mirrors the existing password login flow exactly. On failure, display an error alert and redirect to the login page.&lt;br /&gt;
* The existing username and password login flow remains unchanged and fully functional.&lt;br /&gt;
&lt;br /&gt;
=== Design Patterns ===&lt;br /&gt;
The implementation uses the '''Strategy pattern''' for provider configuration. Each OIDC provider is defined declaratively in YAML with its own credentials, scopes, and endpoints, while the controller logic remains provider-agnostic. Adding a new identity provider requires only a new configuration block and environment variables, with no code changes.&lt;br /&gt;
&lt;br /&gt;
=== Schema ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; table stores temporary OIDC login state. Each row represents a single in-progress login attempt and is deleted after use or expiry.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Column !! Type !! Constraints !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt; || bigint || primary key || Row identifier&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; || string || unique, indexed || CSRF protection; used to look up the request on callback&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;nonce&amp;lt;/code&amp;gt; || string || not null || Replay attack prevention; verified against the ID token claim&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;code_verifier&amp;lt;/code&amp;gt; || string || not null || PKCE secret; sent to the token endpoint to prove the same party initiated the flow&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;provider&amp;lt;/code&amp;gt; || string || not null || Which OIDC provider config to use on callback&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;created_at&amp;lt;/code&amp;gt; || datetime || not null || Used to expire rows older than 5 minutes&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
No foreign keys or associations to other tables. This table is intentionally generic to support future federated auth protocols such as SAML.&lt;br /&gt;
&lt;br /&gt;
== Library Choice ==&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem (by nov, [https://github.com/nov/openid_connect github.com/nov/openid_connect]) was chosen over &amp;lt;code&amp;gt;omniauth_openid_connect&amp;lt;/code&amp;gt; for the following reasons:&lt;br /&gt;
&lt;br /&gt;
* '''No cookie/session dependency:''' &amp;lt;code&amp;gt;omniauth_openid_connect&amp;lt;/code&amp;gt; stores state and nonce in the server-side session via cookies. With a separate frontend and backend on different origins, session cookies are not reliably shared due to SameSite restrictions. Using &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; directly allows state management via the database instead.&lt;br /&gt;
* '''Explicit control:''' The gem provides building blocks (discovery, client construction, token exchange, ID token verification) without middleware magic. Each step in the OIDC flow is visible in the controller code.&lt;br /&gt;
* '''Lightweight:''' No OmniAuth middleware stack or Rack integration required. The gem handles the protocol; the application handles routing and state.&lt;br /&gt;
* '''Actively maintained:''' The gem is OpenID Foundation certified and used by 2,700+ projects on GitHub.&lt;br /&gt;
&lt;br /&gt;
The tradeoff is approximately 10 additional lines of code for state management (generating and storing state/nonce/PKCE in the &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; table), which is minimal compared to the complexity of debugging cross-origin cookie issues.&lt;br /&gt;
&lt;br /&gt;
== File Diffs and Additions (Prototype and subject to change) ==&lt;br /&gt;
&lt;br /&gt;
=== Backend (Rails) ===&lt;br /&gt;
([https://github.com/johnmweisz/reimplementation-back-end/tree/2618-oidc-login branch: 2618-oidc-login])&lt;br /&gt;
&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/app/controllers/oidc_login_controller.rb app/controllers/oidc_login_controller.rb]  — Controller with providers, client_select, and callback actions&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/app/models/oidc_config.rb app/models/oidc_config.rb]                 — YAML config loader with validation&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/app/models/auth_request.rb app/models/auth_request.rb]                — ActiveRecord model for state/nonce/PKCE storage&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/config/oidc_providers.yml config/oidc_providers.yml]                 — Provider configuration (ERB for env var injection)&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/config/initializers/oidc.rb config/initializers/oidc.rb]               — Boot-time config validation&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/db/migrate/20260407003623_create_auth_requests.rb db/migrate/*_create_auth_requests.rb]      — Migration for auth_requests table&lt;br /&gt;
&lt;br /&gt;
=== Backend (RSpec) Tests ===&lt;br /&gt;
([https://github.com/johnmweisz/reimplementation-back-end/tree/2618-oidc-login branch: 2618-oidc-login])&lt;br /&gt;
&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/models/oidc_config_spec.rb spec/models/oidc_config_spec.rb]          — Config loading, validation, missing keys, public_list secrets exclusion, provider lookup&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/models/auth_request_spec.rb spec/models/auth_request_spec.rb]         — State uniqueness, expiry scope, cleanup of stale rows&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/requests/oidc_login_spec.rb spec/requests/oidc_login_spec.rb]         — Endpoint tests covering:&lt;br /&gt;
&lt;br /&gt;
'''GET /auth/providers'''&lt;br /&gt;
* Returns provider list with id and name only, no secrets leaked&lt;br /&gt;
* '''TODO''' Returns empty array when no providers configured&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/client-select'''&lt;br /&gt;
* '''TODO''' Returns authorization URL with expected query parameters (client_id, redirect_uri, scope, state, nonce, code_challenge)&lt;br /&gt;
* '''TODO''' Creates an auth_requests row&lt;br /&gt;
* '''TODO''' Returns 404 for unknown provider&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — Happy Path'''&lt;br /&gt;
* '''TODO''' Exchanges valid code and state for a session JWT with same payload structure as password login&lt;br /&gt;
* '''TODO''' Deletes the auth_requests row after successful use&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — CSRF Protection (State Validation)'''&lt;br /&gt;
* '''TODO''' Rejects unknown state (422)&lt;br /&gt;
* '''TODO''' Rejects expired state older than 5 minutes (422)&lt;br /&gt;
* '''TODO''' Rejects already-consumed state / replay (422)&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — Replay Protection (Nonce Validation)'''&lt;br /&gt;
* '''TODO''' Rejects ID token with mismatched nonce&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — Token Verification'''&lt;br /&gt;
* '''TODO''' Rejects ID token with invalid signature&lt;br /&gt;
* '''TODO''' Rejects ID token with mismatched issuer&lt;br /&gt;
* '''TODO''' Rejects ID token with mismatched audience (client_id)&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — User Matching'''&lt;br /&gt;
* '''TODO''' Returns 404 when no local user matches the email&lt;br /&gt;
* '''TODO''' Matches correct user when multiple users exist&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — PKCE'''&lt;br /&gt;
* '''TODO''' Sends code_verifier to the token endpoint during code exchange&lt;br /&gt;
&lt;br /&gt;
=== Frontend (React) ===&lt;br /&gt;
([https://github.com/johnmweisz/reimplementation-front-end/tree/2618-oidc-login branch: 2618-oidc-login])&lt;br /&gt;
&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/components/OidcLogin/OidcLogin.tsx src/components/OidcLogin/OidcLogin.tsx]     — Provider dropdown component&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/pages/OidcCallback/OidcCallback.tsx src/pages/OidcCallback/OidcCallback.tsx]    — Callback page handling code exchange&lt;br /&gt;
&lt;br /&gt;
=== Frontend (Vitest) Tests ===&lt;br /&gt;
([https://github.com/johnmweisz/reimplementation-front-end/tree/2618-oidc-login branch: 2618-oidc-login])&lt;br /&gt;
&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/components/OidcLogin/OidcLogin.test.tsx src/components/OidcLogin/OidcLogin.test.tsx]     — Provider dropdown component tests&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/pages/OidcCallback/OidcCallback.test.tsx src/pages/OidcCallback/OidcCallback.test.tsx]    — Callback page tests&lt;br /&gt;
&lt;br /&gt;
'''OidcLogin Component'''&lt;br /&gt;
* '''TODO''' Renders dropdown with providers when GET /auth/providers returns a non-empty list&lt;br /&gt;
* '''TODO''' Renders nothing when GET /auth/providers returns an empty array&lt;br /&gt;
* '''TODO''' Renders nothing when GET /auth/providers fails&lt;br /&gt;
* '''TODO''' Calls POST /auth/client-select with selected provider id on dropdown change&lt;br /&gt;
* '''TODO''' Redirects browser to returned authorization URL on successful client-select&lt;br /&gt;
* '''TODO''' Does not redirect when client-select fails&lt;br /&gt;
* '''TODO''' Existing login form remains visible and functional alongside the dropdown&lt;br /&gt;
&lt;br /&gt;
'''OidcCallback Component'''&lt;br /&gt;
* '''TODO''' Posts code and state to POST /auth/callback on mount&lt;br /&gt;
* '''TODO''' Stores session JWT and dispatches auth state on success&lt;br /&gt;
* '''TODO''' Redirects to dashboard on successful login&lt;br /&gt;
* '''TODO''' Displays error alert and redirects to login on backend failure (e.g. no matching account)&lt;br /&gt;
* '''TODO''' Displays error and redirects to login when IdP returns an error parameter (e.g. user denied consent) without calling the backend&lt;br /&gt;
* '''TODO''' Redirects to login when code or state query parameters are missing&lt;br /&gt;
* '''TODO''' Shows &amp;quot;Completing login...&amp;quot; message while request is in flight&lt;br /&gt;
&lt;br /&gt;
=== Routes ===&lt;br /&gt;
 GET  /auth/providers      → oidc_login#providers&lt;br /&gt;
 POST /auth/client-select  → oidc_login#client_select&lt;br /&gt;
 POST /auth/callback       → oidc_login#callback&lt;br /&gt;
 GET  /auth/callback       → React OidcCallback component&lt;br /&gt;
&lt;br /&gt;
== Development Stories (Planning) ==&lt;br /&gt;
&lt;br /&gt;
=== Story 1: Backend — OIDC Provider Configuration ===&lt;br /&gt;
'''As a''' developer, '''I want''' provider configurations loaded from a YAML file at boot, '''so that''' new OIDC providers can be added without code changes.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Create &amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt; with ERB support for injecting secrets from environment variables.&lt;br /&gt;
* Create an &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; class that loads and validates the YAML at boot, exposing methods to list providers and look up a provider by id.&lt;br /&gt;
* Define the config file path as a constant (&amp;lt;code&amp;gt;CONFIG_FILE&amp;lt;/code&amp;gt;) for clarity.&lt;br /&gt;
* Validate required keys: &amp;lt;code&amp;gt;display_name&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;issuer&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;client_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;client_secret&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;redirect_uri&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;scopes&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Raise a clear boot-time error if required values are missing via &amp;lt;code&amp;gt;config/initializers/oidc.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Add unit tests for config loading, validation, and missing key detection.&lt;br /&gt;
&lt;br /&gt;
=== Story 2: Backend — Auth Requests Table ===&lt;br /&gt;
'''As a''' developer, '''I want''' a database-backed store for OIDC state, nonce, and PKCE values, '''so that''' the backend can validate callbacks without relying on cookies.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Generate an ActiveRecord migration for &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; with columns: &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; (string, indexed, unique), &amp;lt;code&amp;gt;nonce&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;code_verifier&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;provider&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;created_at&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Create the &amp;lt;code&amp;gt;AuthRequest&amp;lt;/code&amp;gt; model with a uniqueness validation on state, an &amp;lt;code&amp;gt;expired&amp;lt;/code&amp;gt; scope, and a &amp;lt;code&amp;gt;cleanup_expired&amp;lt;/code&amp;gt; class method.&lt;br /&gt;
* Add a rake task or scheduled job to periodically clean up expired rows.&lt;br /&gt;
* Add unit tests for creation, lookup, expiry, and deletion.&lt;br /&gt;
&lt;br /&gt;
=== Story 3: Backend — Provider List Endpoint ===&lt;br /&gt;
'''As a''' frontend developer, '''I want''' a &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; endpoint, '''so that''' the login page can dynamically render provider options.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Create a controller action that returns a JSON array of &amp;lt;code&amp;gt;{ id, name }&amp;lt;/code&amp;gt; from &amp;lt;code&amp;gt;OidcConfig.public_list&amp;lt;/code&amp;gt;.&lt;br /&gt;
* No secrets or endpoint URLs are included in the response.&lt;br /&gt;
* Add a request spec covering the response format and a case with multiple providers.&lt;br /&gt;
&lt;br /&gt;
=== Story 4: Backend — Client Select Endpoint ===&lt;br /&gt;
'''As a''' frontend developer, '''I want''' a &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; endpoint that returns an authorization URL, '''so that''' the frontend can redirect the user to the identity provider.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Accept a &amp;lt;code&amp;gt;provider&amp;lt;/code&amp;gt; param and look up the provider config.&lt;br /&gt;
* Generate cryptographically random state, nonce, and PKCE code verifier/challenge.&lt;br /&gt;
* Insert a row into &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Construct and return the authorization URL with client_id, redirect_uri, scopes, state, nonce, and code_challenge.&lt;br /&gt;
* Return a 404 if the provider is unknown.&lt;br /&gt;
* Add request specs covering the happy path, unknown provider, and that the auth_requests row is created.&lt;br /&gt;
&lt;br /&gt;
=== Story 5: Backend — Callback Endpoint ===&lt;br /&gt;
'''As a''' frontend developer, '''I want''' a &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt; endpoint that exchanges the authorization code for tokens and returns a session, '''so that''' the user is logged in after completing the OIDC flow.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Accept &amp;lt;code&amp;gt;code&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; params.&lt;br /&gt;
* Look up and delete the matching &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; row. Reject if not found or expired.&lt;br /&gt;
* Exchange the code for tokens using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem with the stored code_verifier.&lt;br /&gt;
* Verify the ID token signature (JWKS), issuer, client_id, and nonce.&lt;br /&gt;
* Extract the email claim and match to an existing local user.&lt;br /&gt;
* On match: issue a session JWT using the same payload structure as &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; and return it with the user profile.&lt;br /&gt;
* On no match: return a 404 error indicating no local account was found.&lt;br /&gt;
* Handle &amp;lt;code&amp;gt;ActiveRecord::RecordNotFound&amp;lt;/code&amp;gt; (invalid/expired state) and &amp;lt;code&amp;gt;InvalidToken&amp;lt;/code&amp;gt; (verification failure) exceptions.&lt;br /&gt;
* Add request specs covering the happy path, expired state, invalid state, and no matching user.&lt;br /&gt;
&lt;br /&gt;
=== Story 6: Frontend — Provider Dropdown on Login Page ===&lt;br /&gt;
'''As a''' user, '''I want''' to see a provider dropdown on the login page, '''so that''' I can authenticate with my school credentials.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Create an &amp;lt;code&amp;gt;OidcLogin&amp;lt;/code&amp;gt; component that calls &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; on mount.&lt;br /&gt;
* Render a &amp;lt;code&amp;gt;Form.Select&amp;lt;/code&amp;gt; dropdown with a disabled &amp;quot;Sign in with...&amp;quot; default option.&lt;br /&gt;
* If the request fails or returns empty, render nothing (no error, no placeholder).&lt;br /&gt;
* Existing login form remains unchanged and fully functional.&lt;br /&gt;
* Add component tests for rendering with providers and graceful fallback.&lt;br /&gt;
&lt;br /&gt;
=== Story 7: Frontend — Initiate OIDC Flow ===&lt;br /&gt;
'''As a''' user, '''I want''' selecting a provider from the dropdown to start the login flow, '''so that''' I am redirected to my school's login page.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* On selection change, &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;/auth/client-select&amp;lt;/code&amp;gt; with the provider id.&lt;br /&gt;
* On success, redirect the browser to the returned authorization URL via &amp;lt;code&amp;gt;window.location.href&amp;lt;/code&amp;gt;.&lt;br /&gt;
* On failure, log the error to the console.&lt;br /&gt;
* Add component tests for the redirect and error handling.&lt;br /&gt;
&lt;br /&gt;
=== Story 8: Frontend — Callback Route and Login Completion ===&lt;br /&gt;
'''As a''' user, '''I want''' to be logged in automatically after authenticating with my school, '''so that''' I don't have to take any additional steps.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add a &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt; route in the React router pointing to the &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt; component.&lt;br /&gt;
* Extract &amp;lt;code&amp;gt;code&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; from query parameters and &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; them to &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt;.&lt;br /&gt;
* If the query parameters contain an &amp;lt;code&amp;gt;error&amp;lt;/code&amp;gt; param (e.g. user denied consent), display the error via the alert slice and redirect to login without calling the backend.&lt;br /&gt;
* On success: call &amp;lt;code&amp;gt;setAuthToken&amp;lt;/code&amp;gt;, persist session to localStorage, dispatch &amp;lt;code&amp;gt;authenticationActions.setAuthentication&amp;lt;/code&amp;gt;, and redirect to the dashboard — mirroring the existing password login flow.&lt;br /&gt;
* On failure: display an error message via the alert slice and redirect to the login page.&lt;br /&gt;
* Show a &amp;quot;Completing login...&amp;quot; message while the token exchange is in progress.&lt;br /&gt;
* Add component tests for success, provider error, and backend error scenarios.&lt;br /&gt;
&lt;br /&gt;
=== Story 9: Backend — Tests ===&lt;br /&gt;
'''As a''' developer, '''I want''' test coverage for the OIDC backend, '''so that''' I have confidence the endpoints and models work correctly.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add request specs for &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Mock the identity provider's discovery, token, and JWKS endpoints to avoid external calls in tests.&lt;br /&gt;
* Cover error scenarios: expired state, invalid state, unknown provider, no matching user, invalid ID token.&lt;br /&gt;
* Add model specs for &amp;lt;code&amp;gt;AuthRequest&amp;lt;/code&amp;gt; covering creation, uniqueness, expiry scope, and cleanup.&lt;br /&gt;
* Add unit tests for &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; covering loading, validation, and missing key detection.&lt;br /&gt;
* Verify the existing &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; is unaffected.&lt;br /&gt;
&lt;br /&gt;
=== Story 10: Frontend — Tests ===&lt;br /&gt;
'''As a''' developer, '''I want''' test coverage for the OIDC frontend components, '''so that''' I have confidence the login flow and callback work correctly.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add component tests for &amp;lt;code&amp;gt;OidcLogin&amp;lt;/code&amp;gt;: renders dropdown when providers are returned, renders nothing on empty or failed response, triggers &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; on selection.&lt;br /&gt;
* Add component tests for &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt;: posts code and state to backend on mount, redirects to dashboard on success, displays error and redirects to login on failure, handles IdP error parameter without calling the backend.&lt;br /&gt;
* Mock axios calls to avoid external requests in tests.&lt;br /&gt;
* Verify the existing login page renders and functions correctly with and without the &amp;lt;code&amp;gt;OidcLogin&amp;lt;/code&amp;gt; component.&lt;br /&gt;
&lt;br /&gt;
=== Additional Refactoring Stories ===&lt;br /&gt;
&lt;br /&gt;
==== Story 11: Backend — Unified Session Response ====&lt;br /&gt;
'''As a''' developer, '''I want''' the session object returned to the frontend clearly defined and reusable by all login flows, '''so that''' the frontend can rely on a consistent response shape regardless of authentication method.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Extract the JWT payload construction and token issuance logic from &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;OidcLoginController#callback&amp;lt;/code&amp;gt; into a shared method on the &amp;lt;code&amp;gt;User&amp;lt;/code&amp;gt; model (e.g. &amp;lt;code&amp;gt;user.generate_jwt&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Define a consistent response structure (e.g. &amp;lt;code&amp;gt;{ token, user: { id, name, full_name, role, institution_id } }&amp;lt;/code&amp;gt;) and use it in both controllers.&lt;br /&gt;
* Update the existing password login endpoint to use the shared method without changing its external response shape.&lt;br /&gt;
* Add or update request specs for both login endpoints to assert the response structure matches.&lt;br /&gt;
&lt;br /&gt;
==== Story 12: Frontend — Externalize Hardcoded Configuration ====&lt;br /&gt;
'''As a''' developer, '''I want''' all hardcoded values moved to isolated configuration files, '''so that''' environment-specific settings can be changed without code modifications.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Move the frontend API base URL to an environment variable or shared config file (e.g. &amp;lt;code&amp;gt;.env&amp;lt;/code&amp;gt; with &amp;lt;code&amp;gt;REACT_APP_API_URL&amp;lt;/code&amp;gt;) and replace all hardcoded references.&lt;br /&gt;
* Ensure all existing tests continue to pass after the extraction.&lt;br /&gt;
&lt;br /&gt;
==== Story 13: Backend — Swagger Documentation for Provider Endpoints ====&lt;br /&gt;
'''As a''' developer, '''I want''' the OIDC provider endpoints documented in Swagger, '''so that''' frontend developers and future contributors can understand the API contract without reading the source code.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add Swagger/OpenAPI annotations for &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Document request parameters, response schemas (including success and error shapes), and HTTP status codes for each endpoint.&lt;br /&gt;
* Include example request and response payloads.&lt;br /&gt;
* Verify the endpoints appear correctly in the generated Swagger UI.&lt;br /&gt;
&lt;br /&gt;
==== Story 14: Backend — Cleanup Expired Auth Requests ====&lt;br /&gt;
'''As a''' developer, '''I want''' expired auth request rows cleaned up automatically, '''so that''' the table does not grow unbounded from abandoned login attempts.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add a recurring ActiveJob solid_queue &amp;lt;code&amp;gt;auth_requests:cleanup&amp;lt;/code&amp;gt; that calls &amp;lt;code&amp;gt;AuthRequest.cleanup_expired&amp;lt;/code&amp;gt;.&lt;br /&gt;
* The task deletes all rows with &amp;lt;code&amp;gt;created_at&amp;lt;/code&amp;gt; older than 5 minutes.&lt;br /&gt;
* Schedule the task to run periodically (e.g. 24 hours).&lt;br /&gt;
* Add a test verifying that only expired rows are deleted.&lt;br /&gt;
&lt;br /&gt;
== Demo ==&lt;br /&gt;
&lt;br /&gt;
todo add screenshots of oidc login at each step&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2618._Support_OIDC_Logins&amp;diff=167956</id>
		<title>CSC/ECE 517 Spring 2026 - E2618. Support OIDC Logins</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2618._Support_OIDC_Logins&amp;diff=167956"/>
		<updated>2026-04-14T21:38:55Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Purpose */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Purpose ==&lt;br /&gt;
By integrating OIDC login, users can authenticate using their existing university credentials, providing a familiar and streamlined login experience. Traditional username and password login will continue to be supported alongside OIDC, allowing users to choose their preferred authentication method.&lt;br /&gt;
&lt;br /&gt;
== Requirements ==&lt;br /&gt;
=== Authentication Flow ===&lt;br /&gt;
Users select a provider from a dropdown on the login page that redirects them to the school's OIDC provider, authenticates them, and redirects back to the application with a valid session. The frontend fetches available providers from the backend via &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; and renders them dynamically in a dropdown. On provider selection, the frontend posts to the backend, which returns an authorization URL. After the user completes the OIDC flow, the IdP redirects back to the frontend callback, which posts the authorization code and state to the backend to complete login.&lt;br /&gt;
&lt;br /&gt;
=== Session Management ===&lt;br /&gt;
Issue and maintain a local application session (JWT) after successful OIDC authentication, using the same &amp;lt;code&amp;gt;JsonWebToken&amp;lt;/code&amp;gt; class and payload structure as the existing password login. Refresh token grant flow will not be considered at this time (since session is managed by the application).&lt;br /&gt;
&lt;br /&gt;
=== Account Linking ===&lt;br /&gt;
Match the authenticated user's email from the ID token to an existing local account. No dedicated account linking table or just-in-time account creation will be built at this time. If no matching local account is found, an error is returned.&lt;br /&gt;
&lt;br /&gt;
=== Configuration ===&lt;br /&gt;
OIDC provider configurations (display name, scopes, endpoints) are defined in a YAML config file (&amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt;). Client credentials (client ID, client secret) are stored in environment variables and injected via ERB. Providers that support OIDC discovery have their endpoints and JWKS keys fetched automatically. The system supports multiple OIDC provider configurations simultaneously. The configuration is validated at boot via an initializer, surfacing missing values immediately on deploy rather than at login time.&lt;br /&gt;
&lt;br /&gt;
=== State Management ===&lt;br /&gt;
OIDC state, nonce, and PKCE code verifier are stored server-side in an &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; database table (via ActiveRecord) rather than in session cookies. This avoids cross-origin cookie issues between the separate frontend and backend. Rows are expired after 5 minutes and deleted after use. This table is named generically to support future federated auth protocols such as SAML. Note that many OIDC libraries (including &amp;lt;code&amp;gt;omniauth_openid_connect&amp;lt;/code&amp;gt;) use cookies to track state; due to SameSite restrictions on cross-origin requests, this approach leads to instability with a separated frontend and backend and should be avoided.&lt;br /&gt;
&lt;br /&gt;
=== Logout ===&lt;br /&gt;
Logout will not be impacted.&lt;br /&gt;
&lt;br /&gt;
=== Error Handling ===&lt;br /&gt;
Gracefully handle failures such as no local account matching the authenticated email, expired or invalid state parameters, token exchange errors, ID token verification errors, and user-denied consent at the IdP.&lt;br /&gt;
&lt;br /&gt;
=== Security ===&lt;br /&gt;
Use the Authorization Code flow with the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; Ruby gem (by nov). Validate the ID token signature and claims via JWKS keys from the provider's discovery document. Enforce a &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; parameter to prevent CSRF and a &amp;lt;code&amp;gt;nonce&amp;lt;/code&amp;gt; to prevent replay attacks. PKCE (code verifier and code challenge) is always included in the authorization request and token exchange; providers that support it will enforce it, and providers that do not will ignore the extra parameters. The backend is a confidential client and always authenticates with both client secret and PKCE.&lt;br /&gt;
&lt;br /&gt;
=== Testing ===&lt;br /&gt;
Backend and frontend are tested independently. Backend request specs use WebMock to stub the identity provider's discovery, token, and JWKS endpoints, allowing the full controller logic (state management, token exchange, ID token verification, user matching) to be tested without external dependencies. Frontend component tests mock axios calls to verify rendering, dropdown behavior, callback handling, and error display. End-to-end testing across both systems with a live identity provider is not planned at this time, as it would require standing up a mock IdP server (e.g. Keycloak or mock-oauth2-server), which is beyond the scope of the existing test infrastructure. The full OIDC login flow will be manually verified against Google's OIDC provider in a local development environment and demonstrated as needed.&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
[[File:OIDC Provider-2026-04-06-223511.png|1000px]]&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
* '''Boot (Step 0):''' Load provider configurations from &amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt; with secrets injected from environment variables via ERB. Each provider entry defines a display name, scopes, issuer, client credentials, and redirect URI. The &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; class validates that all required keys are present at boot via &amp;lt;code&amp;gt;config/initializers/oidc.rb&amp;lt;/code&amp;gt;. For providers with &amp;lt;code&amp;gt;discovery: true&amp;lt;/code&amp;gt;, the &amp;lt;code&amp;gt;.well-known/openid-configuration&amp;lt;/code&amp;gt; document is fetched using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem to resolve the authorization endpoint, token endpoint, userinfo endpoint, and JWKS keys. Discovery results are not aggressively cached to allow for key rotation; on signature verification failure, keys are re-fetched and verification is retried once.&lt;br /&gt;
* '''Provider List (Step 1):''' Expose a &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; endpoint that returns a JSON array of &amp;lt;code&amp;gt;{ id, name }&amp;lt;/code&amp;gt; from &amp;lt;code&amp;gt;OidcConfig.public_list&amp;lt;/code&amp;gt;. No secrets or endpoint details are included in this response.&lt;br /&gt;
* '''Client Select (Step 2):''' Expose a &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; endpoint that accepts a provider id. Generate a cryptographically random state and nonce via &amp;lt;code&amp;gt;SecureRandom.hex(32)&amp;lt;/code&amp;gt;, and a PKCE code verifier via &amp;lt;code&amp;gt;SecureRandom.urlsafe_base64(64)&amp;lt;/code&amp;gt; with a SHA256 code challenge. Insert a row into the &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; table containing the state, nonce, code verifier, provider id, and creation timestamp. Construct the authorization URL using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem's &amp;lt;code&amp;gt;authorization_uri&amp;lt;/code&amp;gt; method and return it to the frontend.&lt;br /&gt;
* '''Callback (Step 4):''' Expose a &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt; endpoint (and a temporary &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; for direct IdP redirect during backend-only testing) that accepts the authorization code and state. Look up the matching &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; row by state, rejecting the request if no row is found or if the row is older than 5 minutes. Delete the row to prevent reuse. Using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem, exchange the authorization code for tokens via &amp;lt;code&amp;gt;access_token!&amp;lt;/code&amp;gt; with the stored code verifier. Decode the ID token using &amp;lt;code&amp;gt;OpenIDConnect::ResponseObject::IdToken.decode&amp;lt;/code&amp;gt; against the provider's JWKS keys, and verify the issuer, client_id, and nonce via &amp;lt;code&amp;gt;id_token.verify!&amp;lt;/code&amp;gt;. Extract the user's email from the ID token claims and look up a matching local user. If a match is found, issue a session JWT using the same &amp;lt;code&amp;gt;JsonWebToken.encode&amp;lt;/code&amp;gt; method and payload structure as the existing &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; action. If no match is found, return a 404 error indicating no local account exists for that email.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
* '''Login Page (Step 1):''' On page load, the &amp;lt;code&amp;gt;OidcLogin&amp;lt;/code&amp;gt; component calls &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; and renders a dropdown (&amp;lt;code&amp;gt;Form.Select&amp;lt;/code&amp;gt;) for each configured provider below the existing username and password form. If the request fails or returns empty, the component renders nothing and the standard login form remains available and unaffected. No loading state is shown to avoid visual disruption when no providers are configured.&lt;br /&gt;
* '''Initiate Login (Step 2):''' When the user selects a provider from the dropdown, &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;/auth/client-select&amp;lt;/code&amp;gt; with the selected provider id. On success, redirect the browser to the returned authorization URL via &amp;lt;code&amp;gt;window.location.href&amp;lt;/code&amp;gt;. The user then authenticates with the identity provider and is redirected back to the frontend callback route.&lt;br /&gt;
* '''Callback (Step 4):''' The &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt; page component handles the redirect back from the identity provider at &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt;. It extracts the authorization code and state from the query parameters and &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt;s them to &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt;. If the query parameters contain an &amp;lt;code&amp;gt;error&amp;lt;/code&amp;gt; parameter instead of a code (e.g. the user denied consent), the error is displayed without calling the backend and the user is redirected to the login page.&lt;br /&gt;
* '''Login Complete (Step 5):''' On a successful callback response, store the session JWT via &amp;lt;code&amp;gt;setAuthToken&amp;lt;/code&amp;gt;, update the Redux auth state via &amp;lt;code&amp;gt;authenticationActions.setAuthentication&amp;lt;/code&amp;gt;, persist the session to localStorage, and redirect the user to the dashboard. This mirrors the existing password login flow exactly. On failure, display an error alert and redirect to the login page.&lt;br /&gt;
* The existing username and password login flow remains unchanged and fully functional.&lt;br /&gt;
&lt;br /&gt;
=== Design Patterns ===&lt;br /&gt;
The implementation uses the '''Strategy pattern''' for provider configuration. Each OIDC provider is defined declaratively in YAML with its own credentials, scopes, and endpoints, while the controller logic remains provider-agnostic. Adding a new identity provider requires only a new configuration block and environment variables, with no code changes.&lt;br /&gt;
&lt;br /&gt;
=== Schema ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; table stores temporary OIDC login state. Each row represents a single in-progress login attempt and is deleted after use or expiry.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Column !! Type !! Constraints !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt; || bigint || primary key || Row identifier&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; || string || unique, indexed || CSRF protection; used to look up the request on callback&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;nonce&amp;lt;/code&amp;gt; || string || not null || Replay attack prevention; verified against the ID token claim&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;code_verifier&amp;lt;/code&amp;gt; || string || not null || PKCE secret; sent to the token endpoint to prove the same party initiated the flow&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;provider&amp;lt;/code&amp;gt; || string || not null || Which OIDC provider config to use on callback&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;created_at&amp;lt;/code&amp;gt; || datetime || not null || Used to expire rows older than 5 minutes&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
No foreign keys or associations to other tables. This table is intentionally generic to support future federated auth protocols such as SAML.&lt;br /&gt;
&lt;br /&gt;
== Library Choice ==&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem (by nov, [https://github.com/nov/openid_connect github.com/nov/openid_connect]) was chosen over &amp;lt;code&amp;gt;omniauth_openid_connect&amp;lt;/code&amp;gt; for the following reasons:&lt;br /&gt;
&lt;br /&gt;
* '''No cookie/session dependency:''' &amp;lt;code&amp;gt;omniauth_openid_connect&amp;lt;/code&amp;gt; stores state and nonce in the server-side session via cookies. With a separate frontend and backend on different origins, session cookies are not reliably shared due to SameSite restrictions. Using &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; directly allows state management via the database instead.&lt;br /&gt;
* '''Explicit control:''' The gem provides building blocks (discovery, client construction, token exchange, ID token verification) without middleware magic. Each step in the OIDC flow is visible in the controller code.&lt;br /&gt;
* '''Lightweight:''' No OmniAuth middleware stack or Rack integration required. The gem handles the protocol; the application handles routing and state.&lt;br /&gt;
* '''Actively maintained:''' The gem is OpenID Foundation certified and used by 2,700+ projects on GitHub.&lt;br /&gt;
&lt;br /&gt;
The tradeoff is approximately 10 additional lines of code for state management (generating and storing state/nonce/PKCE in the &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; table), which is minimal compared to the complexity of debugging cross-origin cookie issues.&lt;br /&gt;
&lt;br /&gt;
== File Diffs and Additions (Prototype and subject to change) ==&lt;br /&gt;
&lt;br /&gt;
=== Backend (Rails) ===&lt;br /&gt;
([https://github.com/johnmweisz/reimplementation-back-end/tree/2618-oidc-login branch: 2618-oidc-login])&lt;br /&gt;
&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/app/controllers/oidc_login_controller.rb app/controllers/oidc_login_controller.rb]  — Controller with providers, client_select, and callback actions&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/app/models/oidc_config.rb app/models/oidc_config.rb]                 — YAML config loader with validation&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/app/models/auth_request.rb app/models/auth_request.rb]                — ActiveRecord model for state/nonce/PKCE storage&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/config/oidc_providers.yml config/oidc_providers.yml]                 — Provider configuration (ERB for env var injection)&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/config/initializers/oidc.rb config/initializers/oidc.rb]               — Boot-time config validation&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/db/migrate/20260407003623_create_auth_requests.rb db/migrate/*_create_auth_requests.rb]      — Migration for auth_requests table&lt;br /&gt;
&lt;br /&gt;
=== Backend (RSpec) Tests ===&lt;br /&gt;
([https://github.com/johnmweisz/reimplementation-back-end/tree/2618-oidc-login branch: 2618-oidc-login])&lt;br /&gt;
&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/models/oidc_config_spec.rb spec/models/oidc_config_spec.rb]          — Config loading, validation, missing keys, public_list secrets exclusion, provider lookup&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/models/auth_request_spec.rb spec/models/auth_request_spec.rb]         — State uniqueness, expiry scope, cleanup of stale rows&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/requests/oidc_login_spec.rb spec/requests/oidc_login_spec.rb]         — Endpoint tests covering:&lt;br /&gt;
&lt;br /&gt;
'''GET /auth/providers'''&lt;br /&gt;
* Returns provider list with id and name only, no secrets leaked&lt;br /&gt;
* '''TODO''' Returns empty array when no providers configured&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/client-select'''&lt;br /&gt;
* '''TODO''' Returns authorization URL with expected query parameters (client_id, redirect_uri, scope, state, nonce, code_challenge)&lt;br /&gt;
* '''TODO''' Creates an auth_requests row&lt;br /&gt;
* '''TODO''' Returns 404 for unknown provider&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — Happy Path'''&lt;br /&gt;
* '''TODO''' Exchanges valid code and state for a session JWT with same payload structure as password login&lt;br /&gt;
* '''TODO''' Deletes the auth_requests row after successful use&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — CSRF Protection (State Validation)'''&lt;br /&gt;
* '''TODO''' Rejects unknown state (422)&lt;br /&gt;
* '''TODO''' Rejects expired state older than 5 minutes (422)&lt;br /&gt;
* '''TODO''' Rejects already-consumed state / replay (422)&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — Replay Protection (Nonce Validation)'''&lt;br /&gt;
* '''TODO''' Rejects ID token with mismatched nonce&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — Token Verification'''&lt;br /&gt;
* '''TODO''' Rejects ID token with invalid signature&lt;br /&gt;
* '''TODO''' Rejects ID token with mismatched issuer&lt;br /&gt;
* '''TODO''' Rejects ID token with mismatched audience (client_id)&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — User Matching'''&lt;br /&gt;
* '''TODO''' Returns 404 when no local user matches the email&lt;br /&gt;
* '''TODO''' Matches correct user when multiple users exist&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — PKCE'''&lt;br /&gt;
* '''TODO''' Sends code_verifier to the token endpoint during code exchange&lt;br /&gt;
&lt;br /&gt;
=== Frontend (React) ===&lt;br /&gt;
([https://github.com/johnmweisz/reimplementation-front-end/tree/2618-oidc-login branch: 2618-oidc-login])&lt;br /&gt;
&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/components/OidcLogin/OidcLogin.tsx src/components/OidcLogin/OidcLogin.tsx]     — Provider dropdown component&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/pages/OidcCallback/OidcCallback.tsx src/pages/OidcCallback/OidcCallback.tsx]    — Callback page handling code exchange&lt;br /&gt;
&lt;br /&gt;
=== Frontend (Vitest) Tests ===&lt;br /&gt;
([https://github.com/johnmweisz/reimplementation-front-end/tree/2618-oidc-login branch: 2618-oidc-login])&lt;br /&gt;
&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/components/OidcLogin/OidcLogin.test.tsx src/components/OidcLogin/OidcLogin.test.tsx]     — Provider dropdown component tests&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/pages/OidcCallback/OidcCallback.test.tsx src/pages/OidcCallback/OidcCallback.test.tsx]    — Callback page tests&lt;br /&gt;
&lt;br /&gt;
'''OidcLogin Component'''&lt;br /&gt;
* '''TODO''' Renders dropdown with providers when GET /auth/providers returns a non-empty list&lt;br /&gt;
* '''TODO''' Renders nothing when GET /auth/providers returns an empty array&lt;br /&gt;
* '''TODO''' Renders nothing when GET /auth/providers fails&lt;br /&gt;
* '''TODO''' Calls POST /auth/client-select with selected provider id on dropdown change&lt;br /&gt;
* '''TODO''' Redirects browser to returned authorization URL on successful client-select&lt;br /&gt;
* '''TODO''' Does not redirect when client-select fails&lt;br /&gt;
* '''TODO''' Existing login form remains visible and functional alongside the dropdown&lt;br /&gt;
&lt;br /&gt;
'''OidcCallback Component'''&lt;br /&gt;
* '''TODO''' Posts code and state to POST /auth/callback on mount&lt;br /&gt;
* '''TODO''' Stores session JWT and dispatches auth state on success&lt;br /&gt;
* '''TODO''' Redirects to dashboard on successful login&lt;br /&gt;
* '''TODO''' Displays error alert and redirects to login on backend failure (e.g. no matching account)&lt;br /&gt;
* '''TODO''' Displays error and redirects to login when IdP returns an error parameter (e.g. user denied consent) without calling the backend&lt;br /&gt;
* '''TODO''' Redirects to login when code or state query parameters are missing&lt;br /&gt;
* '''TODO''' Shows &amp;quot;Completing login...&amp;quot; message while request is in flight&lt;br /&gt;
&lt;br /&gt;
=== Routes ===&lt;br /&gt;
 GET  /auth/providers      → oidc_login#providers&lt;br /&gt;
 POST /auth/client-select  → oidc_login#client_select&lt;br /&gt;
 POST /auth/callback       → oidc_login#callback&lt;br /&gt;
 GET  /auth/callback       → React OidcCallback component&lt;br /&gt;
&lt;br /&gt;
== Development Stories (Planning) ==&lt;br /&gt;
&lt;br /&gt;
=== Story 1: Backend — OIDC Provider Configuration ===&lt;br /&gt;
'''As a''' developer, '''I want''' provider configurations loaded from a YAML file at boot, '''so that''' new OIDC providers can be added without code changes.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Create &amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt; with ERB support for injecting secrets from environment variables.&lt;br /&gt;
* Create an &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; class that loads and validates the YAML at boot, exposing methods to list providers and look up a provider by id.&lt;br /&gt;
* Define the config file path as a constant (&amp;lt;code&amp;gt;CONFIG_FILE&amp;lt;/code&amp;gt;) for clarity.&lt;br /&gt;
* Validate required keys: &amp;lt;code&amp;gt;display_name&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;issuer&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;client_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;client_secret&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;redirect_uri&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;scopes&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Raise a clear boot-time error if required values are missing via &amp;lt;code&amp;gt;config/initializers/oidc.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Add unit tests for config loading, validation, and missing key detection.&lt;br /&gt;
&lt;br /&gt;
=== Story 2: Backend — Auth Requests Table ===&lt;br /&gt;
'''As a''' developer, '''I want''' a database-backed store for OIDC state, nonce, and PKCE values, '''so that''' the backend can validate callbacks without relying on cookies.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Generate an ActiveRecord migration for &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; with columns: &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; (string, indexed, unique), &amp;lt;code&amp;gt;nonce&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;code_verifier&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;provider&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;created_at&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Create the &amp;lt;code&amp;gt;AuthRequest&amp;lt;/code&amp;gt; model with a uniqueness validation on state, an &amp;lt;code&amp;gt;expired&amp;lt;/code&amp;gt; scope, and a &amp;lt;code&amp;gt;cleanup_expired&amp;lt;/code&amp;gt; class method.&lt;br /&gt;
* Add a rake task or scheduled job to periodically clean up expired rows.&lt;br /&gt;
* Add unit tests for creation, lookup, expiry, and deletion.&lt;br /&gt;
&lt;br /&gt;
=== Story 3: Backend — Provider List Endpoint ===&lt;br /&gt;
'''As a''' frontend developer, '''I want''' a &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; endpoint, '''so that''' the login page can dynamically render provider options.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Create a controller action that returns a JSON array of &amp;lt;code&amp;gt;{ id, name }&amp;lt;/code&amp;gt; from &amp;lt;code&amp;gt;OidcConfig.public_list&amp;lt;/code&amp;gt;.&lt;br /&gt;
* No secrets or endpoint URLs are included in the response.&lt;br /&gt;
* Add a request spec covering the response format and a case with multiple providers.&lt;br /&gt;
&lt;br /&gt;
=== Story 4: Backend — Client Select Endpoint ===&lt;br /&gt;
'''As a''' frontend developer, '''I want''' a &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; endpoint that returns an authorization URL, '''so that''' the frontend can redirect the user to the identity provider.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Accept a &amp;lt;code&amp;gt;provider&amp;lt;/code&amp;gt; param and look up the provider config.&lt;br /&gt;
* Generate cryptographically random state, nonce, and PKCE code verifier/challenge.&lt;br /&gt;
* Insert a row into &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Construct and return the authorization URL with client_id, redirect_uri, scopes, state, nonce, and code_challenge.&lt;br /&gt;
* Return a 404 if the provider is unknown.&lt;br /&gt;
* Add request specs covering the happy path, unknown provider, and that the auth_requests row is created.&lt;br /&gt;
&lt;br /&gt;
=== Story 5: Backend — Callback Endpoint ===&lt;br /&gt;
'''As a''' frontend developer, '''I want''' a &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt; endpoint that exchanges the authorization code for tokens and returns a session, '''so that''' the user is logged in after completing the OIDC flow.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Accept &amp;lt;code&amp;gt;code&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; params.&lt;br /&gt;
* Look up and delete the matching &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; row. Reject if not found or expired.&lt;br /&gt;
* Exchange the code for tokens using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem with the stored code_verifier.&lt;br /&gt;
* Verify the ID token signature (JWKS), issuer, client_id, and nonce.&lt;br /&gt;
* Extract the email claim and match to an existing local user.&lt;br /&gt;
* On match: issue a session JWT using the same payload structure as &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; and return it with the user profile.&lt;br /&gt;
* On no match: return a 404 error indicating no local account was found.&lt;br /&gt;
* Handle &amp;lt;code&amp;gt;ActiveRecord::RecordNotFound&amp;lt;/code&amp;gt; (invalid/expired state) and &amp;lt;code&amp;gt;InvalidToken&amp;lt;/code&amp;gt; (verification failure) exceptions.&lt;br /&gt;
* Add request specs covering the happy path, expired state, invalid state, and no matching user.&lt;br /&gt;
&lt;br /&gt;
=== Story 6: Frontend — Provider Dropdown on Login Page ===&lt;br /&gt;
'''As a''' user, '''I want''' to see a provider dropdown on the login page, '''so that''' I can authenticate with my school credentials.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Create an &amp;lt;code&amp;gt;OidcLogin&amp;lt;/code&amp;gt; component that calls &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; on mount.&lt;br /&gt;
* Render a &amp;lt;code&amp;gt;Form.Select&amp;lt;/code&amp;gt; dropdown with a disabled &amp;quot;Sign in with...&amp;quot; default option.&lt;br /&gt;
* If the request fails or returns empty, render nothing (no error, no placeholder).&lt;br /&gt;
* Existing login form remains unchanged and fully functional.&lt;br /&gt;
* Add component tests for rendering with providers and graceful fallback.&lt;br /&gt;
&lt;br /&gt;
=== Story 7: Frontend — Initiate OIDC Flow ===&lt;br /&gt;
'''As a''' user, '''I want''' selecting a provider from the dropdown to start the login flow, '''so that''' I am redirected to my school's login page.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* On selection change, &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;/auth/client-select&amp;lt;/code&amp;gt; with the provider id.&lt;br /&gt;
* On success, redirect the browser to the returned authorization URL via &amp;lt;code&amp;gt;window.location.href&amp;lt;/code&amp;gt;.&lt;br /&gt;
* On failure, log the error to the console.&lt;br /&gt;
* Add component tests for the redirect and error handling.&lt;br /&gt;
&lt;br /&gt;
=== Story 8: Frontend — Callback Route and Login Completion ===&lt;br /&gt;
'''As a''' user, '''I want''' to be logged in automatically after authenticating with my school, '''so that''' I don't have to take any additional steps.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add a &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt; route in the React router pointing to the &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt; component.&lt;br /&gt;
* Extract &amp;lt;code&amp;gt;code&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; from query parameters and &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; them to &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt;.&lt;br /&gt;
* If the query parameters contain an &amp;lt;code&amp;gt;error&amp;lt;/code&amp;gt; param (e.g. user denied consent), display the error via the alert slice and redirect to login without calling the backend.&lt;br /&gt;
* On success: call &amp;lt;code&amp;gt;setAuthToken&amp;lt;/code&amp;gt;, persist session to localStorage, dispatch &amp;lt;code&amp;gt;authenticationActions.setAuthentication&amp;lt;/code&amp;gt;, and redirect to the dashboard — mirroring the existing password login flow.&lt;br /&gt;
* On failure: display an error message via the alert slice and redirect to the login page.&lt;br /&gt;
* Show a &amp;quot;Completing login...&amp;quot; message while the token exchange is in progress.&lt;br /&gt;
* Add component tests for success, provider error, and backend error scenarios.&lt;br /&gt;
&lt;br /&gt;
=== Story 9: Backend — Tests ===&lt;br /&gt;
'''As a''' developer, '''I want''' test coverage for the OIDC backend, '''so that''' I have confidence the endpoints and models work correctly.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add request specs for &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Mock the identity provider's discovery, token, and JWKS endpoints to avoid external calls in tests.&lt;br /&gt;
* Cover error scenarios: expired state, invalid state, unknown provider, no matching user, invalid ID token.&lt;br /&gt;
* Add model specs for &amp;lt;code&amp;gt;AuthRequest&amp;lt;/code&amp;gt; covering creation, uniqueness, expiry scope, and cleanup.&lt;br /&gt;
* Add unit tests for &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; covering loading, validation, and missing key detection.&lt;br /&gt;
* Verify the existing &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; is unaffected.&lt;br /&gt;
&lt;br /&gt;
=== Story 10: Frontend — Tests ===&lt;br /&gt;
'''As a''' developer, '''I want''' test coverage for the OIDC frontend components, '''so that''' I have confidence the login flow and callback work correctly.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add component tests for &amp;lt;code&amp;gt;OidcLogin&amp;lt;/code&amp;gt;: renders dropdown when providers are returned, renders nothing on empty or failed response, triggers &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; on selection.&lt;br /&gt;
* Add component tests for &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt;: posts code and state to backend on mount, redirects to dashboard on success, displays error and redirects to login on failure, handles IdP error parameter without calling the backend.&lt;br /&gt;
* Mock axios calls to avoid external requests in tests.&lt;br /&gt;
* Verify the existing login page renders and functions correctly with and without the &amp;lt;code&amp;gt;OidcLogin&amp;lt;/code&amp;gt; component.&lt;br /&gt;
&lt;br /&gt;
=== Additional Refactoring Stories ===&lt;br /&gt;
&lt;br /&gt;
==== Story 11: Backend — Unified Session Response ====&lt;br /&gt;
'''As a''' developer, '''I want''' the session object returned to the frontend clearly defined and reusable by all login flows, '''so that''' the frontend can rely on a consistent response shape regardless of authentication method.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Extract the JWT payload construction and token issuance logic from &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;OidcLoginController#callback&amp;lt;/code&amp;gt; into a shared method on the &amp;lt;code&amp;gt;User&amp;lt;/code&amp;gt; model (e.g. &amp;lt;code&amp;gt;user.generate_jwt&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Define a consistent response structure (e.g. &amp;lt;code&amp;gt;{ token, user: { id, name, full_name, role, institution_id } }&amp;lt;/code&amp;gt;) and use it in both controllers.&lt;br /&gt;
* Update the existing password login endpoint to use the shared method without changing its external response shape.&lt;br /&gt;
* Add or update request specs for both login endpoints to assert the response structure matches.&lt;br /&gt;
&lt;br /&gt;
==== Story 12: Frontend — Externalize Hardcoded Configuration ====&lt;br /&gt;
'''As a''' developer, '''I want''' all hardcoded values moved to isolated configuration files, '''so that''' environment-specific settings can be changed without code modifications.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Move the frontend API base URL to an environment variable or shared config file (e.g. &amp;lt;code&amp;gt;.env&amp;lt;/code&amp;gt; with &amp;lt;code&amp;gt;REACT_APP_API_URL&amp;lt;/code&amp;gt;) and replace all hardcoded references.&lt;br /&gt;
* Ensure all existing tests continue to pass after the extraction.&lt;br /&gt;
&lt;br /&gt;
==== Story 13: Backend — Swagger Documentation for Provider Endpoints ====&lt;br /&gt;
'''As a''' developer, '''I want''' the OIDC provider endpoints documented in Swagger, '''so that''' frontend developers and future contributors can understand the API contract without reading the source code.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add Swagger/OpenAPI annotations for &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Document request parameters, response schemas (including success and error shapes), and HTTP status codes for each endpoint.&lt;br /&gt;
* Include example request and response payloads.&lt;br /&gt;
* Verify the endpoints appear correctly in the generated Swagger UI.&lt;br /&gt;
&lt;br /&gt;
==== Story 14: Backend — Cleanup Expired Auth Requests ====&lt;br /&gt;
'''As a''' developer, '''I want''' expired auth request rows cleaned up automatically, '''so that''' the table does not grow unbounded from abandoned login attempts.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add a recurring ActiveJob solid_queue &amp;lt;code&amp;gt;auth_requests:cleanup&amp;lt;/code&amp;gt; that calls &amp;lt;code&amp;gt;AuthRequest.cleanup_expired&amp;lt;/code&amp;gt;.&lt;br /&gt;
* The task deletes all rows with &amp;lt;code&amp;gt;created_at&amp;lt;/code&amp;gt; older than 5 minutes.&lt;br /&gt;
* Schedule the task to run periodically (e.g. 24 hours).&lt;br /&gt;
* Add a test verifying that only expired rows are deleted.&lt;br /&gt;
&lt;br /&gt;
== Demo ==&lt;br /&gt;
&lt;br /&gt;
todo add screenshots of oidc login at each step&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2618._Support_OIDC_Logins&amp;diff=167955</id>
		<title>CSC/ECE 517 Spring 2026 - E2618. Support OIDC Logins</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2618._Support_OIDC_Logins&amp;diff=167955"/>
		<updated>2026-04-14T21:38:30Z</updated>

		<summary type="html">&lt;p&gt;Admin: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Purpose ==&lt;br /&gt;
By integrating [[https://openid.net/developers/how-connect-works/|OIDC]] login, users can authenticate using their existing university credentials, providing a familiar and streamlined login experience. Traditional username and password login will continue to be supported alongside OIDC, allowing users to choose their preferred authentication method.&lt;br /&gt;
&lt;br /&gt;
== Requirements ==&lt;br /&gt;
=== Authentication Flow ===&lt;br /&gt;
Users select a provider from a dropdown on the login page that redirects them to the school's OIDC provider, authenticates them, and redirects back to the application with a valid session. The frontend fetches available providers from the backend via &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; and renders them dynamically in a dropdown. On provider selection, the frontend posts to the backend, which returns an authorization URL. After the user completes the OIDC flow, the IdP redirects back to the frontend callback, which posts the authorization code and state to the backend to complete login.&lt;br /&gt;
&lt;br /&gt;
=== Session Management ===&lt;br /&gt;
Issue and maintain a local application session (JWT) after successful OIDC authentication, using the same &amp;lt;code&amp;gt;JsonWebToken&amp;lt;/code&amp;gt; class and payload structure as the existing password login. Refresh token grant flow will not be considered at this time (since session is managed by the application).&lt;br /&gt;
&lt;br /&gt;
=== Account Linking ===&lt;br /&gt;
Match the authenticated user's email from the ID token to an existing local account. No dedicated account linking table or just-in-time account creation will be built at this time. If no matching local account is found, an error is returned.&lt;br /&gt;
&lt;br /&gt;
=== Configuration ===&lt;br /&gt;
OIDC provider configurations (display name, scopes, endpoints) are defined in a YAML config file (&amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt;). Client credentials (client ID, client secret) are stored in environment variables and injected via ERB. Providers that support OIDC discovery have their endpoints and JWKS keys fetched automatically. The system supports multiple OIDC provider configurations simultaneously. The configuration is validated at boot via an initializer, surfacing missing values immediately on deploy rather than at login time.&lt;br /&gt;
&lt;br /&gt;
=== State Management ===&lt;br /&gt;
OIDC state, nonce, and PKCE code verifier are stored server-side in an &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; database table (via ActiveRecord) rather than in session cookies. This avoids cross-origin cookie issues between the separate frontend and backend. Rows are expired after 5 minutes and deleted after use. This table is named generically to support future federated auth protocols such as SAML. Note that many OIDC libraries (including &amp;lt;code&amp;gt;omniauth_openid_connect&amp;lt;/code&amp;gt;) use cookies to track state; due to SameSite restrictions on cross-origin requests, this approach leads to instability with a separated frontend and backend and should be avoided.&lt;br /&gt;
&lt;br /&gt;
=== Logout ===&lt;br /&gt;
Logout will not be impacted.&lt;br /&gt;
&lt;br /&gt;
=== Error Handling ===&lt;br /&gt;
Gracefully handle failures such as no local account matching the authenticated email, expired or invalid state parameters, token exchange errors, ID token verification errors, and user-denied consent at the IdP.&lt;br /&gt;
&lt;br /&gt;
=== Security ===&lt;br /&gt;
Use the Authorization Code flow with the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; Ruby gem (by nov). Validate the ID token signature and claims via JWKS keys from the provider's discovery document. Enforce a &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; parameter to prevent CSRF and a &amp;lt;code&amp;gt;nonce&amp;lt;/code&amp;gt; to prevent replay attacks. PKCE (code verifier and code challenge) is always included in the authorization request and token exchange; providers that support it will enforce it, and providers that do not will ignore the extra parameters. The backend is a confidential client and always authenticates with both client secret and PKCE.&lt;br /&gt;
&lt;br /&gt;
=== Testing ===&lt;br /&gt;
Backend and frontend are tested independently. Backend request specs use WebMock to stub the identity provider's discovery, token, and JWKS endpoints, allowing the full controller logic (state management, token exchange, ID token verification, user matching) to be tested without external dependencies. Frontend component tests mock axios calls to verify rendering, dropdown behavior, callback handling, and error display. End-to-end testing across both systems with a live identity provider is not planned at this time, as it would require standing up a mock IdP server (e.g. Keycloak or mock-oauth2-server), which is beyond the scope of the existing test infrastructure. The full OIDC login flow will be manually verified against Google's OIDC provider in a local development environment and demonstrated as needed.&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
[[File:OIDC Provider-2026-04-06-223511.png|1000px]]&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
* '''Boot (Step 0):''' Load provider configurations from &amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt; with secrets injected from environment variables via ERB. Each provider entry defines a display name, scopes, issuer, client credentials, and redirect URI. The &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; class validates that all required keys are present at boot via &amp;lt;code&amp;gt;config/initializers/oidc.rb&amp;lt;/code&amp;gt;. For providers with &amp;lt;code&amp;gt;discovery: true&amp;lt;/code&amp;gt;, the &amp;lt;code&amp;gt;.well-known/openid-configuration&amp;lt;/code&amp;gt; document is fetched using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem to resolve the authorization endpoint, token endpoint, userinfo endpoint, and JWKS keys. Discovery results are not aggressively cached to allow for key rotation; on signature verification failure, keys are re-fetched and verification is retried once.&lt;br /&gt;
* '''Provider List (Step 1):''' Expose a &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; endpoint that returns a JSON array of &amp;lt;code&amp;gt;{ id, name }&amp;lt;/code&amp;gt; from &amp;lt;code&amp;gt;OidcConfig.public_list&amp;lt;/code&amp;gt;. No secrets or endpoint details are included in this response.&lt;br /&gt;
* '''Client Select (Step 2):''' Expose a &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; endpoint that accepts a provider id. Generate a cryptographically random state and nonce via &amp;lt;code&amp;gt;SecureRandom.hex(32)&amp;lt;/code&amp;gt;, and a PKCE code verifier via &amp;lt;code&amp;gt;SecureRandom.urlsafe_base64(64)&amp;lt;/code&amp;gt; with a SHA256 code challenge. Insert a row into the &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; table containing the state, nonce, code verifier, provider id, and creation timestamp. Construct the authorization URL using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem's &amp;lt;code&amp;gt;authorization_uri&amp;lt;/code&amp;gt; method and return it to the frontend.&lt;br /&gt;
* '''Callback (Step 4):''' Expose a &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt; endpoint (and a temporary &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; for direct IdP redirect during backend-only testing) that accepts the authorization code and state. Look up the matching &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; row by state, rejecting the request if no row is found or if the row is older than 5 minutes. Delete the row to prevent reuse. Using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem, exchange the authorization code for tokens via &amp;lt;code&amp;gt;access_token!&amp;lt;/code&amp;gt; with the stored code verifier. Decode the ID token using &amp;lt;code&amp;gt;OpenIDConnect::ResponseObject::IdToken.decode&amp;lt;/code&amp;gt; against the provider's JWKS keys, and verify the issuer, client_id, and nonce via &amp;lt;code&amp;gt;id_token.verify!&amp;lt;/code&amp;gt;. Extract the user's email from the ID token claims and look up a matching local user. If a match is found, issue a session JWT using the same &amp;lt;code&amp;gt;JsonWebToken.encode&amp;lt;/code&amp;gt; method and payload structure as the existing &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; action. If no match is found, return a 404 error indicating no local account exists for that email.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
* '''Login Page (Step 1):''' On page load, the &amp;lt;code&amp;gt;OidcLogin&amp;lt;/code&amp;gt; component calls &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; and renders a dropdown (&amp;lt;code&amp;gt;Form.Select&amp;lt;/code&amp;gt;) for each configured provider below the existing username and password form. If the request fails or returns empty, the component renders nothing and the standard login form remains available and unaffected. No loading state is shown to avoid visual disruption when no providers are configured.&lt;br /&gt;
* '''Initiate Login (Step 2):''' When the user selects a provider from the dropdown, &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;/auth/client-select&amp;lt;/code&amp;gt; with the selected provider id. On success, redirect the browser to the returned authorization URL via &amp;lt;code&amp;gt;window.location.href&amp;lt;/code&amp;gt;. The user then authenticates with the identity provider and is redirected back to the frontend callback route.&lt;br /&gt;
* '''Callback (Step 4):''' The &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt; page component handles the redirect back from the identity provider at &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt;. It extracts the authorization code and state from the query parameters and &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt;s them to &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt;. If the query parameters contain an &amp;lt;code&amp;gt;error&amp;lt;/code&amp;gt; parameter instead of a code (e.g. the user denied consent), the error is displayed without calling the backend and the user is redirected to the login page.&lt;br /&gt;
* '''Login Complete (Step 5):''' On a successful callback response, store the session JWT via &amp;lt;code&amp;gt;setAuthToken&amp;lt;/code&amp;gt;, update the Redux auth state via &amp;lt;code&amp;gt;authenticationActions.setAuthentication&amp;lt;/code&amp;gt;, persist the session to localStorage, and redirect the user to the dashboard. This mirrors the existing password login flow exactly. On failure, display an error alert and redirect to the login page.&lt;br /&gt;
* The existing username and password login flow remains unchanged and fully functional.&lt;br /&gt;
&lt;br /&gt;
=== Design Patterns ===&lt;br /&gt;
The implementation uses the '''Strategy pattern''' for provider configuration. Each OIDC provider is defined declaratively in YAML with its own credentials, scopes, and endpoints, while the controller logic remains provider-agnostic. Adding a new identity provider requires only a new configuration block and environment variables, with no code changes.&lt;br /&gt;
&lt;br /&gt;
=== Schema ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; table stores temporary OIDC login state. Each row represents a single in-progress login attempt and is deleted after use or expiry.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Column !! Type !! Constraints !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt; || bigint || primary key || Row identifier&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; || string || unique, indexed || CSRF protection; used to look up the request on callback&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;nonce&amp;lt;/code&amp;gt; || string || not null || Replay attack prevention; verified against the ID token claim&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;code_verifier&amp;lt;/code&amp;gt; || string || not null || PKCE secret; sent to the token endpoint to prove the same party initiated the flow&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;provider&amp;lt;/code&amp;gt; || string || not null || Which OIDC provider config to use on callback&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;created_at&amp;lt;/code&amp;gt; || datetime || not null || Used to expire rows older than 5 minutes&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
No foreign keys or associations to other tables. This table is intentionally generic to support future federated auth protocols such as SAML.&lt;br /&gt;
&lt;br /&gt;
== Library Choice ==&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem (by nov, [https://github.com/nov/openid_connect github.com/nov/openid_connect]) was chosen over &amp;lt;code&amp;gt;omniauth_openid_connect&amp;lt;/code&amp;gt; for the following reasons:&lt;br /&gt;
&lt;br /&gt;
* '''No cookie/session dependency:''' &amp;lt;code&amp;gt;omniauth_openid_connect&amp;lt;/code&amp;gt; stores state and nonce in the server-side session via cookies. With a separate frontend and backend on different origins, session cookies are not reliably shared due to SameSite restrictions. Using &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; directly allows state management via the database instead.&lt;br /&gt;
* '''Explicit control:''' The gem provides building blocks (discovery, client construction, token exchange, ID token verification) without middleware magic. Each step in the OIDC flow is visible in the controller code.&lt;br /&gt;
* '''Lightweight:''' No OmniAuth middleware stack or Rack integration required. The gem handles the protocol; the application handles routing and state.&lt;br /&gt;
* '''Actively maintained:''' The gem is OpenID Foundation certified and used by 2,700+ projects on GitHub.&lt;br /&gt;
&lt;br /&gt;
The tradeoff is approximately 10 additional lines of code for state management (generating and storing state/nonce/PKCE in the &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; table), which is minimal compared to the complexity of debugging cross-origin cookie issues.&lt;br /&gt;
&lt;br /&gt;
== File Diffs and Additions (Prototype and subject to change) ==&lt;br /&gt;
&lt;br /&gt;
=== Backend (Rails) ===&lt;br /&gt;
([https://github.com/johnmweisz/reimplementation-back-end/tree/2618-oidc-login branch: 2618-oidc-login])&lt;br /&gt;
&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/app/controllers/oidc_login_controller.rb app/controllers/oidc_login_controller.rb]  — Controller with providers, client_select, and callback actions&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/app/models/oidc_config.rb app/models/oidc_config.rb]                 — YAML config loader with validation&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/app/models/auth_request.rb app/models/auth_request.rb]                — ActiveRecord model for state/nonce/PKCE storage&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/config/oidc_providers.yml config/oidc_providers.yml]                 — Provider configuration (ERB for env var injection)&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/config/initializers/oidc.rb config/initializers/oidc.rb]               — Boot-time config validation&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/db/migrate/20260407003623_create_auth_requests.rb db/migrate/*_create_auth_requests.rb]      — Migration for auth_requests table&lt;br /&gt;
&lt;br /&gt;
=== Backend (RSpec) Tests ===&lt;br /&gt;
([https://github.com/johnmweisz/reimplementation-back-end/tree/2618-oidc-login branch: 2618-oidc-login])&lt;br /&gt;
&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/models/oidc_config_spec.rb spec/models/oidc_config_spec.rb]          — Config loading, validation, missing keys, public_list secrets exclusion, provider lookup&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/models/auth_request_spec.rb spec/models/auth_request_spec.rb]         — State uniqueness, expiry scope, cleanup of stale rows&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/requests/oidc_login_spec.rb spec/requests/oidc_login_spec.rb]         — Endpoint tests covering:&lt;br /&gt;
&lt;br /&gt;
'''GET /auth/providers'''&lt;br /&gt;
* Returns provider list with id and name only, no secrets leaked&lt;br /&gt;
* '''TODO''' Returns empty array when no providers configured&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/client-select'''&lt;br /&gt;
* '''TODO''' Returns authorization URL with expected query parameters (client_id, redirect_uri, scope, state, nonce, code_challenge)&lt;br /&gt;
* '''TODO''' Creates an auth_requests row&lt;br /&gt;
* '''TODO''' Returns 404 for unknown provider&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — Happy Path'''&lt;br /&gt;
* '''TODO''' Exchanges valid code and state for a session JWT with same payload structure as password login&lt;br /&gt;
* '''TODO''' Deletes the auth_requests row after successful use&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — CSRF Protection (State Validation)'''&lt;br /&gt;
* '''TODO''' Rejects unknown state (422)&lt;br /&gt;
* '''TODO''' Rejects expired state older than 5 minutes (422)&lt;br /&gt;
* '''TODO''' Rejects already-consumed state / replay (422)&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — Replay Protection (Nonce Validation)'''&lt;br /&gt;
* '''TODO''' Rejects ID token with mismatched nonce&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — Token Verification'''&lt;br /&gt;
* '''TODO''' Rejects ID token with invalid signature&lt;br /&gt;
* '''TODO''' Rejects ID token with mismatched issuer&lt;br /&gt;
* '''TODO''' Rejects ID token with mismatched audience (client_id)&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — User Matching'''&lt;br /&gt;
* '''TODO''' Returns 404 when no local user matches the email&lt;br /&gt;
* '''TODO''' Matches correct user when multiple users exist&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — PKCE'''&lt;br /&gt;
* '''TODO''' Sends code_verifier to the token endpoint during code exchange&lt;br /&gt;
&lt;br /&gt;
=== Frontend (React) ===&lt;br /&gt;
([https://github.com/johnmweisz/reimplementation-front-end/tree/2618-oidc-login branch: 2618-oidc-login])&lt;br /&gt;
&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/components/OidcLogin/OidcLogin.tsx src/components/OidcLogin/OidcLogin.tsx]     — Provider dropdown component&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/pages/OidcCallback/OidcCallback.tsx src/pages/OidcCallback/OidcCallback.tsx]    — Callback page handling code exchange&lt;br /&gt;
&lt;br /&gt;
=== Frontend (Vitest) Tests ===&lt;br /&gt;
([https://github.com/johnmweisz/reimplementation-front-end/tree/2618-oidc-login branch: 2618-oidc-login])&lt;br /&gt;
&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/components/OidcLogin/OidcLogin.test.tsx src/components/OidcLogin/OidcLogin.test.tsx]     — Provider dropdown component tests&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/pages/OidcCallback/OidcCallback.test.tsx src/pages/OidcCallback/OidcCallback.test.tsx]    — Callback page tests&lt;br /&gt;
&lt;br /&gt;
'''OidcLogin Component'''&lt;br /&gt;
* '''TODO''' Renders dropdown with providers when GET /auth/providers returns a non-empty list&lt;br /&gt;
* '''TODO''' Renders nothing when GET /auth/providers returns an empty array&lt;br /&gt;
* '''TODO''' Renders nothing when GET /auth/providers fails&lt;br /&gt;
* '''TODO''' Calls POST /auth/client-select with selected provider id on dropdown change&lt;br /&gt;
* '''TODO''' Redirects browser to returned authorization URL on successful client-select&lt;br /&gt;
* '''TODO''' Does not redirect when client-select fails&lt;br /&gt;
* '''TODO''' Existing login form remains visible and functional alongside the dropdown&lt;br /&gt;
&lt;br /&gt;
'''OidcCallback Component'''&lt;br /&gt;
* '''TODO''' Posts code and state to POST /auth/callback on mount&lt;br /&gt;
* '''TODO''' Stores session JWT and dispatches auth state on success&lt;br /&gt;
* '''TODO''' Redirects to dashboard on successful login&lt;br /&gt;
* '''TODO''' Displays error alert and redirects to login on backend failure (e.g. no matching account)&lt;br /&gt;
* '''TODO''' Displays error and redirects to login when IdP returns an error parameter (e.g. user denied consent) without calling the backend&lt;br /&gt;
* '''TODO''' Redirects to login when code or state query parameters are missing&lt;br /&gt;
* '''TODO''' Shows &amp;quot;Completing login...&amp;quot; message while request is in flight&lt;br /&gt;
&lt;br /&gt;
=== Routes ===&lt;br /&gt;
 GET  /auth/providers      → oidc_login#providers&lt;br /&gt;
 POST /auth/client-select  → oidc_login#client_select&lt;br /&gt;
 POST /auth/callback       → oidc_login#callback&lt;br /&gt;
 GET  /auth/callback       → React OidcCallback component&lt;br /&gt;
&lt;br /&gt;
== Development Stories (Planning) ==&lt;br /&gt;
&lt;br /&gt;
=== Story 1: Backend — OIDC Provider Configuration ===&lt;br /&gt;
'''As a''' developer, '''I want''' provider configurations loaded from a YAML file at boot, '''so that''' new OIDC providers can be added without code changes.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Create &amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt; with ERB support for injecting secrets from environment variables.&lt;br /&gt;
* Create an &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; class that loads and validates the YAML at boot, exposing methods to list providers and look up a provider by id.&lt;br /&gt;
* Define the config file path as a constant (&amp;lt;code&amp;gt;CONFIG_FILE&amp;lt;/code&amp;gt;) for clarity.&lt;br /&gt;
* Validate required keys: &amp;lt;code&amp;gt;display_name&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;issuer&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;client_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;client_secret&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;redirect_uri&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;scopes&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Raise a clear boot-time error if required values are missing via &amp;lt;code&amp;gt;config/initializers/oidc.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Add unit tests for config loading, validation, and missing key detection.&lt;br /&gt;
&lt;br /&gt;
=== Story 2: Backend — Auth Requests Table ===&lt;br /&gt;
'''As a''' developer, '''I want''' a database-backed store for OIDC state, nonce, and PKCE values, '''so that''' the backend can validate callbacks without relying on cookies.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Generate an ActiveRecord migration for &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; with columns: &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; (string, indexed, unique), &amp;lt;code&amp;gt;nonce&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;code_verifier&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;provider&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;created_at&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Create the &amp;lt;code&amp;gt;AuthRequest&amp;lt;/code&amp;gt; model with a uniqueness validation on state, an &amp;lt;code&amp;gt;expired&amp;lt;/code&amp;gt; scope, and a &amp;lt;code&amp;gt;cleanup_expired&amp;lt;/code&amp;gt; class method.&lt;br /&gt;
* Add a rake task or scheduled job to periodically clean up expired rows.&lt;br /&gt;
* Add unit tests for creation, lookup, expiry, and deletion.&lt;br /&gt;
&lt;br /&gt;
=== Story 3: Backend — Provider List Endpoint ===&lt;br /&gt;
'''As a''' frontend developer, '''I want''' a &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; endpoint, '''so that''' the login page can dynamically render provider options.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Create a controller action that returns a JSON array of &amp;lt;code&amp;gt;{ id, name }&amp;lt;/code&amp;gt; from &amp;lt;code&amp;gt;OidcConfig.public_list&amp;lt;/code&amp;gt;.&lt;br /&gt;
* No secrets or endpoint URLs are included in the response.&lt;br /&gt;
* Add a request spec covering the response format and a case with multiple providers.&lt;br /&gt;
&lt;br /&gt;
=== Story 4: Backend — Client Select Endpoint ===&lt;br /&gt;
'''As a''' frontend developer, '''I want''' a &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; endpoint that returns an authorization URL, '''so that''' the frontend can redirect the user to the identity provider.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Accept a &amp;lt;code&amp;gt;provider&amp;lt;/code&amp;gt; param and look up the provider config.&lt;br /&gt;
* Generate cryptographically random state, nonce, and PKCE code verifier/challenge.&lt;br /&gt;
* Insert a row into &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Construct and return the authorization URL with client_id, redirect_uri, scopes, state, nonce, and code_challenge.&lt;br /&gt;
* Return a 404 if the provider is unknown.&lt;br /&gt;
* Add request specs covering the happy path, unknown provider, and that the auth_requests row is created.&lt;br /&gt;
&lt;br /&gt;
=== Story 5: Backend — Callback Endpoint ===&lt;br /&gt;
'''As a''' frontend developer, '''I want''' a &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt; endpoint that exchanges the authorization code for tokens and returns a session, '''so that''' the user is logged in after completing the OIDC flow.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Accept &amp;lt;code&amp;gt;code&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; params.&lt;br /&gt;
* Look up and delete the matching &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; row. Reject if not found or expired.&lt;br /&gt;
* Exchange the code for tokens using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem with the stored code_verifier.&lt;br /&gt;
* Verify the ID token signature (JWKS), issuer, client_id, and nonce.&lt;br /&gt;
* Extract the email claim and match to an existing local user.&lt;br /&gt;
* On match: issue a session JWT using the same payload structure as &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; and return it with the user profile.&lt;br /&gt;
* On no match: return a 404 error indicating no local account was found.&lt;br /&gt;
* Handle &amp;lt;code&amp;gt;ActiveRecord::RecordNotFound&amp;lt;/code&amp;gt; (invalid/expired state) and &amp;lt;code&amp;gt;InvalidToken&amp;lt;/code&amp;gt; (verification failure) exceptions.&lt;br /&gt;
* Add request specs covering the happy path, expired state, invalid state, and no matching user.&lt;br /&gt;
&lt;br /&gt;
=== Story 6: Frontend — Provider Dropdown on Login Page ===&lt;br /&gt;
'''As a''' user, '''I want''' to see a provider dropdown on the login page, '''so that''' I can authenticate with my school credentials.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Create an &amp;lt;code&amp;gt;OidcLogin&amp;lt;/code&amp;gt; component that calls &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; on mount.&lt;br /&gt;
* Render a &amp;lt;code&amp;gt;Form.Select&amp;lt;/code&amp;gt; dropdown with a disabled &amp;quot;Sign in with...&amp;quot; default option.&lt;br /&gt;
* If the request fails or returns empty, render nothing (no error, no placeholder).&lt;br /&gt;
* Existing login form remains unchanged and fully functional.&lt;br /&gt;
* Add component tests for rendering with providers and graceful fallback.&lt;br /&gt;
&lt;br /&gt;
=== Story 7: Frontend — Initiate OIDC Flow ===&lt;br /&gt;
'''As a''' user, '''I want''' selecting a provider from the dropdown to start the login flow, '''so that''' I am redirected to my school's login page.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* On selection change, &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;/auth/client-select&amp;lt;/code&amp;gt; with the provider id.&lt;br /&gt;
* On success, redirect the browser to the returned authorization URL via &amp;lt;code&amp;gt;window.location.href&amp;lt;/code&amp;gt;.&lt;br /&gt;
* On failure, log the error to the console.&lt;br /&gt;
* Add component tests for the redirect and error handling.&lt;br /&gt;
&lt;br /&gt;
=== Story 8: Frontend — Callback Route and Login Completion ===&lt;br /&gt;
'''As a''' user, '''I want''' to be logged in automatically after authenticating with my school, '''so that''' I don't have to take any additional steps.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add a &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt; route in the React router pointing to the &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt; component.&lt;br /&gt;
* Extract &amp;lt;code&amp;gt;code&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; from query parameters and &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; them to &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt;.&lt;br /&gt;
* If the query parameters contain an &amp;lt;code&amp;gt;error&amp;lt;/code&amp;gt; param (e.g. user denied consent), display the error via the alert slice and redirect to login without calling the backend.&lt;br /&gt;
* On success: call &amp;lt;code&amp;gt;setAuthToken&amp;lt;/code&amp;gt;, persist session to localStorage, dispatch &amp;lt;code&amp;gt;authenticationActions.setAuthentication&amp;lt;/code&amp;gt;, and redirect to the dashboard — mirroring the existing password login flow.&lt;br /&gt;
* On failure: display an error message via the alert slice and redirect to the login page.&lt;br /&gt;
* Show a &amp;quot;Completing login...&amp;quot; message while the token exchange is in progress.&lt;br /&gt;
* Add component tests for success, provider error, and backend error scenarios.&lt;br /&gt;
&lt;br /&gt;
=== Story 9: Backend — Tests ===&lt;br /&gt;
'''As a''' developer, '''I want''' test coverage for the OIDC backend, '''so that''' I have confidence the endpoints and models work correctly.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add request specs for &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Mock the identity provider's discovery, token, and JWKS endpoints to avoid external calls in tests.&lt;br /&gt;
* Cover error scenarios: expired state, invalid state, unknown provider, no matching user, invalid ID token.&lt;br /&gt;
* Add model specs for &amp;lt;code&amp;gt;AuthRequest&amp;lt;/code&amp;gt; covering creation, uniqueness, expiry scope, and cleanup.&lt;br /&gt;
* Add unit tests for &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; covering loading, validation, and missing key detection.&lt;br /&gt;
* Verify the existing &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; is unaffected.&lt;br /&gt;
&lt;br /&gt;
=== Story 10: Frontend — Tests ===&lt;br /&gt;
'''As a''' developer, '''I want''' test coverage for the OIDC frontend components, '''so that''' I have confidence the login flow and callback work correctly.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add component tests for &amp;lt;code&amp;gt;OidcLogin&amp;lt;/code&amp;gt;: renders dropdown when providers are returned, renders nothing on empty or failed response, triggers &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; on selection.&lt;br /&gt;
* Add component tests for &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt;: posts code and state to backend on mount, redirects to dashboard on success, displays error and redirects to login on failure, handles IdP error parameter without calling the backend.&lt;br /&gt;
* Mock axios calls to avoid external requests in tests.&lt;br /&gt;
* Verify the existing login page renders and functions correctly with and without the &amp;lt;code&amp;gt;OidcLogin&amp;lt;/code&amp;gt; component.&lt;br /&gt;
&lt;br /&gt;
=== Additional Refactoring Stories ===&lt;br /&gt;
&lt;br /&gt;
==== Story 11: Backend — Unified Session Response ====&lt;br /&gt;
'''As a''' developer, '''I want''' the session object returned to the frontend clearly defined and reusable by all login flows, '''so that''' the frontend can rely on a consistent response shape regardless of authentication method.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Extract the JWT payload construction and token issuance logic from &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;OidcLoginController#callback&amp;lt;/code&amp;gt; into a shared method on the &amp;lt;code&amp;gt;User&amp;lt;/code&amp;gt; model (e.g. &amp;lt;code&amp;gt;user.generate_jwt&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Define a consistent response structure (e.g. &amp;lt;code&amp;gt;{ token, user: { id, name, full_name, role, institution_id } }&amp;lt;/code&amp;gt;) and use it in both controllers.&lt;br /&gt;
* Update the existing password login endpoint to use the shared method without changing its external response shape.&lt;br /&gt;
* Add or update request specs for both login endpoints to assert the response structure matches.&lt;br /&gt;
&lt;br /&gt;
==== Story 12: Frontend — Externalize Hardcoded Configuration ====&lt;br /&gt;
'''As a''' developer, '''I want''' all hardcoded values moved to isolated configuration files, '''so that''' environment-specific settings can be changed without code modifications.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Move the frontend API base URL to an environment variable or shared config file (e.g. &amp;lt;code&amp;gt;.env&amp;lt;/code&amp;gt; with &amp;lt;code&amp;gt;REACT_APP_API_URL&amp;lt;/code&amp;gt;) and replace all hardcoded references.&lt;br /&gt;
* Ensure all existing tests continue to pass after the extraction.&lt;br /&gt;
&lt;br /&gt;
==== Story 13: Backend — Swagger Documentation for Provider Endpoints ====&lt;br /&gt;
'''As a''' developer, '''I want''' the OIDC provider endpoints documented in Swagger, '''so that''' frontend developers and future contributors can understand the API contract without reading the source code.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add Swagger/OpenAPI annotations for &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Document request parameters, response schemas (including success and error shapes), and HTTP status codes for each endpoint.&lt;br /&gt;
* Include example request and response payloads.&lt;br /&gt;
* Verify the endpoints appear correctly in the generated Swagger UI.&lt;br /&gt;
&lt;br /&gt;
==== Story 14: Backend — Cleanup Expired Auth Requests ====&lt;br /&gt;
'''As a''' developer, '''I want''' expired auth request rows cleaned up automatically, '''so that''' the table does not grow unbounded from abandoned login attempts.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add a recurring ActiveJob solid_queue &amp;lt;code&amp;gt;auth_requests:cleanup&amp;lt;/code&amp;gt; that calls &amp;lt;code&amp;gt;AuthRequest.cleanup_expired&amp;lt;/code&amp;gt;.&lt;br /&gt;
* The task deletes all rows with &amp;lt;code&amp;gt;created_at&amp;lt;/code&amp;gt; older than 5 minutes.&lt;br /&gt;
* Schedule the task to run periodically (e.g. 24 hours).&lt;br /&gt;
* Add a test verifying that only expired rows are deleted.&lt;br /&gt;
&lt;br /&gt;
== Demo ==&lt;br /&gt;
&lt;br /&gt;
todo add screenshots of oidc login at each step&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2618._Support_OIDC_Logins&amp;diff=167954</id>
		<title>CSC/ECE 517 Spring 2026 - E2618. Support OIDC Logins</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2618._Support_OIDC_Logins&amp;diff=167954"/>
		<updated>2026-04-14T21:38:04Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Purpose */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Purpose ==&lt;br /&gt;
By integrating [[OIDC|https://openid.net/developers/how-connect-works/]] login, users can authenticate using their existing university credentials, providing a familiar and streamlined login experience. Traditional username and password login will continue to be supported alongside OIDC, allowing users to choose their preferred authentication method.&lt;br /&gt;
&lt;br /&gt;
== Requirements ==&lt;br /&gt;
=== Authentication Flow ===&lt;br /&gt;
Users select a provider from a dropdown on the login page that redirects them to the school's OIDC provider, authenticates them, and redirects back to the application with a valid session. The frontend fetches available providers from the backend via &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; and renders them dynamically in a dropdown. On provider selection, the frontend posts to the backend, which returns an authorization URL. After the user completes the OIDC flow, the IdP redirects back to the frontend callback, which posts the authorization code and state to the backend to complete login.&lt;br /&gt;
&lt;br /&gt;
=== Session Management ===&lt;br /&gt;
Issue and maintain a local application session (JWT) after successful OIDC authentication, using the same &amp;lt;code&amp;gt;JsonWebToken&amp;lt;/code&amp;gt; class and payload structure as the existing password login. Refresh token grant flow will not be considered at this time (since session is managed by the application).&lt;br /&gt;
&lt;br /&gt;
=== Account Linking ===&lt;br /&gt;
Match the authenticated user's email from the ID token to an existing local account. No dedicated account linking table or just-in-time account creation will be built at this time. If no matching local account is found, an error is returned.&lt;br /&gt;
&lt;br /&gt;
=== Configuration ===&lt;br /&gt;
OIDC provider configurations (display name, scopes, endpoints) are defined in a YAML config file (&amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt;). Client credentials (client ID, client secret) are stored in environment variables and injected via ERB. Providers that support OIDC discovery have their endpoints and JWKS keys fetched automatically. The system supports multiple OIDC provider configurations simultaneously. The configuration is validated at boot via an initializer, surfacing missing values immediately on deploy rather than at login time.&lt;br /&gt;
&lt;br /&gt;
=== State Management ===&lt;br /&gt;
OIDC state, nonce, and PKCE code verifier are stored server-side in an &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; database table (via ActiveRecord) rather than in session cookies. This avoids cross-origin cookie issues between the separate frontend and backend. Rows are expired after 5 minutes and deleted after use. This table is named generically to support future federated auth protocols such as SAML. Note that many OIDC libraries (including &amp;lt;code&amp;gt;omniauth_openid_connect&amp;lt;/code&amp;gt;) use cookies to track state; due to SameSite restrictions on cross-origin requests, this approach leads to instability with a separated frontend and backend and should be avoided.&lt;br /&gt;
&lt;br /&gt;
=== Logout ===&lt;br /&gt;
Logout will not be impacted.&lt;br /&gt;
&lt;br /&gt;
=== Error Handling ===&lt;br /&gt;
Gracefully handle failures such as no local account matching the authenticated email, expired or invalid state parameters, token exchange errors, ID token verification errors, and user-denied consent at the IdP.&lt;br /&gt;
&lt;br /&gt;
=== Security ===&lt;br /&gt;
Use the Authorization Code flow with the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; Ruby gem (by nov). Validate the ID token signature and claims via JWKS keys from the provider's discovery document. Enforce a &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; parameter to prevent CSRF and a &amp;lt;code&amp;gt;nonce&amp;lt;/code&amp;gt; to prevent replay attacks. PKCE (code verifier and code challenge) is always included in the authorization request and token exchange; providers that support it will enforce it, and providers that do not will ignore the extra parameters. The backend is a confidential client and always authenticates with both client secret and PKCE.&lt;br /&gt;
&lt;br /&gt;
=== Testing ===&lt;br /&gt;
Backend and frontend are tested independently. Backend request specs use WebMock to stub the identity provider's discovery, token, and JWKS endpoints, allowing the full controller logic (state management, token exchange, ID token verification, user matching) to be tested without external dependencies. Frontend component tests mock axios calls to verify rendering, dropdown behavior, callback handling, and error display. End-to-end testing across both systems with a live identity provider is not planned at this time, as it would require standing up a mock IdP server (e.g. Keycloak or mock-oauth2-server), which is beyond the scope of the existing test infrastructure. The full OIDC login flow will be manually verified against Google's OIDC provider in a local development environment and demonstrated as needed.&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
[[File:OIDC Provider-2026-04-06-223511.png|1000px]]&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
* '''Boot (Step 0):''' Load provider configurations from &amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt; with secrets injected from environment variables via ERB. Each provider entry defines a display name, scopes, issuer, client credentials, and redirect URI. The &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; class validates that all required keys are present at boot via &amp;lt;code&amp;gt;config/initializers/oidc.rb&amp;lt;/code&amp;gt;. For providers with &amp;lt;code&amp;gt;discovery: true&amp;lt;/code&amp;gt;, the &amp;lt;code&amp;gt;.well-known/openid-configuration&amp;lt;/code&amp;gt; document is fetched using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem to resolve the authorization endpoint, token endpoint, userinfo endpoint, and JWKS keys. Discovery results are not aggressively cached to allow for key rotation; on signature verification failure, keys are re-fetched and verification is retried once.&lt;br /&gt;
* '''Provider List (Step 1):''' Expose a &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; endpoint that returns a JSON array of &amp;lt;code&amp;gt;{ id, name }&amp;lt;/code&amp;gt; from &amp;lt;code&amp;gt;OidcConfig.public_list&amp;lt;/code&amp;gt;. No secrets or endpoint details are included in this response.&lt;br /&gt;
* '''Client Select (Step 2):''' Expose a &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; endpoint that accepts a provider id. Generate a cryptographically random state and nonce via &amp;lt;code&amp;gt;SecureRandom.hex(32)&amp;lt;/code&amp;gt;, and a PKCE code verifier via &amp;lt;code&amp;gt;SecureRandom.urlsafe_base64(64)&amp;lt;/code&amp;gt; with a SHA256 code challenge. Insert a row into the &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; table containing the state, nonce, code verifier, provider id, and creation timestamp. Construct the authorization URL using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem's &amp;lt;code&amp;gt;authorization_uri&amp;lt;/code&amp;gt; method and return it to the frontend.&lt;br /&gt;
* '''Callback (Step 4):''' Expose a &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt; endpoint (and a temporary &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; for direct IdP redirect during backend-only testing) that accepts the authorization code and state. Look up the matching &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; row by state, rejecting the request if no row is found or if the row is older than 5 minutes. Delete the row to prevent reuse. Using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem, exchange the authorization code for tokens via &amp;lt;code&amp;gt;access_token!&amp;lt;/code&amp;gt; with the stored code verifier. Decode the ID token using &amp;lt;code&amp;gt;OpenIDConnect::ResponseObject::IdToken.decode&amp;lt;/code&amp;gt; against the provider's JWKS keys, and verify the issuer, client_id, and nonce via &amp;lt;code&amp;gt;id_token.verify!&amp;lt;/code&amp;gt;. Extract the user's email from the ID token claims and look up a matching local user. If a match is found, issue a session JWT using the same &amp;lt;code&amp;gt;JsonWebToken.encode&amp;lt;/code&amp;gt; method and payload structure as the existing &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; action. If no match is found, return a 404 error indicating no local account exists for that email.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
* '''Login Page (Step 1):''' On page load, the &amp;lt;code&amp;gt;OidcLogin&amp;lt;/code&amp;gt; component calls &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; and renders a dropdown (&amp;lt;code&amp;gt;Form.Select&amp;lt;/code&amp;gt;) for each configured provider below the existing username and password form. If the request fails or returns empty, the component renders nothing and the standard login form remains available and unaffected. No loading state is shown to avoid visual disruption when no providers are configured.&lt;br /&gt;
* '''Initiate Login (Step 2):''' When the user selects a provider from the dropdown, &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;/auth/client-select&amp;lt;/code&amp;gt; with the selected provider id. On success, redirect the browser to the returned authorization URL via &amp;lt;code&amp;gt;window.location.href&amp;lt;/code&amp;gt;. The user then authenticates with the identity provider and is redirected back to the frontend callback route.&lt;br /&gt;
* '''Callback (Step 4):''' The &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt; page component handles the redirect back from the identity provider at &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt;. It extracts the authorization code and state from the query parameters and &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt;s them to &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt;. If the query parameters contain an &amp;lt;code&amp;gt;error&amp;lt;/code&amp;gt; parameter instead of a code (e.g. the user denied consent), the error is displayed without calling the backend and the user is redirected to the login page.&lt;br /&gt;
* '''Login Complete (Step 5):''' On a successful callback response, store the session JWT via &amp;lt;code&amp;gt;setAuthToken&amp;lt;/code&amp;gt;, update the Redux auth state via &amp;lt;code&amp;gt;authenticationActions.setAuthentication&amp;lt;/code&amp;gt;, persist the session to localStorage, and redirect the user to the dashboard. This mirrors the existing password login flow exactly. On failure, display an error alert and redirect to the login page.&lt;br /&gt;
* The existing username and password login flow remains unchanged and fully functional.&lt;br /&gt;
&lt;br /&gt;
=== Design Patterns ===&lt;br /&gt;
The implementation uses the '''Strategy pattern''' for provider configuration. Each OIDC provider is defined declaratively in YAML with its own credentials, scopes, and endpoints, while the controller logic remains provider-agnostic. Adding a new identity provider requires only a new configuration block and environment variables, with no code changes.&lt;br /&gt;
&lt;br /&gt;
=== Schema ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; table stores temporary OIDC login state. Each row represents a single in-progress login attempt and is deleted after use or expiry.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Column !! Type !! Constraints !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt; || bigint || primary key || Row identifier&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; || string || unique, indexed || CSRF protection; used to look up the request on callback&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;nonce&amp;lt;/code&amp;gt; || string || not null || Replay attack prevention; verified against the ID token claim&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;code_verifier&amp;lt;/code&amp;gt; || string || not null || PKCE secret; sent to the token endpoint to prove the same party initiated the flow&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;provider&amp;lt;/code&amp;gt; || string || not null || Which OIDC provider config to use on callback&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;created_at&amp;lt;/code&amp;gt; || datetime || not null || Used to expire rows older than 5 minutes&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
No foreign keys or associations to other tables. This table is intentionally generic to support future federated auth protocols such as SAML.&lt;br /&gt;
&lt;br /&gt;
== Library Choice ==&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem (by nov, [https://github.com/nov/openid_connect github.com/nov/openid_connect]) was chosen over &amp;lt;code&amp;gt;omniauth_openid_connect&amp;lt;/code&amp;gt; for the following reasons:&lt;br /&gt;
&lt;br /&gt;
* '''No cookie/session dependency:''' &amp;lt;code&amp;gt;omniauth_openid_connect&amp;lt;/code&amp;gt; stores state and nonce in the server-side session via cookies. With a separate frontend and backend on different origins, session cookies are not reliably shared due to SameSite restrictions. Using &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; directly allows state management via the database instead.&lt;br /&gt;
* '''Explicit control:''' The gem provides building blocks (discovery, client construction, token exchange, ID token verification) without middleware magic. Each step in the OIDC flow is visible in the controller code.&lt;br /&gt;
* '''Lightweight:''' No OmniAuth middleware stack or Rack integration required. The gem handles the protocol; the application handles routing and state.&lt;br /&gt;
* '''Actively maintained:''' The gem is OpenID Foundation certified and used by 2,700+ projects on GitHub.&lt;br /&gt;
&lt;br /&gt;
The tradeoff is approximately 10 additional lines of code for state management (generating and storing state/nonce/PKCE in the &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; table), which is minimal compared to the complexity of debugging cross-origin cookie issues.&lt;br /&gt;
&lt;br /&gt;
== File Diffs and Additions (Prototype and subject to change) ==&lt;br /&gt;
&lt;br /&gt;
=== Backend (Rails) ===&lt;br /&gt;
([https://github.com/johnmweisz/reimplementation-back-end/tree/2618-oidc-login branch: 2618-oidc-login])&lt;br /&gt;
&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/app/controllers/oidc_login_controller.rb app/controllers/oidc_login_controller.rb]  — Controller with providers, client_select, and callback actions&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/app/models/oidc_config.rb app/models/oidc_config.rb]                 — YAML config loader with validation&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/app/models/auth_request.rb app/models/auth_request.rb]                — ActiveRecord model for state/nonce/PKCE storage&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/config/oidc_providers.yml config/oidc_providers.yml]                 — Provider configuration (ERB for env var injection)&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/config/initializers/oidc.rb config/initializers/oidc.rb]               — Boot-time config validation&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/db/migrate/20260407003623_create_auth_requests.rb db/migrate/*_create_auth_requests.rb]      — Migration for auth_requests table&lt;br /&gt;
&lt;br /&gt;
=== Backend (RSpec) Tests ===&lt;br /&gt;
([https://github.com/johnmweisz/reimplementation-back-end/tree/2618-oidc-login branch: 2618-oidc-login])&lt;br /&gt;
&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/models/oidc_config_spec.rb spec/models/oidc_config_spec.rb]          — Config loading, validation, missing keys, public_list secrets exclusion, provider lookup&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/models/auth_request_spec.rb spec/models/auth_request_spec.rb]         — State uniqueness, expiry scope, cleanup of stale rows&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/requests/oidc_login_spec.rb spec/requests/oidc_login_spec.rb]         — Endpoint tests covering:&lt;br /&gt;
&lt;br /&gt;
'''GET /auth/providers'''&lt;br /&gt;
* Returns provider list with id and name only, no secrets leaked&lt;br /&gt;
* '''TODO''' Returns empty array when no providers configured&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/client-select'''&lt;br /&gt;
* '''TODO''' Returns authorization URL with expected query parameters (client_id, redirect_uri, scope, state, nonce, code_challenge)&lt;br /&gt;
* '''TODO''' Creates an auth_requests row&lt;br /&gt;
* '''TODO''' Returns 404 for unknown provider&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — Happy Path'''&lt;br /&gt;
* '''TODO''' Exchanges valid code and state for a session JWT with same payload structure as password login&lt;br /&gt;
* '''TODO''' Deletes the auth_requests row after successful use&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — CSRF Protection (State Validation)'''&lt;br /&gt;
* '''TODO''' Rejects unknown state (422)&lt;br /&gt;
* '''TODO''' Rejects expired state older than 5 minutes (422)&lt;br /&gt;
* '''TODO''' Rejects already-consumed state / replay (422)&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — Replay Protection (Nonce Validation)'''&lt;br /&gt;
* '''TODO''' Rejects ID token with mismatched nonce&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — Token Verification'''&lt;br /&gt;
* '''TODO''' Rejects ID token with invalid signature&lt;br /&gt;
* '''TODO''' Rejects ID token with mismatched issuer&lt;br /&gt;
* '''TODO''' Rejects ID token with mismatched audience (client_id)&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — User Matching'''&lt;br /&gt;
* '''TODO''' Returns 404 when no local user matches the email&lt;br /&gt;
* '''TODO''' Matches correct user when multiple users exist&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — PKCE'''&lt;br /&gt;
* '''TODO''' Sends code_verifier to the token endpoint during code exchange&lt;br /&gt;
&lt;br /&gt;
=== Frontend (React) ===&lt;br /&gt;
([https://github.com/johnmweisz/reimplementation-front-end/tree/2618-oidc-login branch: 2618-oidc-login])&lt;br /&gt;
&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/components/OidcLogin/OidcLogin.tsx src/components/OidcLogin/OidcLogin.tsx]     — Provider dropdown component&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/pages/OidcCallback/OidcCallback.tsx src/pages/OidcCallback/OidcCallback.tsx]    — Callback page handling code exchange&lt;br /&gt;
&lt;br /&gt;
=== Frontend (Vitest) Tests ===&lt;br /&gt;
([https://github.com/johnmweisz/reimplementation-front-end/tree/2618-oidc-login branch: 2618-oidc-login])&lt;br /&gt;
&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/components/OidcLogin/OidcLogin.test.tsx src/components/OidcLogin/OidcLogin.test.tsx]     — Provider dropdown component tests&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/pages/OidcCallback/OidcCallback.test.tsx src/pages/OidcCallback/OidcCallback.test.tsx]    — Callback page tests&lt;br /&gt;
&lt;br /&gt;
'''OidcLogin Component'''&lt;br /&gt;
* '''TODO''' Renders dropdown with providers when GET /auth/providers returns a non-empty list&lt;br /&gt;
* '''TODO''' Renders nothing when GET /auth/providers returns an empty array&lt;br /&gt;
* '''TODO''' Renders nothing when GET /auth/providers fails&lt;br /&gt;
* '''TODO''' Calls POST /auth/client-select with selected provider id on dropdown change&lt;br /&gt;
* '''TODO''' Redirects browser to returned authorization URL on successful client-select&lt;br /&gt;
* '''TODO''' Does not redirect when client-select fails&lt;br /&gt;
* '''TODO''' Existing login form remains visible and functional alongside the dropdown&lt;br /&gt;
&lt;br /&gt;
'''OidcCallback Component'''&lt;br /&gt;
* '''TODO''' Posts code and state to POST /auth/callback on mount&lt;br /&gt;
* '''TODO''' Stores session JWT and dispatches auth state on success&lt;br /&gt;
* '''TODO''' Redirects to dashboard on successful login&lt;br /&gt;
* '''TODO''' Displays error alert and redirects to login on backend failure (e.g. no matching account)&lt;br /&gt;
* '''TODO''' Displays error and redirects to login when IdP returns an error parameter (e.g. user denied consent) without calling the backend&lt;br /&gt;
* '''TODO''' Redirects to login when code or state query parameters are missing&lt;br /&gt;
* '''TODO''' Shows &amp;quot;Completing login...&amp;quot; message while request is in flight&lt;br /&gt;
&lt;br /&gt;
=== Routes ===&lt;br /&gt;
 GET  /auth/providers      → oidc_login#providers&lt;br /&gt;
 POST /auth/client-select  → oidc_login#client_select&lt;br /&gt;
 POST /auth/callback       → oidc_login#callback&lt;br /&gt;
 GET  /auth/callback       → React OidcCallback component&lt;br /&gt;
&lt;br /&gt;
== Development Stories (Planning) ==&lt;br /&gt;
&lt;br /&gt;
=== Story 1: Backend — OIDC Provider Configuration ===&lt;br /&gt;
'''As a''' developer, '''I want''' provider configurations loaded from a YAML file at boot, '''so that''' new OIDC providers can be added without code changes.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Create &amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt; with ERB support for injecting secrets from environment variables.&lt;br /&gt;
* Create an &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; class that loads and validates the YAML at boot, exposing methods to list providers and look up a provider by id.&lt;br /&gt;
* Define the config file path as a constant (&amp;lt;code&amp;gt;CONFIG_FILE&amp;lt;/code&amp;gt;) for clarity.&lt;br /&gt;
* Validate required keys: &amp;lt;code&amp;gt;display_name&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;issuer&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;client_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;client_secret&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;redirect_uri&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;scopes&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Raise a clear boot-time error if required values are missing via &amp;lt;code&amp;gt;config/initializers/oidc.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Add unit tests for config loading, validation, and missing key detection.&lt;br /&gt;
&lt;br /&gt;
=== Story 2: Backend — Auth Requests Table ===&lt;br /&gt;
'''As a''' developer, '''I want''' a database-backed store for OIDC state, nonce, and PKCE values, '''so that''' the backend can validate callbacks without relying on cookies.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Generate an ActiveRecord migration for &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; with columns: &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; (string, indexed, unique), &amp;lt;code&amp;gt;nonce&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;code_verifier&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;provider&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;created_at&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Create the &amp;lt;code&amp;gt;AuthRequest&amp;lt;/code&amp;gt; model with a uniqueness validation on state, an &amp;lt;code&amp;gt;expired&amp;lt;/code&amp;gt; scope, and a &amp;lt;code&amp;gt;cleanup_expired&amp;lt;/code&amp;gt; class method.&lt;br /&gt;
* Add a rake task or scheduled job to periodically clean up expired rows.&lt;br /&gt;
* Add unit tests for creation, lookup, expiry, and deletion.&lt;br /&gt;
&lt;br /&gt;
=== Story 3: Backend — Provider List Endpoint ===&lt;br /&gt;
'''As a''' frontend developer, '''I want''' a &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; endpoint, '''so that''' the login page can dynamically render provider options.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Create a controller action that returns a JSON array of &amp;lt;code&amp;gt;{ id, name }&amp;lt;/code&amp;gt; from &amp;lt;code&amp;gt;OidcConfig.public_list&amp;lt;/code&amp;gt;.&lt;br /&gt;
* No secrets or endpoint URLs are included in the response.&lt;br /&gt;
* Add a request spec covering the response format and a case with multiple providers.&lt;br /&gt;
&lt;br /&gt;
=== Story 4: Backend — Client Select Endpoint ===&lt;br /&gt;
'''As a''' frontend developer, '''I want''' a &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; endpoint that returns an authorization URL, '''so that''' the frontend can redirect the user to the identity provider.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Accept a &amp;lt;code&amp;gt;provider&amp;lt;/code&amp;gt; param and look up the provider config.&lt;br /&gt;
* Generate cryptographically random state, nonce, and PKCE code verifier/challenge.&lt;br /&gt;
* Insert a row into &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Construct and return the authorization URL with client_id, redirect_uri, scopes, state, nonce, and code_challenge.&lt;br /&gt;
* Return a 404 if the provider is unknown.&lt;br /&gt;
* Add request specs covering the happy path, unknown provider, and that the auth_requests row is created.&lt;br /&gt;
&lt;br /&gt;
=== Story 5: Backend — Callback Endpoint ===&lt;br /&gt;
'''As a''' frontend developer, '''I want''' a &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt; endpoint that exchanges the authorization code for tokens and returns a session, '''so that''' the user is logged in after completing the OIDC flow.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Accept &amp;lt;code&amp;gt;code&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; params.&lt;br /&gt;
* Look up and delete the matching &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; row. Reject if not found or expired.&lt;br /&gt;
* Exchange the code for tokens using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem with the stored code_verifier.&lt;br /&gt;
* Verify the ID token signature (JWKS), issuer, client_id, and nonce.&lt;br /&gt;
* Extract the email claim and match to an existing local user.&lt;br /&gt;
* On match: issue a session JWT using the same payload structure as &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; and return it with the user profile.&lt;br /&gt;
* On no match: return a 404 error indicating no local account was found.&lt;br /&gt;
* Handle &amp;lt;code&amp;gt;ActiveRecord::RecordNotFound&amp;lt;/code&amp;gt; (invalid/expired state) and &amp;lt;code&amp;gt;InvalidToken&amp;lt;/code&amp;gt; (verification failure) exceptions.&lt;br /&gt;
* Add request specs covering the happy path, expired state, invalid state, and no matching user.&lt;br /&gt;
&lt;br /&gt;
=== Story 6: Frontend — Provider Dropdown on Login Page ===&lt;br /&gt;
'''As a''' user, '''I want''' to see a provider dropdown on the login page, '''so that''' I can authenticate with my school credentials.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Create an &amp;lt;code&amp;gt;OidcLogin&amp;lt;/code&amp;gt; component that calls &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; on mount.&lt;br /&gt;
* Render a &amp;lt;code&amp;gt;Form.Select&amp;lt;/code&amp;gt; dropdown with a disabled &amp;quot;Sign in with...&amp;quot; default option.&lt;br /&gt;
* If the request fails or returns empty, render nothing (no error, no placeholder).&lt;br /&gt;
* Existing login form remains unchanged and fully functional.&lt;br /&gt;
* Add component tests for rendering with providers and graceful fallback.&lt;br /&gt;
&lt;br /&gt;
=== Story 7: Frontend — Initiate OIDC Flow ===&lt;br /&gt;
'''As a''' user, '''I want''' selecting a provider from the dropdown to start the login flow, '''so that''' I am redirected to my school's login page.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* On selection change, &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;/auth/client-select&amp;lt;/code&amp;gt; with the provider id.&lt;br /&gt;
* On success, redirect the browser to the returned authorization URL via &amp;lt;code&amp;gt;window.location.href&amp;lt;/code&amp;gt;.&lt;br /&gt;
* On failure, log the error to the console.&lt;br /&gt;
* Add component tests for the redirect and error handling.&lt;br /&gt;
&lt;br /&gt;
=== Story 8: Frontend — Callback Route and Login Completion ===&lt;br /&gt;
'''As a''' user, '''I want''' to be logged in automatically after authenticating with my school, '''so that''' I don't have to take any additional steps.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add a &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt; route in the React router pointing to the &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt; component.&lt;br /&gt;
* Extract &amp;lt;code&amp;gt;code&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; from query parameters and &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; them to &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt;.&lt;br /&gt;
* If the query parameters contain an &amp;lt;code&amp;gt;error&amp;lt;/code&amp;gt; param (e.g. user denied consent), display the error via the alert slice and redirect to login without calling the backend.&lt;br /&gt;
* On success: call &amp;lt;code&amp;gt;setAuthToken&amp;lt;/code&amp;gt;, persist session to localStorage, dispatch &amp;lt;code&amp;gt;authenticationActions.setAuthentication&amp;lt;/code&amp;gt;, and redirect to the dashboard — mirroring the existing password login flow.&lt;br /&gt;
* On failure: display an error message via the alert slice and redirect to the login page.&lt;br /&gt;
* Show a &amp;quot;Completing login...&amp;quot; message while the token exchange is in progress.&lt;br /&gt;
* Add component tests for success, provider error, and backend error scenarios.&lt;br /&gt;
&lt;br /&gt;
=== Story 9: Backend — Tests ===&lt;br /&gt;
'''As a''' developer, '''I want''' test coverage for the OIDC backend, '''so that''' I have confidence the endpoints and models work correctly.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add request specs for &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Mock the identity provider's discovery, token, and JWKS endpoints to avoid external calls in tests.&lt;br /&gt;
* Cover error scenarios: expired state, invalid state, unknown provider, no matching user, invalid ID token.&lt;br /&gt;
* Add model specs for &amp;lt;code&amp;gt;AuthRequest&amp;lt;/code&amp;gt; covering creation, uniqueness, expiry scope, and cleanup.&lt;br /&gt;
* Add unit tests for &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; covering loading, validation, and missing key detection.&lt;br /&gt;
* Verify the existing &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; is unaffected.&lt;br /&gt;
&lt;br /&gt;
=== Story 10: Frontend — Tests ===&lt;br /&gt;
'''As a''' developer, '''I want''' test coverage for the OIDC frontend components, '''so that''' I have confidence the login flow and callback work correctly.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add component tests for &amp;lt;code&amp;gt;OidcLogin&amp;lt;/code&amp;gt;: renders dropdown when providers are returned, renders nothing on empty or failed response, triggers &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; on selection.&lt;br /&gt;
* Add component tests for &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt;: posts code and state to backend on mount, redirects to dashboard on success, displays error and redirects to login on failure, handles IdP error parameter without calling the backend.&lt;br /&gt;
* Mock axios calls to avoid external requests in tests.&lt;br /&gt;
* Verify the existing login page renders and functions correctly with and without the &amp;lt;code&amp;gt;OidcLogin&amp;lt;/code&amp;gt; component.&lt;br /&gt;
&lt;br /&gt;
=== Additional Refactoring Stories ===&lt;br /&gt;
&lt;br /&gt;
==== Story 11: Backend — Unified Session Response ====&lt;br /&gt;
'''As a''' developer, '''I want''' the session object returned to the frontend clearly defined and reusable by all login flows, '''so that''' the frontend can rely on a consistent response shape regardless of authentication method.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Extract the JWT payload construction and token issuance logic from &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;OidcLoginController#callback&amp;lt;/code&amp;gt; into a shared method on the &amp;lt;code&amp;gt;User&amp;lt;/code&amp;gt; model (e.g. &amp;lt;code&amp;gt;user.generate_jwt&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Define a consistent response structure (e.g. &amp;lt;code&amp;gt;{ token, user: { id, name, full_name, role, institution_id } }&amp;lt;/code&amp;gt;) and use it in both controllers.&lt;br /&gt;
* Update the existing password login endpoint to use the shared method without changing its external response shape.&lt;br /&gt;
* Add or update request specs for both login endpoints to assert the response structure matches.&lt;br /&gt;
&lt;br /&gt;
==== Story 12: Frontend — Externalize Hardcoded Configuration ====&lt;br /&gt;
'''As a''' developer, '''I want''' all hardcoded values moved to isolated configuration files, '''so that''' environment-specific settings can be changed without code modifications.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Move the frontend API base URL to an environment variable or shared config file (e.g. &amp;lt;code&amp;gt;.env&amp;lt;/code&amp;gt; with &amp;lt;code&amp;gt;REACT_APP_API_URL&amp;lt;/code&amp;gt;) and replace all hardcoded references.&lt;br /&gt;
* Ensure all existing tests continue to pass after the extraction.&lt;br /&gt;
&lt;br /&gt;
==== Story 13: Backend — Swagger Documentation for Provider Endpoints ====&lt;br /&gt;
'''As a''' developer, '''I want''' the OIDC provider endpoints documented in Swagger, '''so that''' frontend developers and future contributors can understand the API contract without reading the source code.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add Swagger/OpenAPI annotations for &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Document request parameters, response schemas (including success and error shapes), and HTTP status codes for each endpoint.&lt;br /&gt;
* Include example request and response payloads.&lt;br /&gt;
* Verify the endpoints appear correctly in the generated Swagger UI.&lt;br /&gt;
&lt;br /&gt;
==== Story 14: Backend — Cleanup Expired Auth Requests ====&lt;br /&gt;
'''As a''' developer, '''I want''' expired auth request rows cleaned up automatically, '''so that''' the table does not grow unbounded from abandoned login attempts.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add a recurring ActiveJob solid_queue &amp;lt;code&amp;gt;auth_requests:cleanup&amp;lt;/code&amp;gt; that calls &amp;lt;code&amp;gt;AuthRequest.cleanup_expired&amp;lt;/code&amp;gt;.&lt;br /&gt;
* The task deletes all rows with &amp;lt;code&amp;gt;created_at&amp;lt;/code&amp;gt; older than 5 minutes.&lt;br /&gt;
* Schedule the task to run periodically (e.g. 24 hours).&lt;br /&gt;
* Add a test verifying that only expired rows are deleted.&lt;br /&gt;
&lt;br /&gt;
== Demo ==&lt;br /&gt;
&lt;br /&gt;
todo add screenshots of oidc login at each step&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167776</id>
		<title>CSC/ECE 517 Spring 2026 - E2610. Teams hierarchy testing</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167776"/>
		<updated>2026-04-09T01:52:26Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Model Specs */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== About This Project ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2610 — Teams Hierarchy Testing&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Vihar Manojkumar Shah&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || Atharva Waingankar, Krisha Darji, Saladin Al-Bataineh&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Membership Join Model''' || &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
The Expertiza platform organises student collaboration through a structured class hierarchy dedicated to managing teams. Teams are the fundamental unit of participation: they allow students to reserve topics, collaborate on coursework, and submit assignments for evaluation.&lt;br /&gt;
&lt;br /&gt;
This project focuses on ensuring the correctness of the Team hierarchy and strengthening automated test coverage. The work emphasizes membership validity (enrolment-based participation), preventing duplicate membership across teams, enforcing capacity limits when configured, ensuring MentoredTeam uses duty-based mentor identification, and validating controller authorization and HTTP response behavior.&lt;br /&gt;
&lt;br /&gt;
== Background ==&lt;br /&gt;
&lt;br /&gt;
=== The Team Hierarchy ===&lt;br /&gt;
The hierarchy is composed of exactly four classes:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; — STI superclass providing shared associations and common behaviours&lt;br /&gt;
* &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt; — course-scoped teams that persist across a course&lt;br /&gt;
* &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; — assignment-scoped teams created for a single assignment&lt;br /&gt;
* &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt; — subclass of &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; with a designated mentor&lt;br /&gt;
&lt;br /&gt;
Two broad kinds of team exist:&lt;br /&gt;
&lt;br /&gt;
* '''Course teams''' (&amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;) persist throughout a course. In some instructional designs (e.g., Team-Based Learning), these teams are reused for multiple assignments.&lt;br /&gt;
* '''Assignment teams''' (&amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;) exist only for a single assignment. When mentors are used, assignment teams are instantiated as &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Class Hierarchy Diagram ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
                        ┌─────────────────┐&lt;br /&gt;
                        │      Team       │&lt;br /&gt;
                        │  (STI parent)   │&lt;br /&gt;
                        └────────┬────────┘&lt;br /&gt;
                                 │&lt;br /&gt;
               ┌─────────────────┴──────────────────┐&lt;br /&gt;
               │                                    │&lt;br /&gt;
      ┌────────┴────────┐                ┌──────────┴──────────┐&lt;br /&gt;
      │   CourseTeam    │                │   AssignmentTeam    │&lt;br /&gt;
      │ (course-scoped) │                │ (assignment-scoped) │&lt;br /&gt;
      └─────────────────┘                └──────────┬──────────┘&lt;br /&gt;
                                                    │&lt;br /&gt;
                                         ┌──────────┴──────────┐&lt;br /&gt;
                                         │    MentoredTeam     │&lt;br /&gt;
                                         │ (mentor via duty)   │&lt;br /&gt;
                                         └─────────────────────┘&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Subclass Responsibilities ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Class !! Scope !! Key Constraint !! Notes&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; || — || Shared membership/associations || Parent class for all team types (STI)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt; || Course || Member must be a course participant || Can be converted to/from assignment teams&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; || Assignment || Member must be an assignment participant || Capacity comes from &amp;lt;code&amp;gt;Assignment#max_team_size&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt; || Assignment || Mentor identified by participant duty || Uses &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; + participant &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Duty vs. Role Distinction ==&lt;br /&gt;
&lt;br /&gt;
A critical design point (and a core source of bugs in this area) is the separation between a user's '''role''' and a participant's '''duty'''.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Concept !! Definition !! Examples&lt;br /&gt;
|-&lt;br /&gt;
| '''Role''' || System-level permission level assigned to a user account || &amp;lt;code&amp;gt;Instructor&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Teaching Assistant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Student&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Duty''' || A function assigned to a participant ''within a specific team'' || &amp;lt;code&amp;gt;Submitter&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Reviewer&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Mentor&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
In Expertiza, mentors are not privileged system accounts; they are normal users assigned the duty of Mentor on one team. Therefore, mentor logic must use participant duty rather than user role.&lt;br /&gt;
&lt;br /&gt;
== Previous Implementation ==&lt;br /&gt;
&lt;br /&gt;
=== 1) MentoredTeam used role-based assumptions ===&lt;br /&gt;
The incorrect approach was to treat “mentor” as a system-level role check. This is incompatible with Expertiza’s domain model, where mentor is a team-level duty assigned to a participant.&lt;br /&gt;
&lt;br /&gt;
'''What we changed:''' MentoredTeam mentor assignment and mentor discovery use &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; and the participant’s &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== 2) Capacity enforcement was inconsistent / bypassable ===&lt;br /&gt;
Membership could be created through multiple paths (domain method, controllers, join-request acceptance). If enforcement only happened in one place, direct creation of &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt; could bypass capacity checks.&lt;br /&gt;
&lt;br /&gt;
'''What we changed:''' Capacity is enforced at both:&lt;br /&gt;
* the domain method level (&amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt;)&lt;br /&gt;
* the join model level (&amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt; validation backstop)&lt;br /&gt;
&lt;br /&gt;
Join-request acceptance also checks capacity inside a lock for race-safety.&lt;br /&gt;
&lt;br /&gt;
=== 3) TeamsController authorisation was not explicit ===&lt;br /&gt;
The authorisation framework existed (global before_action), but &amp;lt;code&amp;gt;TeamsController&amp;lt;/code&amp;gt; did not implement an explicit access policy. As a result, student actions could succeed where they should be forbidden.&lt;br /&gt;
&lt;br /&gt;
'''What we changed:''' TeamsController now defines &amp;lt;code&amp;gt;action_allowed?&amp;lt;/code&amp;gt;, and request specs verify correct &amp;lt;code&amp;gt;403 Forbidden&amp;lt;/code&amp;gt; behaviour.&lt;br /&gt;
&lt;br /&gt;
=== 4) CourseTeam capacity was assumed without schema support ===&lt;br /&gt;
The current schema does not provide a &amp;lt;code&amp;gt;max_team_size&amp;lt;/code&amp;gt; attribute for courses. Attempting to enforce CourseTeam capacity via &amp;lt;code&amp;gt;course.max_team_size&amp;lt;/code&amp;gt; results in runtime errors and breaks previously passing tests.&lt;br /&gt;
&lt;br /&gt;
'''What we changed:''' Capacity enforcement is based on assignment configuration (&amp;lt;code&amp;gt;Assignment#max_team_size&amp;lt;/code&amp;gt;). CourseTeams remain uncapped by default unless the schema is extended in the future.&lt;br /&gt;
&lt;br /&gt;
== Implementation Summary  ==&lt;br /&gt;
&lt;br /&gt;
=== 1) Enrollment-based membership ===&lt;br /&gt;
Membership is grounded in valid participation. A user can only be added if the appropriate participant record exists in the correct parent scope.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/team.rb&lt;br /&gt;
participant_type = is_a?(AssignmentTeam) ? AssignmentParticipant : CourseParticipant&lt;br /&gt;
participant_type.find_by(user_id: participant_or_user.id, parent_id: parent_id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2) Prevent duplicate membership across teams ===&lt;br /&gt;
The join model prevents a participant from appearing on multiple teams:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validates :participant_id, uniqueness: true&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A unique index migration reinforces this at the database level.&lt;br /&gt;
&lt;br /&gt;
=== 3) Capacity enforcement (configured via Assignment) ===&lt;br /&gt;
Capacity is enforced via the assignment’s &amp;lt;code&amp;gt;max_team_size&amp;lt;/code&amp;gt;. When at capacity, attempts to add are rejected with an error.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/team.rb&lt;br /&gt;
def full?&lt;br /&gt;
  max = max_size&lt;br /&gt;
  return false if max.blank?&lt;br /&gt;
  participants.count &amp;gt;= max&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/team.rb&lt;br /&gt;
return { success: false, error: &amp;quot;Unable to add participant: team is at full capacity.&amp;quot; } if full?&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 4) Join-model capacity backstop ===&lt;br /&gt;
Even direct join-table creation is blocked when a team is at capacity.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validate :team_not_full, on: :create&lt;br /&gt;
&lt;br /&gt;
def team_not_full&lt;br /&gt;
  return unless team&lt;br /&gt;
  max = team.max_size&lt;br /&gt;
  return if max.blank?&lt;br /&gt;
&lt;br /&gt;
  if team.participants.count &amp;gt;= max&lt;br /&gt;
    errors.add(:base, &amp;quot;Team is at full capacity (max #{max}).&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 5) Race-safe join request acceptance ===&lt;br /&gt;
Acceptance checks capacity while holding a lock to prevent two concurrent acceptances from overfilling a team.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/controllers/join_team_requests_controller.rb (accept)&lt;br /&gt;
ActiveRecord::Base.transaction do&lt;br /&gt;
  team.with_lock do&lt;br /&gt;
    if team.full?&lt;br /&gt;
      # reject accept&lt;br /&gt;
    end&lt;br /&gt;
    result = team.add_member(participant)&lt;br /&gt;
    # reject if result failed&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  @join_team_request.update!(reply_status: ACCEPTED)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 6) TeamsController authorisation and HTTP codes ===&lt;br /&gt;
TeamsController now declares an explicit policy:&lt;br /&gt;
&lt;br /&gt;
* Teaching staff (TA and above): allowed&lt;br /&gt;
* Students: cannot list all teams, cannot manage membership, can only view teams they belong to&lt;br /&gt;
&lt;br /&gt;
Request specs assert that restricted actions return &amp;lt;code&amp;gt;403&amp;lt;/code&amp;gt; with an error payload.&lt;br /&gt;
&lt;br /&gt;
== RSpec Test Coverage ==&lt;br /&gt;
&lt;br /&gt;
The test suite is organised into model specs (domain rules) and request specs (API behaviour + HTTP status codes).&lt;br /&gt;
&lt;br /&gt;
=== Model Specs ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Team membership + validations || &amp;lt;code&amp;gt;spec/models/team_spec.rb&amp;lt;/code&amp;gt; || Type/parent validation, membership checks, &amp;lt;code&amp;gt;full?&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;add_member&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Capacity behavior || &amp;lt;code&amp;gt;spec/models/team_capacity_spec.rb&amp;lt;/code&amp;gt; || AssignmentTeam capacity and error rejection&lt;br /&gt;
|-&lt;br /&gt;
| Association integrity || &amp;lt;code&amp;gt;spec/models/team_association_spec.rb&amp;lt;/code&amp;gt; || Parent linkage and membership scoping&lt;br /&gt;
|-&lt;br /&gt;
| Conversion behavior || &amp;lt;code&amp;gt;spec/models/team_conversion_spec.rb&amp;lt;/code&amp;gt; || CourseTeam ⇄ AssignmentTeam conversions and member copying&lt;br /&gt;
|-&lt;br /&gt;
| MentoredTeam duty behavior || &amp;lt;code&amp;gt;spec/models/mentored_team_spec.rb&amp;lt;/code&amp;gt; || Mentor duty assignment/removal and mentor lookup by duty&lt;br /&gt;
|-&lt;br /&gt;
| Join model constraints || &amp;lt;code&amp;gt;spec/models/teams_participant_spec.rb&amp;lt;/code&amp;gt; || Uniqueness, presence, and capacity backstop (no bypass)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Request Specs ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Endpoint Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Teams API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_controller_spec.rb&amp;lt;/code&amp;gt; || Index/show/members/add/remove + authorisation status codes&lt;br /&gt;
|-&lt;br /&gt;
| JoinTeamRequests API || &amp;lt;code&amp;gt;spec/requests/api/v1/join_team_requests_controller_spec.rb&amp;lt;/code&amp;gt; || Authorisation, create/accept/decline, capacity rejection and rollback behaviour&lt;br /&gt;
|-&lt;br /&gt;
| TeamsParticipants API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_participants_controller_spec.rb&amp;lt;/code&amp;gt; || Duty update authorisation, list/add/delete flows&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Important Example Tests  ===&lt;br /&gt;
&lt;br /&gt;
==== Capacity and size limits ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/models/team_capacity_spec.rb&lt;br /&gt;
it 'rejects member when team is full' do&lt;br /&gt;
  2.times { |i| team.add_member(make_participant(&amp;quot;full#{i}&amp;quot;)) }&lt;br /&gt;
  extra  = make_participant('extra')&lt;br /&gt;
&lt;br /&gt;
  result = team.add_member(extra)&lt;br /&gt;
&lt;br /&gt;
  expect(result[:success]).to be false&lt;br /&gt;
  expect(result[:error]).to match(/capacity/i)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: verifies domain-level capacity enforcement returns a structured failure and a meaningful error.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/models/teams_participant_spec.rb&lt;br /&gt;
describe 'capacity validation (team_not_full)' do&lt;br /&gt;
  it 'raises when creating beyond capacity' do&lt;br /&gt;
    assignment.update!(max_team_size: 1)&lt;br /&gt;
    team = AssignmentTeam.create!(name: 'Cap Team2', parent_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
    u1 = make_user('cap2_u1')&lt;br /&gt;
    p1 = AssignmentParticipant.create!(user: u1, parent_id: assignment.id, handle: u1.name)&lt;br /&gt;
    TeamsParticipant.create!(team: team, participant: p1, user: u1)&lt;br /&gt;
&lt;br /&gt;
    u2 = make_user('cap2_u2')&lt;br /&gt;
    p2 = AssignmentParticipant.create!(user: u2, parent_id: assignment.id, handle: u2.name)&lt;br /&gt;
&lt;br /&gt;
    expect {&lt;br /&gt;
      TeamsParticipant.create!(team: team, participant: p2, user: u2)&lt;br /&gt;
    }.to raise_error(ActiveRecord::RecordInvalid)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: ensures capacity cannot be bypassed by direct join-table creation.&lt;br /&gt;
&lt;br /&gt;
==== Join request acceptance rollback correctness ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/requests/api/v1/join_team_requests_controller_spec.rb&lt;br /&gt;
it 'does not change reply_status or add participant when team is full' do&lt;br /&gt;
  assignment.update!(max_team_size: 1)&lt;br /&gt;
&lt;br /&gt;
  patch &amp;quot;/join_team_requests/#{join_team_request.id}/accept&amp;quot;, headers: team_member_headers&lt;br /&gt;
&lt;br /&gt;
  expect(response).to have_http_status(:unprocessable_entity)&lt;br /&gt;
  expect(join_team_request.reload.reply_status).to eq('PENDING')&lt;br /&gt;
  expect(team1.participants.reload).not_to include(participant2)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: verifies the accept endpoint rejects at capacity and does not mutate state.&lt;br /&gt;
&lt;br /&gt;
==== TeamsController authorisation / HTTP status codes ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/requests/api/v1/teams_controller_spec.rb&lt;br /&gt;
it 'returns 403 for student on GET /teams' do&lt;br /&gt;
  student_token   = JsonWebToken.encode(id: other_user.id)&lt;br /&gt;
  student_headers = { Authorization: &amp;quot;Bearer #{student_token}&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  get '/teams', headers: student_headers&lt;br /&gt;
&lt;br /&gt;
  expect(response).to have_http_status(:forbidden)&lt;br /&gt;
  expect(JSON.parse(response.body)).to have_key('error')&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: ensures a student cannot list teams and receives &amp;lt;code&amp;gt;403&amp;lt;/code&amp;gt; with an error payload.&lt;br /&gt;
&lt;br /&gt;
==== MentoredTeam duty-based mentor behaviour ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/models/mentored_team_spec.rb&lt;br /&gt;
it 'identifies mentor by duty not by role' do&lt;br /&gt;
  mentor_duty&lt;br /&gt;
  participant = make_participant('duty_mentor')&lt;br /&gt;
  team.add_member(participant)&lt;br /&gt;
  team.assign_mentor(participant.user)&lt;br /&gt;
&lt;br /&gt;
  expect(team.send(:mentor)).to eq(participant.user)&lt;br /&gt;
  expect(participant.reload.duty).to eq(mentor_duty)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: proves mentor identification is duty-based, matching Expertiza’s domain model.&lt;br /&gt;
== Demo Video==&lt;br /&gt;
https://www.youtube.com/watch?v=KZh-Kwhduok&lt;br /&gt;
== Summary ==&lt;br /&gt;
This project strengthens the Teams hierarchy by ensuring membership is grounded in enrolment, preventing duplicate membership, enforcing assignment team capacity with both domain-level checks and a join-table backstop, making join-request acceptance race-safe with locking, and implementing explicit TeamsController authorisation rules with request specs that validate correct HTTP response codes. MentoredTeam is tested to rely on participant duty (Mentor) rather than user role.&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
* Expertiza project specification — Teams hierarchy testing (Mentor: Vihar Manojkumar Shah)&lt;br /&gt;
* Rails Guides — Active Record Associations&lt;br /&gt;
* RSpec Documentation&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167775</id>
		<title>CSC/ECE 517 Spring 2026 - E2610. Teams hierarchy testing</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167775"/>
		<updated>2026-04-09T01:51:48Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Model Specs */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== About This Project ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2610 — Teams Hierarchy Testing&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Vihar Manojkumar Shah&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || Atharva Waingankar, Krisha Darji, Saladin Al-Bataineh&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Membership Join Model''' || &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
The Expertiza platform organises student collaboration through a structured class hierarchy dedicated to managing teams. Teams are the fundamental unit of participation: they allow students to reserve topics, collaborate on coursework, and submit assignments for evaluation.&lt;br /&gt;
&lt;br /&gt;
This project focuses on ensuring the correctness of the Team hierarchy and strengthening automated test coverage. The work emphasizes membership validity (enrolment-based participation), preventing duplicate membership across teams, enforcing capacity limits when configured, ensuring MentoredTeam uses duty-based mentor identification, and validating controller authorization and HTTP response behavior.&lt;br /&gt;
&lt;br /&gt;
== Background ==&lt;br /&gt;
&lt;br /&gt;
=== The Team Hierarchy ===&lt;br /&gt;
The hierarchy is composed of exactly four classes:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; — STI superclass providing shared associations and common behaviours&lt;br /&gt;
* &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt; — course-scoped teams that persist across a course&lt;br /&gt;
* &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; — assignment-scoped teams created for a single assignment&lt;br /&gt;
* &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt; — subclass of &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; with a designated mentor&lt;br /&gt;
&lt;br /&gt;
Two broad kinds of team exist:&lt;br /&gt;
&lt;br /&gt;
* '''Course teams''' (&amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;) persist throughout a course. In some instructional designs (e.g., Team-Based Learning), these teams are reused for multiple assignments.&lt;br /&gt;
* '''Assignment teams''' (&amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;) exist only for a single assignment. When mentors are used, assignment teams are instantiated as &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Class Hierarchy Diagram ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
                        ┌─────────────────┐&lt;br /&gt;
                        │      Team       │&lt;br /&gt;
                        │  (STI parent)   │&lt;br /&gt;
                        └────────┬────────┘&lt;br /&gt;
                                 │&lt;br /&gt;
               ┌─────────────────┴──────────────────┐&lt;br /&gt;
               │                                    │&lt;br /&gt;
      ┌────────┴────────┐                ┌──────────┴──────────┐&lt;br /&gt;
      │   CourseTeam    │                │   AssignmentTeam    │&lt;br /&gt;
      │ (course-scoped) │                │ (assignment-scoped) │&lt;br /&gt;
      └─────────────────┘                └──────────┬──────────┘&lt;br /&gt;
                                                    │&lt;br /&gt;
                                         ┌──────────┴──────────┐&lt;br /&gt;
                                         │    MentoredTeam     │&lt;br /&gt;
                                         │ (mentor via duty)   │&lt;br /&gt;
                                         └─────────────────────┘&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Subclass Responsibilities ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Class !! Scope !! Key Constraint !! Notes&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; || — || Shared membership/associations || Parent class for all team types (STI)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt; || Course || Member must be a course participant || Can be converted to/from assignment teams&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; || Assignment || Member must be an assignment participant || Capacity comes from &amp;lt;code&amp;gt;Assignment#max_team_size&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt; || Assignment || Mentor identified by participant duty || Uses &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; + participant &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Duty vs. Role Distinction ==&lt;br /&gt;
&lt;br /&gt;
A critical design point (and a core source of bugs in this area) is the separation between a user's '''role''' and a participant's '''duty'''.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Concept !! Definition !! Examples&lt;br /&gt;
|-&lt;br /&gt;
| '''Role''' || System-level permission level assigned to a user account || &amp;lt;code&amp;gt;Instructor&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Teaching Assistant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Student&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Duty''' || A function assigned to a participant ''within a specific team'' || &amp;lt;code&amp;gt;Submitter&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Reviewer&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Mentor&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
In Expertiza, mentors are not privileged system accounts; they are normal users assigned the duty of Mentor on one team. Therefore, mentor logic must use participant duty rather than user role.&lt;br /&gt;
&lt;br /&gt;
== Previous Implementation ==&lt;br /&gt;
&lt;br /&gt;
=== 1) MentoredTeam used role-based assumptions ===&lt;br /&gt;
The incorrect approach was to treat “mentor” as a system-level role check. This is incompatible with Expertiza’s domain model, where mentor is a team-level duty assigned to a participant.&lt;br /&gt;
&lt;br /&gt;
'''What we changed:''' MentoredTeam mentor assignment and mentor discovery use &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; and the participant’s &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== 2) Capacity enforcement was inconsistent / bypassable ===&lt;br /&gt;
Membership could be created through multiple paths (domain method, controllers, join-request acceptance). If enforcement only happened in one place, direct creation of &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt; could bypass capacity checks.&lt;br /&gt;
&lt;br /&gt;
'''What we changed:''' Capacity is enforced at both:&lt;br /&gt;
* the domain method level (&amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt;)&lt;br /&gt;
* the join model level (&amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt; validation backstop)&lt;br /&gt;
&lt;br /&gt;
Join-request acceptance also checks capacity inside a lock for race-safety.&lt;br /&gt;
&lt;br /&gt;
=== 3) TeamsController authorisation was not explicit ===&lt;br /&gt;
The authorisation framework existed (global before_action), but &amp;lt;code&amp;gt;TeamsController&amp;lt;/code&amp;gt; did not implement an explicit access policy. As a result, student actions could succeed where they should be forbidden.&lt;br /&gt;
&lt;br /&gt;
'''What we changed:''' TeamsController now defines &amp;lt;code&amp;gt;action_allowed?&amp;lt;/code&amp;gt;, and request specs verify correct &amp;lt;code&amp;gt;403 Forbidden&amp;lt;/code&amp;gt; behaviour.&lt;br /&gt;
&lt;br /&gt;
=== 4) CourseTeam capacity was assumed without schema support ===&lt;br /&gt;
The current schema does not provide a &amp;lt;code&amp;gt;max_team_size&amp;lt;/code&amp;gt; attribute for courses. Attempting to enforce CourseTeam capacity via &amp;lt;code&amp;gt;course.max_team_size&amp;lt;/code&amp;gt; results in runtime errors and breaks previously passing tests.&lt;br /&gt;
&lt;br /&gt;
'''What we changed:''' Capacity enforcement is based on assignment configuration (&amp;lt;code&amp;gt;Assignment#max_team_size&amp;lt;/code&amp;gt;). CourseTeams remain uncapped by default unless the schema is extended in the future.&lt;br /&gt;
&lt;br /&gt;
== Implementation Summary  ==&lt;br /&gt;
&lt;br /&gt;
=== 1) Enrollment-based membership ===&lt;br /&gt;
Membership is grounded in valid participation. A user can only be added if the appropriate participant record exists in the correct parent scope.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/team.rb&lt;br /&gt;
participant_type = is_a?(AssignmentTeam) ? AssignmentParticipant : CourseParticipant&lt;br /&gt;
participant_type.find_by(user_id: participant_or_user.id, parent_id: parent_id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2) Prevent duplicate membership across teams ===&lt;br /&gt;
The join model prevents a participant from appearing on multiple teams:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validates :participant_id, uniqueness: true&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A unique index migration reinforces this at the database level.&lt;br /&gt;
&lt;br /&gt;
=== 3) Capacity enforcement (configured via Assignment) ===&lt;br /&gt;
Capacity is enforced via the assignment’s &amp;lt;code&amp;gt;max_team_size&amp;lt;/code&amp;gt;. When at capacity, attempts to add are rejected with an error.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/team.rb&lt;br /&gt;
def full?&lt;br /&gt;
  max = max_size&lt;br /&gt;
  return false if max.blank?&lt;br /&gt;
  participants.count &amp;gt;= max&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/team.rb&lt;br /&gt;
return { success: false, error: &amp;quot;Unable to add participant: team is at full capacity.&amp;quot; } if full?&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 4) Join-model capacity backstop ===&lt;br /&gt;
Even direct join-table creation is blocked when a team is at capacity.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validate :team_not_full, on: :create&lt;br /&gt;
&lt;br /&gt;
def team_not_full&lt;br /&gt;
  return unless team&lt;br /&gt;
  max = team.max_size&lt;br /&gt;
  return if max.blank?&lt;br /&gt;
&lt;br /&gt;
  if team.participants.count &amp;gt;= max&lt;br /&gt;
    errors.add(:base, &amp;quot;Team is at full capacity (max #{max}).&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 5) Race-safe join request acceptance ===&lt;br /&gt;
Acceptance checks capacity while holding a lock to prevent two concurrent acceptances from overfilling a team.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/controllers/join_team_requests_controller.rb (accept)&lt;br /&gt;
ActiveRecord::Base.transaction do&lt;br /&gt;
  team.with_lock do&lt;br /&gt;
    if team.full?&lt;br /&gt;
      # reject accept&lt;br /&gt;
    end&lt;br /&gt;
    result = team.add_member(participant)&lt;br /&gt;
    # reject if result failed&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  @join_team_request.update!(reply_status: ACCEPTED)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 6) TeamsController authorisation and HTTP codes ===&lt;br /&gt;
TeamsController now declares an explicit policy:&lt;br /&gt;
&lt;br /&gt;
* Teaching staff (TA and above): allowed&lt;br /&gt;
* Students: cannot list all teams, cannot manage membership, can only view teams they belong to&lt;br /&gt;
&lt;br /&gt;
Request specs assert that restricted actions return &amp;lt;code&amp;gt;403&amp;lt;/code&amp;gt; with an error payload.&lt;br /&gt;
&lt;br /&gt;
== RSpec Test Coverage ==&lt;br /&gt;
&lt;br /&gt;
The test suite is organised into model specs (domain rules) and request specs (API behaviour + HTTP status codes).&lt;br /&gt;
&lt;br /&gt;
=== Model Specs ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Team membership + validations || &amp;lt;code&amp;gt;spec/models/team_spec.rb&amp;lt;/code&amp;gt; || Type/parent validation, membership checks, &amp;lt;code&amp;gt;full?&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;add_member&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Capacity behavior || &amp;lt;code&amp;gt;spec/models/team_capacity_spec.rb&amp;lt;/code&amp;gt; || AssignmentTeam capacity and error rejection&lt;br /&gt;
|-&lt;br /&gt;
| Association integrity || &amp;lt;code&amp;gt;spec/models/team_association_spec.rb&amp;lt;/code&amp;gt; || Parent linkage and membership scoping&lt;br /&gt;
|-&lt;br /&gt;
| Conversion behaviour || &amp;lt;code&amp;gt;spec/models/team_conversion_spec.rb&amp;lt;/code&amp;gt; || CourseTeam ⇄ AssignmentTeam conversions and member copying&lt;br /&gt;
|-&lt;br /&gt;
| MentoredTeam duty behaviour || &amp;lt;code&amp;gt;spec/models/mentored_team_spec.rb&amp;lt;/code&amp;gt; || Mentor duty assignment/removal and mentor lookup by duty&lt;br /&gt;
|-&lt;br /&gt;
| Join model constraints || &amp;lt;code&amp;gt;spec/models/teams_participant_spec.rb&amp;lt;/code&amp;gt; || Uniqueness, presence, and capacity backstop (no bypass)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Request Specs ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Endpoint Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Teams API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_controller_spec.rb&amp;lt;/code&amp;gt; || Index/show/members/add/remove + authorisation status codes&lt;br /&gt;
|-&lt;br /&gt;
| JoinTeamRequests API || &amp;lt;code&amp;gt;spec/requests/api/v1/join_team_requests_controller_spec.rb&amp;lt;/code&amp;gt; || Authorisation, create/accept/decline, capacity rejection and rollback behaviour&lt;br /&gt;
|-&lt;br /&gt;
| TeamsParticipants API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_participants_controller_spec.rb&amp;lt;/code&amp;gt; || Duty update authorisation, list/add/delete flows&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Important Example Tests  ===&lt;br /&gt;
&lt;br /&gt;
==== Capacity and size limits ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/models/team_capacity_spec.rb&lt;br /&gt;
it 'rejects member when team is full' do&lt;br /&gt;
  2.times { |i| team.add_member(make_participant(&amp;quot;full#{i}&amp;quot;)) }&lt;br /&gt;
  extra  = make_participant('extra')&lt;br /&gt;
&lt;br /&gt;
  result = team.add_member(extra)&lt;br /&gt;
&lt;br /&gt;
  expect(result[:success]).to be false&lt;br /&gt;
  expect(result[:error]).to match(/capacity/i)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: verifies domain-level capacity enforcement returns a structured failure and a meaningful error.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/models/teams_participant_spec.rb&lt;br /&gt;
describe 'capacity validation (team_not_full)' do&lt;br /&gt;
  it 'raises when creating beyond capacity' do&lt;br /&gt;
    assignment.update!(max_team_size: 1)&lt;br /&gt;
    team = AssignmentTeam.create!(name: 'Cap Team2', parent_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
    u1 = make_user('cap2_u1')&lt;br /&gt;
    p1 = AssignmentParticipant.create!(user: u1, parent_id: assignment.id, handle: u1.name)&lt;br /&gt;
    TeamsParticipant.create!(team: team, participant: p1, user: u1)&lt;br /&gt;
&lt;br /&gt;
    u2 = make_user('cap2_u2')&lt;br /&gt;
    p2 = AssignmentParticipant.create!(user: u2, parent_id: assignment.id, handle: u2.name)&lt;br /&gt;
&lt;br /&gt;
    expect {&lt;br /&gt;
      TeamsParticipant.create!(team: team, participant: p2, user: u2)&lt;br /&gt;
    }.to raise_error(ActiveRecord::RecordInvalid)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: ensures capacity cannot be bypassed by direct join-table creation.&lt;br /&gt;
&lt;br /&gt;
==== Join request acceptance rollback correctness ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/requests/api/v1/join_team_requests_controller_spec.rb&lt;br /&gt;
it 'does not change reply_status or add participant when team is full' do&lt;br /&gt;
  assignment.update!(max_team_size: 1)&lt;br /&gt;
&lt;br /&gt;
  patch &amp;quot;/join_team_requests/#{join_team_request.id}/accept&amp;quot;, headers: team_member_headers&lt;br /&gt;
&lt;br /&gt;
  expect(response).to have_http_status(:unprocessable_entity)&lt;br /&gt;
  expect(join_team_request.reload.reply_status).to eq('PENDING')&lt;br /&gt;
  expect(team1.participants.reload).not_to include(participant2)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: verifies the accept endpoint rejects at capacity and does not mutate state.&lt;br /&gt;
&lt;br /&gt;
==== TeamsController authorisation / HTTP status codes ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/requests/api/v1/teams_controller_spec.rb&lt;br /&gt;
it 'returns 403 for student on GET /teams' do&lt;br /&gt;
  student_token   = JsonWebToken.encode(id: other_user.id)&lt;br /&gt;
  student_headers = { Authorization: &amp;quot;Bearer #{student_token}&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  get '/teams', headers: student_headers&lt;br /&gt;
&lt;br /&gt;
  expect(response).to have_http_status(:forbidden)&lt;br /&gt;
  expect(JSON.parse(response.body)).to have_key('error')&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: ensures a student cannot list teams and receives &amp;lt;code&amp;gt;403&amp;lt;/code&amp;gt; with an error payload.&lt;br /&gt;
&lt;br /&gt;
==== MentoredTeam duty-based mentor behaviour ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/models/mentored_team_spec.rb&lt;br /&gt;
it 'identifies mentor by duty not by role' do&lt;br /&gt;
  mentor_duty&lt;br /&gt;
  participant = make_participant('duty_mentor')&lt;br /&gt;
  team.add_member(participant)&lt;br /&gt;
  team.assign_mentor(participant.user)&lt;br /&gt;
&lt;br /&gt;
  expect(team.send(:mentor)).to eq(participant.user)&lt;br /&gt;
  expect(participant.reload.duty).to eq(mentor_duty)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: proves mentor identification is duty-based, matching Expertiza’s domain model.&lt;br /&gt;
== Demo Video==&lt;br /&gt;
https://www.youtube.com/watch?v=KZh-Kwhduok&lt;br /&gt;
== Summary ==&lt;br /&gt;
This project strengthens the Teams hierarchy by ensuring membership is grounded in enrolment, preventing duplicate membership, enforcing assignment team capacity with both domain-level checks and a join-table backstop, making join-request acceptance race-safe with locking, and implementing explicit TeamsController authorisation rules with request specs that validate correct HTTP response codes. MentoredTeam is tested to rely on participant duty (Mentor) rather than user role.&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
* Expertiza project specification — Teams hierarchy testing (Mentor: Vihar Manojkumar Shah)&lt;br /&gt;
* Rails Guides — Active Record Associations&lt;br /&gt;
* RSpec Documentation&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167774</id>
		<title>CSC/ECE 517 Spring 2026 - E2610. Teams hierarchy testing</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2610._Teams_hierarchy_testing&amp;diff=167774"/>
		<updated>2026-04-09T01:49:47Z</updated>

		<summary type="html">&lt;p&gt;Admin: Changed several British spellings (flagged by the spellchecker) to American spellings&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== About This Project ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;; margin-left:20px; width:320px;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! colspan=&amp;quot;2&amp;quot; style=&amp;quot;background:#cee0f2; text-align:center;&amp;quot; | Project Info&lt;br /&gt;
|-&lt;br /&gt;
| '''Course''' || CSC/ECE 517 Spring 2026&lt;br /&gt;
|-&lt;br /&gt;
| '''Project''' || E2610 — Teams Hierarchy Testing&lt;br /&gt;
|-&lt;br /&gt;
| '''Instructor''' || Ed Gehringer&lt;br /&gt;
|-&lt;br /&gt;
| '''Mentor''' || Vihar Manojkumar Shah&lt;br /&gt;
|-&lt;br /&gt;
| '''Collaborators''' || Atharva Waingankar, Krisha Darji, Saladin Al-Bataineh&lt;br /&gt;
|-&lt;br /&gt;
| '''Platform''' || Expertiza (Ruby on Rails)&lt;br /&gt;
|-&lt;br /&gt;
| '''Test Framework''' || RSpec&lt;br /&gt;
|-&lt;br /&gt;
| '''Root Class''' || &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; (STI superclass)&lt;br /&gt;
|-&lt;br /&gt;
| '''Subclasses''' || &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Membership Join Model''' || &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
The Expertiza platform organises student collaboration through a structured class hierarchy dedicated to managing teams. Teams are the fundamental unit of participation: they allow students to reserve topics, collaborate on coursework, and submit assignments for evaluation.&lt;br /&gt;
&lt;br /&gt;
This project focuses on ensuring the correctness of the Team hierarchy and strengthening automated test coverage. The work emphasizes membership validity (enrolment-based participation), preventing duplicate membership across teams, enforcing capacity limits when configured, ensuring MentoredTeam uses duty-based mentor identification, and validating controller authorization and HTTP response behavior.&lt;br /&gt;
&lt;br /&gt;
== Background ==&lt;br /&gt;
&lt;br /&gt;
=== The Team Hierarchy ===&lt;br /&gt;
The hierarchy is composed of exactly four classes:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; — STI superclass providing shared associations and common behaviours&lt;br /&gt;
* &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt; — course-scoped teams that persist across a course&lt;br /&gt;
* &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; — assignment-scoped teams created for a single assignment&lt;br /&gt;
* &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt; — subclass of &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; with a designated mentor&lt;br /&gt;
&lt;br /&gt;
Two broad kinds of team exist:&lt;br /&gt;
&lt;br /&gt;
* '''Course teams''' (&amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt;) persist throughout a course. In some instructional designs (e.g., Team-Based Learning), these teams are reused for multiple assignments.&lt;br /&gt;
* '''Assignment teams''' (&amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt;) exist only for a single assignment. When mentors are used, assignment teams are instantiated as &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Class Hierarchy Diagram ===&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
                        ┌─────────────────┐&lt;br /&gt;
                        │      Team       │&lt;br /&gt;
                        │  (STI parent)   │&lt;br /&gt;
                        └────────┬────────┘&lt;br /&gt;
                                 │&lt;br /&gt;
               ┌─────────────────┴──────────────────┐&lt;br /&gt;
               │                                    │&lt;br /&gt;
      ┌────────┴────────┐                ┌──────────┴──────────┐&lt;br /&gt;
      │   CourseTeam    │                │   AssignmentTeam    │&lt;br /&gt;
      │ (course-scoped) │                │ (assignment-scoped) │&lt;br /&gt;
      └─────────────────┘                └──────────┬──────────┘&lt;br /&gt;
                                                    │&lt;br /&gt;
                                         ┌──────────┴──────────┐&lt;br /&gt;
                                         │    MentoredTeam     │&lt;br /&gt;
                                         │ (mentor via duty)   │&lt;br /&gt;
                                         └─────────────────────┘&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Subclass Responsibilities ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Class !! Scope !! Key Constraint !! Notes&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;Team&amp;lt;/code&amp;gt; || — || Shared membership/associations || Parent class for all team types (STI)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;CourseTeam&amp;lt;/code&amp;gt; || Course || Member must be a course participant || Can be converted to/from assignment teams&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;AssignmentTeam&amp;lt;/code&amp;gt; || Assignment || Member must be an assignment participant || Capacity comes from &amp;lt;code&amp;gt;Assignment#max_team_size&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;MentoredTeam&amp;lt;/code&amp;gt; || Assignment || Mentor identified by participant duty || Uses &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; + participant &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Duty vs. Role Distinction ==&lt;br /&gt;
&lt;br /&gt;
A critical design point (and a core source of bugs in this area) is the separation between a user's '''role''' and a participant's '''duty'''.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Concept !! Definition !! Examples&lt;br /&gt;
|-&lt;br /&gt;
| '''Role''' || System-level permission level assigned to a user account || &amp;lt;code&amp;gt;Instructor&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Teaching Assistant&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Student&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| '''Duty''' || A function assigned to a participant ''within a specific team'' || &amp;lt;code&amp;gt;Submitter&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Reviewer&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;Mentor&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
In Expertiza, mentors are not privileged system accounts; they are normal users assigned the duty of Mentor on one team. Therefore, mentor logic must use participant duty rather than user role.&lt;br /&gt;
&lt;br /&gt;
== Previous Implementation ==&lt;br /&gt;
&lt;br /&gt;
=== 1) MentoredTeam used role-based assumptions ===&lt;br /&gt;
The incorrect approach was to treat “mentor” as a system-level role check. This is incompatible with Expertiza’s domain model, where mentor is a team-level duty assigned to a participant.&lt;br /&gt;
&lt;br /&gt;
'''What we changed:''' MentoredTeam mentor assignment and mentor discovery use &amp;lt;code&amp;gt;Duty&amp;lt;/code&amp;gt; and the participant’s &amp;lt;code&amp;gt;duty_id&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== 2) Capacity enforcement was inconsistent / bypassable ===&lt;br /&gt;
Membership could be created through multiple paths (domain method, controllers, join-request acceptance). If enforcement only happened in one place, direct creation of &amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt; could bypass capacity checks.&lt;br /&gt;
&lt;br /&gt;
'''What we changed:''' Capacity is enforced at both:&lt;br /&gt;
* the domain method level (&amp;lt;code&amp;gt;Team#add_member&amp;lt;/code&amp;gt;)&lt;br /&gt;
* the join model level (&amp;lt;code&amp;gt;TeamsParticipant&amp;lt;/code&amp;gt; validation backstop)&lt;br /&gt;
&lt;br /&gt;
Join-request acceptance also checks capacity inside a lock for race-safety.&lt;br /&gt;
&lt;br /&gt;
=== 3) TeamsController authorisation was not explicit ===&lt;br /&gt;
The authorisation framework existed (global before_action), but &amp;lt;code&amp;gt;TeamsController&amp;lt;/code&amp;gt; did not implement an explicit access policy. As a result, student actions could succeed where they should be forbidden.&lt;br /&gt;
&lt;br /&gt;
'''What we changed:''' TeamsController now defines &amp;lt;code&amp;gt;action_allowed?&amp;lt;/code&amp;gt;, and request specs verify correct &amp;lt;code&amp;gt;403 Forbidden&amp;lt;/code&amp;gt; behaviour.&lt;br /&gt;
&lt;br /&gt;
=== 4) CourseTeam capacity was assumed without schema support ===&lt;br /&gt;
The current schema does not provide a &amp;lt;code&amp;gt;max_team_size&amp;lt;/code&amp;gt; attribute for courses. Attempting to enforce CourseTeam capacity via &amp;lt;code&amp;gt;course.max_team_size&amp;lt;/code&amp;gt; results in runtime errors and breaks previously passing tests.&lt;br /&gt;
&lt;br /&gt;
'''What we changed:''' Capacity enforcement is based on assignment configuration (&amp;lt;code&amp;gt;Assignment#max_team_size&amp;lt;/code&amp;gt;). CourseTeams remain uncapped by default unless the schema is extended in the future.&lt;br /&gt;
&lt;br /&gt;
== Implementation Summary  ==&lt;br /&gt;
&lt;br /&gt;
=== 1) Enrollment-based membership ===&lt;br /&gt;
Membership is grounded in valid participation. A user can only be added if the appropriate participant record exists in the correct parent scope.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/team.rb&lt;br /&gt;
participant_type = is_a?(AssignmentTeam) ? AssignmentParticipant : CourseParticipant&lt;br /&gt;
participant_type.find_by(user_id: participant_or_user.id, parent_id: parent_id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2) Prevent duplicate membership across teams ===&lt;br /&gt;
The join model prevents a participant from appearing on multiple teams:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validates :participant_id, uniqueness: true&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A unique index migration reinforces this at the database level.&lt;br /&gt;
&lt;br /&gt;
=== 3) Capacity enforcement (configured via Assignment) ===&lt;br /&gt;
Capacity is enforced via the assignment’s &amp;lt;code&amp;gt;max_team_size&amp;lt;/code&amp;gt;. When at capacity, attempts to add are rejected with an error.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/team.rb&lt;br /&gt;
def full?&lt;br /&gt;
  max = max_size&lt;br /&gt;
  return false if max.blank?&lt;br /&gt;
  participants.count &amp;gt;= max&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/team.rb&lt;br /&gt;
return { success: false, error: &amp;quot;Unable to add participant: team is at full capacity.&amp;quot; } if full?&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 4) Join-model capacity backstop ===&lt;br /&gt;
Even direct join-table creation is blocked when a team is at capacity.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/models/teams_participant.rb&lt;br /&gt;
validate :team_not_full, on: :create&lt;br /&gt;
&lt;br /&gt;
def team_not_full&lt;br /&gt;
  return unless team&lt;br /&gt;
  max = team.max_size&lt;br /&gt;
  return if max.blank?&lt;br /&gt;
&lt;br /&gt;
  if team.participants.count &amp;gt;= max&lt;br /&gt;
    errors.add(:base, &amp;quot;Team is at full capacity (max #{max}).&amp;quot;)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 5) Race-safe join request acceptance ===&lt;br /&gt;
Acceptance checks capacity while holding a lock to prevent two concurrent acceptances from overfilling a team.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# app/controllers/join_team_requests_controller.rb (accept)&lt;br /&gt;
ActiveRecord::Base.transaction do&lt;br /&gt;
  team.with_lock do&lt;br /&gt;
    if team.full?&lt;br /&gt;
      # reject accept&lt;br /&gt;
    end&lt;br /&gt;
    result = team.add_member(participant)&lt;br /&gt;
    # reject if result failed&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  @join_team_request.update!(reply_status: ACCEPTED)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 6) TeamsController authorisation and HTTP codes ===&lt;br /&gt;
TeamsController now declares an explicit policy:&lt;br /&gt;
&lt;br /&gt;
* Teaching staff (TA and above): allowed&lt;br /&gt;
* Students: cannot list all teams, cannot manage membership, can only view teams they belong to&lt;br /&gt;
&lt;br /&gt;
Request specs assert that restricted actions return &amp;lt;code&amp;gt;403&amp;lt;/code&amp;gt; with an error payload.&lt;br /&gt;
&lt;br /&gt;
== RSpec Test Coverage ==&lt;br /&gt;
&lt;br /&gt;
The test suite is organised into model specs (domain rules) and request specs (API behaviour + HTTP status codes).&lt;br /&gt;
&lt;br /&gt;
=== Model Specs ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Team membership + validations || &amp;lt;code&amp;gt;spec/models/team_spec.rb&amp;lt;/code&amp;gt; || Type/parent validation, membership checks, &amp;lt;code&amp;gt;full?&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;add_member&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Capacity behaviour || &amp;lt;code&amp;gt;spec/models/team_capacity_spec.rb&amp;lt;/code&amp;gt; || AssignmentTeam capacity and error rejection&lt;br /&gt;
|-&lt;br /&gt;
| Association integrity || &amp;lt;code&amp;gt;spec/models/team_association_spec.rb&amp;lt;/code&amp;gt; || Parent linkage and membership scoping&lt;br /&gt;
|-&lt;br /&gt;
| Conversion behaviour || &amp;lt;code&amp;gt;spec/models/team_conversion_spec.rb&amp;lt;/code&amp;gt; || CourseTeam ⇄ AssignmentTeam conversions and member copying&lt;br /&gt;
|-&lt;br /&gt;
| MentoredTeam duty behaviour || &amp;lt;code&amp;gt;spec/models/mentored_team_spec.rb&amp;lt;/code&amp;gt; || Mentor duty assignment/removal and mentor lookup by duty&lt;br /&gt;
|-&lt;br /&gt;
| Join model constraints || &amp;lt;code&amp;gt;spec/models/teams_participant_spec.rb&amp;lt;/code&amp;gt; || Uniqueness, presence, and capacity backstop (no bypass)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Request Specs ===&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width:100%;&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Endpoint Area !! Spec File(s) !! What is validated&lt;br /&gt;
|-&lt;br /&gt;
| Teams API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_controller_spec.rb&amp;lt;/code&amp;gt; || Index/show/members/add/remove + authorisation status codes&lt;br /&gt;
|-&lt;br /&gt;
| JoinTeamRequests API || &amp;lt;code&amp;gt;spec/requests/api/v1/join_team_requests_controller_spec.rb&amp;lt;/code&amp;gt; || Authorisation, create/accept/decline, capacity rejection and rollback behaviour&lt;br /&gt;
|-&lt;br /&gt;
| TeamsParticipants API || &amp;lt;code&amp;gt;spec/requests/api/v1/teams_participants_controller_spec.rb&amp;lt;/code&amp;gt; || Duty update authorisation, list/add/delete flows&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Important Example Tests  ===&lt;br /&gt;
&lt;br /&gt;
==== Capacity and size limits ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/models/team_capacity_spec.rb&lt;br /&gt;
it 'rejects member when team is full' do&lt;br /&gt;
  2.times { |i| team.add_member(make_participant(&amp;quot;full#{i}&amp;quot;)) }&lt;br /&gt;
  extra  = make_participant('extra')&lt;br /&gt;
&lt;br /&gt;
  result = team.add_member(extra)&lt;br /&gt;
&lt;br /&gt;
  expect(result[:success]).to be false&lt;br /&gt;
  expect(result[:error]).to match(/capacity/i)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: verifies domain-level capacity enforcement returns a structured failure and a meaningful error.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/models/teams_participant_spec.rb&lt;br /&gt;
describe 'capacity validation (team_not_full)' do&lt;br /&gt;
  it 'raises when creating beyond capacity' do&lt;br /&gt;
    assignment.update!(max_team_size: 1)&lt;br /&gt;
    team = AssignmentTeam.create!(name: 'Cap Team2', parent_id: assignment.id)&lt;br /&gt;
&lt;br /&gt;
    u1 = make_user('cap2_u1')&lt;br /&gt;
    p1 = AssignmentParticipant.create!(user: u1, parent_id: assignment.id, handle: u1.name)&lt;br /&gt;
    TeamsParticipant.create!(team: team, participant: p1, user: u1)&lt;br /&gt;
&lt;br /&gt;
    u2 = make_user('cap2_u2')&lt;br /&gt;
    p2 = AssignmentParticipant.create!(user: u2, parent_id: assignment.id, handle: u2.name)&lt;br /&gt;
&lt;br /&gt;
    expect {&lt;br /&gt;
      TeamsParticipant.create!(team: team, participant: p2, user: u2)&lt;br /&gt;
    }.to raise_error(ActiveRecord::RecordInvalid)&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: ensures capacity cannot be bypassed by direct join-table creation.&lt;br /&gt;
&lt;br /&gt;
==== Join request acceptance rollback correctness ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/requests/api/v1/join_team_requests_controller_spec.rb&lt;br /&gt;
it 'does not change reply_status or add participant when team is full' do&lt;br /&gt;
  assignment.update!(max_team_size: 1)&lt;br /&gt;
&lt;br /&gt;
  patch &amp;quot;/join_team_requests/#{join_team_request.id}/accept&amp;quot;, headers: team_member_headers&lt;br /&gt;
&lt;br /&gt;
  expect(response).to have_http_status(:unprocessable_entity)&lt;br /&gt;
  expect(join_team_request.reload.reply_status).to eq('PENDING')&lt;br /&gt;
  expect(team1.participants.reload).not_to include(participant2)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: verifies the accept endpoint rejects at capacity and does not mutate state.&lt;br /&gt;
&lt;br /&gt;
==== TeamsController authorisation / HTTP status codes ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/requests/api/v1/teams_controller_spec.rb&lt;br /&gt;
it 'returns 403 for student on GET /teams' do&lt;br /&gt;
  student_token   = JsonWebToken.encode(id: other_user.id)&lt;br /&gt;
  student_headers = { Authorization: &amp;quot;Bearer #{student_token}&amp;quot; }&lt;br /&gt;
&lt;br /&gt;
  get '/teams', headers: student_headers&lt;br /&gt;
&lt;br /&gt;
  expect(response).to have_http_status(:forbidden)&lt;br /&gt;
  expect(JSON.parse(response.body)).to have_key('error')&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: ensures a student cannot list teams and receives &amp;lt;code&amp;gt;403&amp;lt;/code&amp;gt; with an error payload.&lt;br /&gt;
&lt;br /&gt;
==== MentoredTeam duty-based mentor behaviour ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# spec/models/mentored_team_spec.rb&lt;br /&gt;
it 'identifies mentor by duty not by role' do&lt;br /&gt;
  mentor_duty&lt;br /&gt;
  participant = make_participant('duty_mentor')&lt;br /&gt;
  team.add_member(participant)&lt;br /&gt;
  team.assign_mentor(participant.user)&lt;br /&gt;
&lt;br /&gt;
  expect(team.send(:mentor)).to eq(participant.user)&lt;br /&gt;
  expect(participant.reload.duty).to eq(mentor_duty)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Purpose: proves mentor identification is duty-based, matching Expertiza’s domain model.&lt;br /&gt;
== Demo Video==&lt;br /&gt;
https://www.youtube.com/watch?v=KZh-Kwhduok&lt;br /&gt;
== Summary ==&lt;br /&gt;
This project strengthens the Teams hierarchy by ensuring membership is grounded in enrolment, preventing duplicate membership, enforcing assignment team capacity with both domain-level checks and a join-table backstop, making join-request acceptance race-safe with locking, and implementing explicit TeamsController authorisation rules with request specs that validate correct HTTP response codes. MentoredTeam is tested to rely on participant duty (Mentor) rather than user role.&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
* Expertiza project specification — Teams hierarchy testing (Mentor: Vihar Manojkumar Shah)&lt;br /&gt;
* Rails Guides — Active Record Associations&lt;br /&gt;
* RSpec Documentation&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Assignments_questionnaires&amp;diff=167680</id>
		<title>Assignments questionnaires</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Assignments_questionnaires&amp;diff=167680"/>
		<updated>2026-04-02T02:02:01Z</updated>

		<summary type="html">&lt;p&gt;Admin: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Assignments Questionnaire Variable Documentation ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; &lt;br /&gt;
!Field Name !!Type !!Description&lt;br /&gt;
|- &lt;br /&gt;
!id   &lt;br /&gt;
|int(11)  &lt;br /&gt;
|The unique record id&lt;br /&gt;
|- &lt;br /&gt;
!questionnaire_id   &lt;br /&gt;
|int(11)  &lt;br /&gt;
|This references a questionnaire that is used in t he assignment&lt;br /&gt;
|- &lt;br /&gt;
!assignment_id   &lt;br /&gt;
|int(11)  &lt;br /&gt;
|This references the assignment in which the questionnaire is used&lt;br /&gt;
|-&lt;br /&gt;
!notification_limit&lt;br /&gt;
|int&lt;br /&gt;
|If a new review has a score that is more than this # of percentage points away from the current average review score for this artifact, then the instructor will be emailed.&lt;br /&gt;
|-&lt;br /&gt;
!questionnaire_weight&lt;br /&gt;
|int&lt;br /&gt;
|Gives the percentage of the contributor's overall score that is based on this questionnaire.&lt;br /&gt;
|-&lt;br /&gt;
!used_in_round&lt;br /&gt;
|int&lt;br /&gt;
|Tells which round (1, 2, ...) of review this questionnaire is used in.  If null, I suspect that the questionnaire is used in all rounds.&lt;br /&gt;
|-&lt;br /&gt;
!dropdown&lt;br /&gt;
|tinyint(1)&lt;br /&gt;
|I believe that if this is true, instead of using a rubric to evaluate this kind of work (e.g., review of work, teammate review), a simple dropdown with integers from 1 to MAX_SCORE is used instead.&lt;br /&gt;
|-&lt;br /&gt;
!topic_id&lt;br /&gt;
|int&lt;br /&gt;
|If this field is non-null, then this rubric is used only to review this particular topic.&lt;br /&gt;
|-&lt;br /&gt;
!duty_id&lt;br /&gt;
|int&lt;br /&gt;
|If this is non-null, then this rubric is used only to review a team member who takes on a particular role (such as scrum master, tester, or documentor).&lt;br /&gt;
|-&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== E/R diagram for Parents Tables ==&lt;br /&gt;
Tables referred by the Assignment Badges Table as Foreign Key Relationship.&lt;br /&gt;
&lt;br /&gt;
[[File:assignments_questionnaires_imported.png]]&lt;br /&gt;
&lt;br /&gt;
== E/R diagram for Child Tables ==&lt;br /&gt;
No Tables refer the Assignment Badges Table as Foreign Key Relationship.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Back to [http://wiki.expertiza.ncsu.edu/index.php/Documentation_on_Database_Tables Database Tables] Main page.&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Response_maps&amp;diff=167562</id>
		<title>Response maps</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Response_maps&amp;diff=167562"/>
		<updated>2026-03-28T00:38:17Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Response maps variable documentation (Reimplementation) */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Maps a connection between the [[participants]] as reviewers and [[participants]] or [[teams]] as reviewees&lt;br /&gt;
&lt;br /&gt;
==Response maps variable documentation (Reimplementation)==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; &lt;br /&gt;
!Field Name !!Type !!Description &lt;br /&gt;
|-&lt;br /&gt;
!id&lt;br /&gt;
|int(11)&lt;br /&gt;
|The unique record id&lt;br /&gt;
|- &lt;br /&gt;
!reviewed_object_id&lt;br /&gt;
|int(11)&lt;br /&gt;
|The object being reviewed in the [[responses|response]]. In the reimplemented system, this will always be an [[assignment]].&lt;br /&gt;
|-&lt;br /&gt;
!reviewer_id&lt;br /&gt;
|int(11)&lt;br /&gt;
|The [[participants|participant]] (actually AssignmentParticipant) providing the response&lt;br /&gt;
|-&lt;br /&gt;
!reviewee_id&lt;br /&gt;
|int(11)&lt;br /&gt;
|Dependent on which subclass of ResponseMap this object is.  For a ReviewResponseMap, the [[teams|team]] (AssignmentTeam) receiving the response.  For a TeammateReviewResponseMap, the AssignmentParticipant who is being reviewed.&lt;br /&gt;
|-&lt;br /&gt;
!type&lt;br /&gt;
|varchar(255)&lt;br /&gt;
|Used for subclassing the response map. Available subclasses are ReviewResponseMap, MetareviewResponseMap, FeedbackResponseMap, TeammateReviewResponseMap  &lt;br /&gt;
|-&lt;br /&gt;
!created_at&lt;br /&gt;
|DATETIME&lt;br /&gt;
|Date and Time for when the record was created&lt;br /&gt;
|-&lt;br /&gt;
!updated_at&lt;br /&gt;
|DATETIME&lt;br /&gt;
|Date and Time when the last update was made&lt;br /&gt;
|-&lt;br /&gt;
!for_calibration&lt;br /&gt;
|int(11)&lt;br /&gt;
|Tells whether the record will be used for calibration or not.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Response maps variable documentation (2023 version)==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; &lt;br /&gt;
!Field Name !!Type !!Description &lt;br /&gt;
|-&lt;br /&gt;
!id&lt;br /&gt;
|int(11)&lt;br /&gt;
|The unique record id&lt;br /&gt;
|- &lt;br /&gt;
!reviewed_object_id&lt;br /&gt;
|int(11)&lt;br /&gt;
|The object being reviewed in the [[responses|response]]. Possible objects include other ResponseMaps or [[assignments]]&lt;br /&gt;
|-&lt;br /&gt;
!reviewer_id&lt;br /&gt;
|int(11)&lt;br /&gt;
|The [[participants|participant]] (actually AssignmentParticipant) providing the response&lt;br /&gt;
|-&lt;br /&gt;
!reviewee_id&lt;br /&gt;
|int(11)&lt;br /&gt;
|Dependent on which subclass of ResponseMap this object is.  For a ReviewResponseMap, the [[teams|team]] (AssignmentTeam) receiving the response.  For a TeammateReviewResponseMap, the AssignmentParticipant who is being reviewed.&lt;br /&gt;
|-&lt;br /&gt;
!type&lt;br /&gt;
|varchar(255)&lt;br /&gt;
|Used for subclassing the response map. Available subclasses are ReviewResponseMap, MetareviewResponseMap, FeedbackResponseMap, TeammateReviewResponseMap  &lt;br /&gt;
|-&lt;br /&gt;
!created_at&lt;br /&gt;
|DATETIME&lt;br /&gt;
|Date and Time for when the record was created&lt;br /&gt;
|-&lt;br /&gt;
!updated_at&lt;br /&gt;
|DATETIME&lt;br /&gt;
|Date and Time when the last update was made&lt;br /&gt;
|-&lt;br /&gt;
!calibrate_to&lt;br /&gt;
|tinyint(1)&lt;br /&gt;
|Tells whether the record will be used for calibration or not.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Back to the [[Documentation_on_Database_Tables|database documentation]]&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Teams&amp;diff=167560</id>
		<title>Teams</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Teams&amp;diff=167560"/>
		<updated>2026-03-27T00:23:21Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Teams Variable Documentation (Reimplementation) */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Teams Variable Documentation (Reimplementation) ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; &lt;br /&gt;
!Field Name !!Type !!Description &lt;br /&gt;
|-&lt;br /&gt;
!id &lt;br /&gt;
|int(11)  &lt;br /&gt;
|&lt;br /&gt;
|- &lt;br /&gt;
!name   &lt;br /&gt;
|varchar(255)  &lt;br /&gt;
|Name of the team&lt;br /&gt;
|- &lt;br /&gt;
!parent_id  &lt;br /&gt;
|int(11)  &lt;br /&gt;
|ID of assignment or course that this team is participating in&lt;br /&gt;
|- &lt;br /&gt;
!type   &lt;br /&gt;
|varchar(255)&lt;br /&gt;
  &lt;br /&gt;
|Type of the team, either AssignmentTeam or CourseTeam&lt;br /&gt;
|- &lt;br /&gt;
!directory_num&lt;br /&gt;
|int(11) &lt;br /&gt;
|directory number where files submitted by this team are stored&lt;br /&gt;
|- &lt;br /&gt;
!grade_for_submission&lt;br /&gt;
|int(11) &lt;br /&gt;
|Stores the grade assigned by the instructor, if any&lt;br /&gt;
|- &lt;br /&gt;
!comment_for_submission&lt;br /&gt;
|text&lt;br /&gt;
|Stores feedback from the instructor for the team&lt;br /&gt;
|} &lt;br /&gt;
&lt;br /&gt;
== Teams Variable Documentation (2023 version) ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; &lt;br /&gt;
!Field Name !!Type !!Description &lt;br /&gt;
|-&lt;br /&gt;
!id &lt;br /&gt;
|int(11)  &lt;br /&gt;
|&lt;br /&gt;
|- &lt;br /&gt;
!name   &lt;br /&gt;
|varchar(255)  &lt;br /&gt;
|Name of the team&lt;br /&gt;
|- &lt;br /&gt;
!parent_id  &lt;br /&gt;
|int(11)  &lt;br /&gt;
|ID of assignment or course that this team is participating in&lt;br /&gt;
|- &lt;br /&gt;
!type   &lt;br /&gt;
|varchar(255)&lt;br /&gt;
  &lt;br /&gt;
|Type of the team, either AssignmentTeam or CourseTeam&lt;br /&gt;
|- &lt;br /&gt;
!comments_for_advertisement  &lt;br /&gt;
|text&lt;br /&gt;
|text for comments&lt;br /&gt;
|- &lt;br /&gt;
!advertise_for_partner  &lt;br /&gt;
|tinyint(1) &lt;br /&gt;
|Boolean that tells whether the team is advertising for a partner&lt;br /&gt;
|- &lt;br /&gt;
!submitted_hyperlinks  &lt;br /&gt;
|text &lt;br /&gt;
|List of submitted hyperlinks&lt;br /&gt;
|- &lt;br /&gt;
!directory_num&lt;br /&gt;
|int(11) &lt;br /&gt;
|directory number where files submitted by this team are stored&lt;br /&gt;
|- &lt;br /&gt;
!grade_for_submission&lt;br /&gt;
|int(11) &lt;br /&gt;
|Stores the grade assigned by the instructor, if any&lt;br /&gt;
|- &lt;br /&gt;
!comment_for_submission&lt;br /&gt;
|text&lt;br /&gt;
|Stores feedback from the instructor for the team&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== E/R diagram of tables referencing users table  ==&lt;br /&gt;
The following image shows the tables referencing teams table&lt;br /&gt;
[[File:teams_export.png]]&lt;br /&gt;
&lt;br /&gt;
== E/R diagram of tables users table is referencing to ==&lt;br /&gt;
The teams table is not referenced by any other tables&lt;br /&gt;
&lt;br /&gt;
Back to [http://wiki.expertiza.ncsu.edu/index.php/Documentation_on_Database_Tables Database Tables] Main page.&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167531</id>
		<title>Reviewer Assignment implementation</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167531"/>
		<updated>2026-03-15T20:06:39Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Least‑Reviewed Submission Strategy */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Overview =&lt;br /&gt;
The review‑assignment subsystem ''in the reimplemented Expertiza'' supports both '''static''' and '''dynamic''' allocation of reviews within an assignment. The goal is to give instructors flexible control over how reviewers are matched to submissions, while also supporting on‑demand, fairness‑aware assignment for students who request reviews dynamically.&lt;br /&gt;
&lt;br /&gt;
This page documents the behavior implemented in ReviewMappingsController and the strategy classes under ReviewMappingStrategies.&lt;br /&gt;
&lt;br /&gt;
= Static Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Static assignment is initiated by the instructor and produces a complete set of review mappings in a single operation. Three static strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Round‑Robin Assignment ==&lt;br /&gt;
'''Controller action:''' assign_round_robin&lt;br /&gt;
&lt;br /&gt;
This strategy cycles through all reviewers in order and assigns each team to the next reviewer in the cycle. Only participants who are eligible to review (can_review?) are included.&lt;br /&gt;
&lt;br /&gt;
'''Characteristics:'''&lt;br /&gt;
&lt;br /&gt;
*Deterministic ordering&lt;br /&gt;
*Each team receives exactly one reviewer&lt;br /&gt;
*Reviewers are reused as needed&lt;br /&gt;
*No workload balancing beyond the simple cycle&lt;br /&gt;
&lt;br /&gt;
== Random Assignment ==&lt;br /&gt;
'''Controller action:''' assign_random&lt;br /&gt;
&lt;br /&gt;
This strategy assigns reviewers to teams randomly. The distribution is determined by the logic inside ReviewMappingHandler.&lt;br /&gt;
&lt;br /&gt;
== CSV‑Based Assignment ==&lt;br /&gt;
'''Controller action:''' assign_from_csv&lt;br /&gt;
&lt;br /&gt;
This strategy imports reviewer–team pairs from an uploaded CSV file.&lt;br /&gt;
&lt;br /&gt;
'''Expected CSV format:'''&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
reviewer_email,team_name&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
For each row:&lt;br /&gt;
&lt;br /&gt;
*The reviewer is located by email and matched to an AssignmentParticipant.&lt;br /&gt;
*The team is located by name within the assignment.&lt;br /&gt;
*A mapping is created if both are found.&lt;br /&gt;
&lt;br /&gt;
This allows instructors to pre‑compute assignments externally and import them directly.&lt;br /&gt;
&lt;br /&gt;
= Dynamic Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Dynamic assignment is initiated by a reviewer requesting a new review. Instead of pre‑assigning all reviews, the system allocates reviews on demand, which helps avoid the common problem of students failing to complete their assigned reviews.&lt;br /&gt;
&lt;br /&gt;
Two dynamic strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Least‑Reviewed Submission Strategy ==&lt;br /&gt;
'''Controller action:''' request_review_fewest  &lt;br /&gt;
'''Strategy:''' LeastReviewedSubmissionStrategy&lt;br /&gt;
&lt;br /&gt;
This strategy assigns the reviewer to the team whose submission has received the fewest completed reviews so far.&lt;br /&gt;
&lt;br /&gt;
'''Eligibility rules:'''&lt;br /&gt;
&lt;br /&gt;
*A reviewer may not be assigned to their own team.&lt;br /&gt;
*A reviewer may not be assigned to a team they have already reviewed.&lt;br /&gt;
*Among eligible teams, the one with the lowest review count is selected.&lt;br /&gt;
&lt;br /&gt;
This ensures a balanced distribution of reviews across submissions.&lt;br /&gt;
&lt;br /&gt;
== Topic‑Balanced Dynamic Assignment ==&lt;br /&gt;
'''Controller action:''' request_review_topic_balance  &lt;br /&gt;
'''Strategy:''' LeastReviewedTopicStrategy&lt;br /&gt;
&lt;br /&gt;
When an assignment uses topics, reviewers may choose which topics they are willing to review. The system then assigns a team within an eligible topic, subject to fairness constraints.&lt;br /&gt;
&lt;br /&gt;
'''Algorithm:'''&lt;br /&gt;
&lt;br /&gt;
Review counts are aggregated per topic.&lt;br /&gt;
Let min_count be the smallest number of reviews received by any topic.&lt;br /&gt;
A topic is eligible if its review count is within '''k''' of min_count.&lt;br /&gt;
The first eligible topic (deterministically) is selected.&lt;br /&gt;
Within that topic, the reviewer is assigned to a team that:&lt;br /&gt;
*is not their own team,&lt;br /&gt;
*has not already been reviewed by them,&lt;br /&gt;
*has the fewest reviews among eligible teams.&lt;br /&gt;
&lt;br /&gt;
This ensures:&lt;br /&gt;
*Reviewers can restrict themselves to topics they prefer.&lt;br /&gt;
*Topics remain approximately balanced, with fairness controlled by the threshold k.&lt;br /&gt;
&lt;br /&gt;
= Calibration Review Assignment =&lt;br /&gt;
&lt;br /&gt;
== Calibration Round‑Robin ==&lt;br /&gt;
'''Controller action:''' assign_calibration_artifacts&lt;br /&gt;
&lt;br /&gt;
This assigns calibration reviews to all reviewers using a round‑robin strategy. Calibration reviews are typically instructor‑provided artifacts used to train or benchmark reviewers.&lt;br /&gt;
&lt;br /&gt;
= Deleting Review Mappings =&lt;br /&gt;
&lt;br /&gt;
== Delete a Single Mapping ==&lt;br /&gt;
'''Controller action:''' destroy&lt;br /&gt;
&lt;br /&gt;
Deletes a specific review mapping by ID.&lt;br /&gt;
&lt;br /&gt;
== Delete All Reviews for a Reviewer ==&lt;br /&gt;
'''Controller action:''' delete_all_for_reviewer&lt;br /&gt;
&lt;br /&gt;
Removes all review mappings associated with a given reviewer. Useful when resetting or reassigning work.&lt;br /&gt;
&lt;br /&gt;
= Instructor Grading of Reviews =&lt;br /&gt;
&lt;br /&gt;
'''Controller action:''' grade_review&lt;br /&gt;
&lt;br /&gt;
Instructors may grade individual reviews. This action:&lt;br /&gt;
&lt;br /&gt;
*Locates the review mapping&lt;br /&gt;
*Records the instructor’s grade and comment&lt;br /&gt;
*Returns a success message&lt;br /&gt;
&lt;br /&gt;
= Strategy Classes =&lt;br /&gt;
&lt;br /&gt;
All strategies inherit from ReviewMappingStrategies::BaseStrategy, which defines two abstract methods:&lt;br /&gt;
&lt;br /&gt;
*each_review_pair — for static strategies&lt;br /&gt;
&lt;br /&gt;
*assign_one — for dynamic strategies&lt;br /&gt;
&lt;br /&gt;
Each concrete strategy implements one of these depending on its purpose.&lt;br /&gt;
&lt;br /&gt;
== CsvImportStrategy ==&lt;br /&gt;
Implements each_review_pair by reading reviewer–team pairs from a CSV file.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedSubmissionStrategy ==&lt;br /&gt;
Implements assign_one by selecting the eligible team with the fewest reviews.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedTopicStrategy ==&lt;br /&gt;
Implements assign_one with topic‑level fairness constraints and a threshold k.&lt;br /&gt;
&lt;br /&gt;
== RoundRobinStrategy ==&lt;br /&gt;
Implements each_review_pair by cycling through reviewers and pairing them with teams.&lt;br /&gt;
&lt;br /&gt;
= Common Eligibility Rules =&lt;br /&gt;
&lt;br /&gt;
Across all dynamic strategies, a reviewer is never assigned to:&lt;br /&gt;
&lt;br /&gt;
*Their own team&lt;br /&gt;
*A team they have already reviewed&lt;br /&gt;
*A team outside the selected topic (for topic‑based assignment)&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167530</id>
		<title>Reviewer Assignment implementation</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167530"/>
		<updated>2026-03-15T20:06:04Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Overview */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Overview =&lt;br /&gt;
The review‑assignment subsystem ''in the reimplemented Expertiza'' supports both '''static''' and '''dynamic''' allocation of reviews within an assignment. The goal is to give instructors flexible control over how reviewers are matched to submissions, while also supporting on‑demand, fairness‑aware assignment for students who request reviews dynamically.&lt;br /&gt;
&lt;br /&gt;
This page documents the behavior implemented in ReviewMappingsController and the strategy classes under ReviewMappingStrategies.&lt;br /&gt;
&lt;br /&gt;
= Static Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Static assignment is initiated by the instructor and produces a complete set of review mappings in a single operation. Three static strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Round‑Robin Assignment ==&lt;br /&gt;
'''Controller action:''' assign_round_robin&lt;br /&gt;
&lt;br /&gt;
This strategy cycles through all reviewers in order and assigns each team to the next reviewer in the cycle. Only participants who are eligible to review (can_review?) are included.&lt;br /&gt;
&lt;br /&gt;
'''Characteristics:'''&lt;br /&gt;
&lt;br /&gt;
*Deterministic ordering&lt;br /&gt;
*Each team receives exactly one reviewer&lt;br /&gt;
*Reviewers are reused as needed&lt;br /&gt;
*No workload balancing beyond the simple cycle&lt;br /&gt;
&lt;br /&gt;
== Random Assignment ==&lt;br /&gt;
'''Controller action:''' assign_random&lt;br /&gt;
&lt;br /&gt;
This strategy assigns reviewers to teams randomly. The distribution is determined by the logic inside ReviewMappingHandler.&lt;br /&gt;
&lt;br /&gt;
== CSV‑Based Assignment ==&lt;br /&gt;
'''Controller action:''' assign_from_csv&lt;br /&gt;
&lt;br /&gt;
This strategy imports reviewer–team pairs from an uploaded CSV file.&lt;br /&gt;
&lt;br /&gt;
'''Expected CSV format:'''&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
reviewer_email,team_name&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
For each row:&lt;br /&gt;
&lt;br /&gt;
*The reviewer is located by email and matched to an AssignmentParticipant.&lt;br /&gt;
*The team is located by name within the assignment.&lt;br /&gt;
*A mapping is created if both are found.&lt;br /&gt;
&lt;br /&gt;
This allows instructors to pre‑compute assignments externally and import them directly.&lt;br /&gt;
&lt;br /&gt;
= Dynamic Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Dynamic assignment is initiated by a reviewer requesting a new review. Instead of pre‑assigning all reviews, the system allocates reviews on demand, which helps avoid the common problem of students failing to complete their assigned reviews.&lt;br /&gt;
&lt;br /&gt;
Two dynamic strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Least‑Reviewed Submission Strategy ==&lt;br /&gt;
'''Controller action:''' request_review_fewest  &lt;br /&gt;
'''Strategy:''' LeastReviewedSubmissionStrategy&lt;br /&gt;
&lt;br /&gt;
This strategy assigns the reviewer to the team whose submission has received the fewest completed reviews so far.&lt;br /&gt;
&lt;br /&gt;
'''Eligibility rules:'''&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to their own team.&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to a team they have already reviewed.&lt;br /&gt;
&lt;br /&gt;
Among eligible teams, the one with the lowest review count is selected.&lt;br /&gt;
&lt;br /&gt;
This ensures a balanced distribution of reviews across submissions.&lt;br /&gt;
&lt;br /&gt;
== Topic‑Balanced Dynamic Assignment ==&lt;br /&gt;
'''Controller action:''' request_review_topic_balance  &lt;br /&gt;
'''Strategy:''' LeastReviewedTopicStrategy&lt;br /&gt;
&lt;br /&gt;
When an assignment uses topics, reviewers may choose which topics they are willing to review. The system then assigns a team within an eligible topic, subject to fairness constraints.&lt;br /&gt;
&lt;br /&gt;
'''Algorithm:'''&lt;br /&gt;
&lt;br /&gt;
Review counts are aggregated per topic.&lt;br /&gt;
Let min_count be the smallest number of reviews received by any topic.&lt;br /&gt;
A topic is eligible if its review count is within '''k''' of min_count.&lt;br /&gt;
The first eligible topic (deterministically) is selected.&lt;br /&gt;
Within that topic, the reviewer is assigned to a team that:&lt;br /&gt;
*is not their own team,&lt;br /&gt;
*has not already been reviewed by them,&lt;br /&gt;
*has the fewest reviews among eligible teams.&lt;br /&gt;
&lt;br /&gt;
This ensures:&lt;br /&gt;
*Reviewers can restrict themselves to topics they prefer.&lt;br /&gt;
*Topics remain approximately balanced, with fairness controlled by the threshold k.&lt;br /&gt;
&lt;br /&gt;
= Calibration Review Assignment =&lt;br /&gt;
&lt;br /&gt;
== Calibration Round‑Robin ==&lt;br /&gt;
'''Controller action:''' assign_calibration_artifacts&lt;br /&gt;
&lt;br /&gt;
This assigns calibration reviews to all reviewers using a round‑robin strategy. Calibration reviews are typically instructor‑provided artifacts used to train or benchmark reviewers.&lt;br /&gt;
&lt;br /&gt;
= Deleting Review Mappings =&lt;br /&gt;
&lt;br /&gt;
== Delete a Single Mapping ==&lt;br /&gt;
'''Controller action:''' destroy&lt;br /&gt;
&lt;br /&gt;
Deletes a specific review mapping by ID.&lt;br /&gt;
&lt;br /&gt;
== Delete All Reviews for a Reviewer ==&lt;br /&gt;
'''Controller action:''' delete_all_for_reviewer&lt;br /&gt;
&lt;br /&gt;
Removes all review mappings associated with a given reviewer. Useful when resetting or reassigning work.&lt;br /&gt;
&lt;br /&gt;
= Instructor Grading of Reviews =&lt;br /&gt;
&lt;br /&gt;
'''Controller action:''' grade_review&lt;br /&gt;
&lt;br /&gt;
Instructors may grade individual reviews. This action:&lt;br /&gt;
&lt;br /&gt;
*Locates the review mapping&lt;br /&gt;
*Records the instructor’s grade and comment&lt;br /&gt;
*Returns a success message&lt;br /&gt;
&lt;br /&gt;
= Strategy Classes =&lt;br /&gt;
&lt;br /&gt;
All strategies inherit from ReviewMappingStrategies::BaseStrategy, which defines two abstract methods:&lt;br /&gt;
&lt;br /&gt;
*each_review_pair — for static strategies&lt;br /&gt;
&lt;br /&gt;
*assign_one — for dynamic strategies&lt;br /&gt;
&lt;br /&gt;
Each concrete strategy implements one of these depending on its purpose.&lt;br /&gt;
&lt;br /&gt;
== CsvImportStrategy ==&lt;br /&gt;
Implements each_review_pair by reading reviewer–team pairs from a CSV file.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedSubmissionStrategy ==&lt;br /&gt;
Implements assign_one by selecting the eligible team with the fewest reviews.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedTopicStrategy ==&lt;br /&gt;
Implements assign_one with topic‑level fairness constraints and a threshold k.&lt;br /&gt;
&lt;br /&gt;
== RoundRobinStrategy ==&lt;br /&gt;
Implements each_review_pair by cycling through reviewers and pairing them with teams.&lt;br /&gt;
&lt;br /&gt;
= Common Eligibility Rules =&lt;br /&gt;
&lt;br /&gt;
Across all dynamic strategies, a reviewer is never assigned to:&lt;br /&gt;
&lt;br /&gt;
*Their own team&lt;br /&gt;
*A team they have already reviewed&lt;br /&gt;
*A team outside the selected topic (for topic‑based assignment)&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167529</id>
		<title>Reviewer Assignment implementation</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167529"/>
		<updated>2026-03-15T20:05:26Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Overview */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Overview =&lt;br /&gt;
The review‑assignment subsystem &lt;br /&gt;
'in the reimplemented Expertiza'&lt;br /&gt;
supports both '''static''' and '''dynamic''' allocation of reviews within an assignment. The goal is to give instructors flexible control over how reviewers are matched to submissions, while also supporting on‑demand, fairness‑aware assignment for students who request reviews dynamically.&lt;br /&gt;
&lt;br /&gt;
This page documents the behavior implemented in ReviewMappingsController and the strategy classes under ReviewMappingStrategies.&lt;br /&gt;
&lt;br /&gt;
= Static Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Static assignment is initiated by the instructor and produces a complete set of review mappings in a single operation. Three static strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Round‑Robin Assignment ==&lt;br /&gt;
'''Controller action:''' assign_round_robin&lt;br /&gt;
&lt;br /&gt;
This strategy cycles through all reviewers in order and assigns each team to the next reviewer in the cycle. Only participants who are eligible to review (can_review?) are included.&lt;br /&gt;
&lt;br /&gt;
'''Characteristics:'''&lt;br /&gt;
&lt;br /&gt;
*Deterministic ordering&lt;br /&gt;
*Each team receives exactly one reviewer&lt;br /&gt;
*Reviewers are reused as needed&lt;br /&gt;
*No workload balancing beyond the simple cycle&lt;br /&gt;
&lt;br /&gt;
== Random Assignment ==&lt;br /&gt;
'''Controller action:''' assign_random&lt;br /&gt;
&lt;br /&gt;
This strategy assigns reviewers to teams randomly. The distribution is determined by the logic inside ReviewMappingHandler.&lt;br /&gt;
&lt;br /&gt;
== CSV‑Based Assignment ==&lt;br /&gt;
'''Controller action:''' assign_from_csv&lt;br /&gt;
&lt;br /&gt;
This strategy imports reviewer–team pairs from an uploaded CSV file.&lt;br /&gt;
&lt;br /&gt;
'''Expected CSV format:'''&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
reviewer_email,team_name&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
For each row:&lt;br /&gt;
&lt;br /&gt;
*The reviewer is located by email and matched to an AssignmentParticipant.&lt;br /&gt;
*The team is located by name within the assignment.&lt;br /&gt;
*A mapping is created if both are found.&lt;br /&gt;
&lt;br /&gt;
This allows instructors to pre‑compute assignments externally and import them directly.&lt;br /&gt;
&lt;br /&gt;
= Dynamic Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Dynamic assignment is initiated by a reviewer requesting a new review. Instead of pre‑assigning all reviews, the system allocates reviews on demand, which helps avoid the common problem of students failing to complete their assigned reviews.&lt;br /&gt;
&lt;br /&gt;
Two dynamic strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Least‑Reviewed Submission Strategy ==&lt;br /&gt;
'''Controller action:''' request_review_fewest  &lt;br /&gt;
'''Strategy:''' LeastReviewedSubmissionStrategy&lt;br /&gt;
&lt;br /&gt;
This strategy assigns the reviewer to the team whose submission has received the fewest completed reviews so far.&lt;br /&gt;
&lt;br /&gt;
'''Eligibility rules:'''&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to their own team.&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to a team they have already reviewed.&lt;br /&gt;
&lt;br /&gt;
Among eligible teams, the one with the lowest review count is selected.&lt;br /&gt;
&lt;br /&gt;
This ensures a balanced distribution of reviews across submissions.&lt;br /&gt;
&lt;br /&gt;
== Topic‑Balanced Dynamic Assignment ==&lt;br /&gt;
'''Controller action:''' request_review_topic_balance  &lt;br /&gt;
'''Strategy:''' LeastReviewedTopicStrategy&lt;br /&gt;
&lt;br /&gt;
When an assignment uses topics, reviewers may choose which topics they are willing to review. The system then assigns a team within an eligible topic, subject to fairness constraints.&lt;br /&gt;
&lt;br /&gt;
'''Algorithm:'''&lt;br /&gt;
&lt;br /&gt;
Review counts are aggregated per topic.&lt;br /&gt;
Let min_count be the smallest number of reviews received by any topic.&lt;br /&gt;
A topic is eligible if its review count is within '''k''' of min_count.&lt;br /&gt;
The first eligible topic (deterministically) is selected.&lt;br /&gt;
Within that topic, the reviewer is assigned to a team that:&lt;br /&gt;
*is not their own team,&lt;br /&gt;
*has not already been reviewed by them,&lt;br /&gt;
*has the fewest reviews among eligible teams.&lt;br /&gt;
&lt;br /&gt;
This ensures:&lt;br /&gt;
*Reviewers can restrict themselves to topics they prefer.&lt;br /&gt;
*Topics remain approximately balanced, with fairness controlled by the threshold k.&lt;br /&gt;
&lt;br /&gt;
= Calibration Review Assignment =&lt;br /&gt;
&lt;br /&gt;
== Calibration Round‑Robin ==&lt;br /&gt;
'''Controller action:''' assign_calibration_artifacts&lt;br /&gt;
&lt;br /&gt;
This assigns calibration reviews to all reviewers using a round‑robin strategy. Calibration reviews are typically instructor‑provided artifacts used to train or benchmark reviewers.&lt;br /&gt;
&lt;br /&gt;
= Deleting Review Mappings =&lt;br /&gt;
&lt;br /&gt;
== Delete a Single Mapping ==&lt;br /&gt;
'''Controller action:''' destroy&lt;br /&gt;
&lt;br /&gt;
Deletes a specific review mapping by ID.&lt;br /&gt;
&lt;br /&gt;
== Delete All Reviews for a Reviewer ==&lt;br /&gt;
'''Controller action:''' delete_all_for_reviewer&lt;br /&gt;
&lt;br /&gt;
Removes all review mappings associated with a given reviewer. Useful when resetting or reassigning work.&lt;br /&gt;
&lt;br /&gt;
= Instructor Grading of Reviews =&lt;br /&gt;
&lt;br /&gt;
'''Controller action:''' grade_review&lt;br /&gt;
&lt;br /&gt;
Instructors may grade individual reviews. This action:&lt;br /&gt;
&lt;br /&gt;
*Locates the review mapping&lt;br /&gt;
*Records the instructor’s grade and comment&lt;br /&gt;
*Returns a success message&lt;br /&gt;
&lt;br /&gt;
= Strategy Classes =&lt;br /&gt;
&lt;br /&gt;
All strategies inherit from ReviewMappingStrategies::BaseStrategy, which defines two abstract methods:&lt;br /&gt;
&lt;br /&gt;
*each_review_pair — for static strategies&lt;br /&gt;
&lt;br /&gt;
*assign_one — for dynamic strategies&lt;br /&gt;
&lt;br /&gt;
Each concrete strategy implements one of these depending on its purpose.&lt;br /&gt;
&lt;br /&gt;
== CsvImportStrategy ==&lt;br /&gt;
Implements each_review_pair by reading reviewer–team pairs from a CSV file.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedSubmissionStrategy ==&lt;br /&gt;
Implements assign_one by selecting the eligible team with the fewest reviews.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedTopicStrategy ==&lt;br /&gt;
Implements assign_one with topic‑level fairness constraints and a threshold k.&lt;br /&gt;
&lt;br /&gt;
== RoundRobinStrategy ==&lt;br /&gt;
Implements each_review_pair by cycling through reviewers and pairing them with teams.&lt;br /&gt;
&lt;br /&gt;
= Common Eligibility Rules =&lt;br /&gt;
&lt;br /&gt;
Across all dynamic strategies, a reviewer is never assigned to:&lt;br /&gt;
&lt;br /&gt;
*Their own team&lt;br /&gt;
*A team they have already reviewed&lt;br /&gt;
*A team outside the selected topic (for topic‑based assignment)&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167528</id>
		<title>Reviewer Assignment implementation</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167528"/>
		<updated>2026-03-15T20:04:10Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Overview */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Overview =&lt;br /&gt;
The review‑assignment subsystem 'in the reimplemented Expertiza' supports both '''static''' and '''dynamic''' allocation of reviews within an assignment. The goal is to give instructors flexible control over how reviewers are matched to submissions, while also supporting on‑demand, fairness‑aware assignment for students who request reviews dynamically.&lt;br /&gt;
&lt;br /&gt;
This page documents the behavior implemented in ReviewMappingsController and the strategy classes under ReviewMappingStrategies.&lt;br /&gt;
&lt;br /&gt;
= Static Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Static assignment is initiated by the instructor and produces a complete set of review mappings in a single operation. Three static strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Round‑Robin Assignment ==&lt;br /&gt;
'''Controller action:''' assign_round_robin&lt;br /&gt;
&lt;br /&gt;
This strategy cycles through all reviewers in order and assigns each team to the next reviewer in the cycle. Only participants who are eligible to review (can_review?) are included.&lt;br /&gt;
&lt;br /&gt;
'''Characteristics:'''&lt;br /&gt;
&lt;br /&gt;
*Deterministic ordering&lt;br /&gt;
*Each team receives exactly one reviewer&lt;br /&gt;
*Reviewers are reused as needed&lt;br /&gt;
*No workload balancing beyond the simple cycle&lt;br /&gt;
&lt;br /&gt;
== Random Assignment ==&lt;br /&gt;
'''Controller action:''' assign_random&lt;br /&gt;
&lt;br /&gt;
This strategy assigns reviewers to teams randomly. The distribution is determined by the logic inside ReviewMappingHandler.&lt;br /&gt;
&lt;br /&gt;
== CSV‑Based Assignment ==&lt;br /&gt;
'''Controller action:''' assign_from_csv&lt;br /&gt;
&lt;br /&gt;
This strategy imports reviewer–team pairs from an uploaded CSV file.&lt;br /&gt;
&lt;br /&gt;
'''Expected CSV format:'''&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
reviewer_email,team_name&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
For each row:&lt;br /&gt;
&lt;br /&gt;
*The reviewer is located by email and matched to an AssignmentParticipant.&lt;br /&gt;
*The team is located by name within the assignment.&lt;br /&gt;
*A mapping is created if both are found.&lt;br /&gt;
&lt;br /&gt;
This allows instructors to pre‑compute assignments externally and import them directly.&lt;br /&gt;
&lt;br /&gt;
= Dynamic Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Dynamic assignment is initiated by a reviewer requesting a new review. Instead of pre‑assigning all reviews, the system allocates reviews on demand, which helps avoid the common problem of students failing to complete their assigned reviews.&lt;br /&gt;
&lt;br /&gt;
Two dynamic strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Least‑Reviewed Submission Strategy ==&lt;br /&gt;
'''Controller action:''' request_review_fewest  &lt;br /&gt;
'''Strategy:''' LeastReviewedSubmissionStrategy&lt;br /&gt;
&lt;br /&gt;
This strategy assigns the reviewer to the team whose submission has received the fewest completed reviews so far.&lt;br /&gt;
&lt;br /&gt;
'''Eligibility rules:'''&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to their own team.&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to a team they have already reviewed.&lt;br /&gt;
&lt;br /&gt;
Among eligible teams, the one with the lowest review count is selected.&lt;br /&gt;
&lt;br /&gt;
This ensures a balanced distribution of reviews across submissions.&lt;br /&gt;
&lt;br /&gt;
== Topic‑Balanced Dynamic Assignment ==&lt;br /&gt;
'''Controller action:''' request_review_topic_balance  &lt;br /&gt;
'''Strategy:''' LeastReviewedTopicStrategy&lt;br /&gt;
&lt;br /&gt;
When an assignment uses topics, reviewers may choose which topics they are willing to review. The system then assigns a team within an eligible topic, subject to fairness constraints.&lt;br /&gt;
&lt;br /&gt;
'''Algorithm:'''&lt;br /&gt;
&lt;br /&gt;
Review counts are aggregated per topic.&lt;br /&gt;
Let min_count be the smallest number of reviews received by any topic.&lt;br /&gt;
A topic is eligible if its review count is within '''k''' of min_count.&lt;br /&gt;
The first eligible topic (deterministically) is selected.&lt;br /&gt;
Within that topic, the reviewer is assigned to a team that:&lt;br /&gt;
*is not their own team,&lt;br /&gt;
*has not already been reviewed by them,&lt;br /&gt;
*has the fewest reviews among eligible teams.&lt;br /&gt;
&lt;br /&gt;
This ensures:&lt;br /&gt;
*Reviewers can restrict themselves to topics they prefer.&lt;br /&gt;
*Topics remain approximately balanced, with fairness controlled by the threshold k.&lt;br /&gt;
&lt;br /&gt;
= Calibration Review Assignment =&lt;br /&gt;
&lt;br /&gt;
== Calibration Round‑Robin ==&lt;br /&gt;
'''Controller action:''' assign_calibration_artifacts&lt;br /&gt;
&lt;br /&gt;
This assigns calibration reviews to all reviewers using a round‑robin strategy. Calibration reviews are typically instructor‑provided artifacts used to train or benchmark reviewers.&lt;br /&gt;
&lt;br /&gt;
= Deleting Review Mappings =&lt;br /&gt;
&lt;br /&gt;
== Delete a Single Mapping ==&lt;br /&gt;
'''Controller action:''' destroy&lt;br /&gt;
&lt;br /&gt;
Deletes a specific review mapping by ID.&lt;br /&gt;
&lt;br /&gt;
== Delete All Reviews for a Reviewer ==&lt;br /&gt;
'''Controller action:''' delete_all_for_reviewer&lt;br /&gt;
&lt;br /&gt;
Removes all review mappings associated with a given reviewer. Useful when resetting or reassigning work.&lt;br /&gt;
&lt;br /&gt;
= Instructor Grading of Reviews =&lt;br /&gt;
&lt;br /&gt;
'''Controller action:''' grade_review&lt;br /&gt;
&lt;br /&gt;
Instructors may grade individual reviews. This action:&lt;br /&gt;
&lt;br /&gt;
*Locates the review mapping&lt;br /&gt;
*Records the instructor’s grade and comment&lt;br /&gt;
*Returns a success message&lt;br /&gt;
&lt;br /&gt;
= Strategy Classes =&lt;br /&gt;
&lt;br /&gt;
All strategies inherit from ReviewMappingStrategies::BaseStrategy, which defines two abstract methods:&lt;br /&gt;
&lt;br /&gt;
*each_review_pair — for static strategies&lt;br /&gt;
&lt;br /&gt;
*assign_one — for dynamic strategies&lt;br /&gt;
&lt;br /&gt;
Each concrete strategy implements one of these depending on its purpose.&lt;br /&gt;
&lt;br /&gt;
== CsvImportStrategy ==&lt;br /&gt;
Implements each_review_pair by reading reviewer–team pairs from a CSV file.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedSubmissionStrategy ==&lt;br /&gt;
Implements assign_one by selecting the eligible team with the fewest reviews.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedTopicStrategy ==&lt;br /&gt;
Implements assign_one with topic‑level fairness constraints and a threshold k.&lt;br /&gt;
&lt;br /&gt;
== RoundRobinStrategy ==&lt;br /&gt;
Implements each_review_pair by cycling through reviewers and pairing them with teams.&lt;br /&gt;
&lt;br /&gt;
= Common Eligibility Rules =&lt;br /&gt;
&lt;br /&gt;
Across all dynamic strategies, a reviewer is never assigned to:&lt;br /&gt;
&lt;br /&gt;
*Their own team&lt;br /&gt;
*A team they have already reviewed&lt;br /&gt;
*A team outside the selected topic (for topic‑based assignment)&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167527</id>
		<title>Reviewer Assignment implementation</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167527"/>
		<updated>2026-03-15T20:03:33Z</updated>

		<summary type="html">&lt;p&gt;Admin: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Overview =&lt;br /&gt;
The review‑assignment subsystem supports both '''static''' and '''dynamic''' allocation of reviews within an assignment. The goal is to give instructors flexible control over how reviewers are matched to submissions, while also supporting on‑demand, fairness‑aware assignment for students who request reviews dynamically.&lt;br /&gt;
&lt;br /&gt;
This page documents the behavior implemented in ReviewMappingsController and the strategy classes under ReviewMappingStrategies.&lt;br /&gt;
&lt;br /&gt;
= Static Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Static assignment is initiated by the instructor and produces a complete set of review mappings in a single operation. Three static strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Round‑Robin Assignment ==&lt;br /&gt;
'''Controller action:''' assign_round_robin&lt;br /&gt;
&lt;br /&gt;
This strategy cycles through all reviewers in order and assigns each team to the next reviewer in the cycle. Only participants who are eligible to review (can_review?) are included.&lt;br /&gt;
&lt;br /&gt;
'''Characteristics:'''&lt;br /&gt;
&lt;br /&gt;
*Deterministic ordering&lt;br /&gt;
*Each team receives exactly one reviewer&lt;br /&gt;
*Reviewers are reused as needed&lt;br /&gt;
*No workload balancing beyond the simple cycle&lt;br /&gt;
&lt;br /&gt;
== Random Assignment ==&lt;br /&gt;
'''Controller action:''' assign_random&lt;br /&gt;
&lt;br /&gt;
This strategy assigns reviewers to teams randomly. The distribution is determined by the logic inside ReviewMappingHandler.&lt;br /&gt;
&lt;br /&gt;
== CSV‑Based Assignment ==&lt;br /&gt;
'''Controller action:''' assign_from_csv&lt;br /&gt;
&lt;br /&gt;
This strategy imports reviewer–team pairs from an uploaded CSV file.&lt;br /&gt;
&lt;br /&gt;
'''Expected CSV format:'''&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
reviewer_email,team_name&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
For each row:&lt;br /&gt;
&lt;br /&gt;
*The reviewer is located by email and matched to an AssignmentParticipant.&lt;br /&gt;
*The team is located by name within the assignment.&lt;br /&gt;
*A mapping is created if both are found.&lt;br /&gt;
&lt;br /&gt;
This allows instructors to pre‑compute assignments externally and import them directly.&lt;br /&gt;
&lt;br /&gt;
= Dynamic Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Dynamic assignment is initiated by a reviewer requesting a new review. Instead of pre‑assigning all reviews, the system allocates reviews on demand, which helps avoid the common problem of students failing to complete their assigned reviews.&lt;br /&gt;
&lt;br /&gt;
Two dynamic strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Least‑Reviewed Submission Strategy ==&lt;br /&gt;
'''Controller action:''' request_review_fewest  &lt;br /&gt;
'''Strategy:''' LeastReviewedSubmissionStrategy&lt;br /&gt;
&lt;br /&gt;
This strategy assigns the reviewer to the team whose submission has received the fewest completed reviews so far.&lt;br /&gt;
&lt;br /&gt;
'''Eligibility rules:'''&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to their own team.&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to a team they have already reviewed.&lt;br /&gt;
&lt;br /&gt;
Among eligible teams, the one with the lowest review count is selected.&lt;br /&gt;
&lt;br /&gt;
This ensures a balanced distribution of reviews across submissions.&lt;br /&gt;
&lt;br /&gt;
== Topic‑Balanced Dynamic Assignment ==&lt;br /&gt;
'''Controller action:''' request_review_topic_balance  &lt;br /&gt;
'''Strategy:''' LeastReviewedTopicStrategy&lt;br /&gt;
&lt;br /&gt;
When an assignment uses topics, reviewers may choose which topics they are willing to review. The system then assigns a team within an eligible topic, subject to fairness constraints.&lt;br /&gt;
&lt;br /&gt;
'''Algorithm:'''&lt;br /&gt;
&lt;br /&gt;
Review counts are aggregated per topic.&lt;br /&gt;
Let min_count be the smallest number of reviews received by any topic.&lt;br /&gt;
A topic is eligible if its review count is within '''k''' of min_count.&lt;br /&gt;
The first eligible topic (deterministically) is selected.&lt;br /&gt;
Within that topic, the reviewer is assigned to a team that:&lt;br /&gt;
*is not their own team,&lt;br /&gt;
*has not already been reviewed by them,&lt;br /&gt;
*has the fewest reviews among eligible teams.&lt;br /&gt;
&lt;br /&gt;
This ensures:&lt;br /&gt;
*Reviewers can restrict themselves to topics they prefer.&lt;br /&gt;
*Topics remain approximately balanced, with fairness controlled by the threshold k.&lt;br /&gt;
&lt;br /&gt;
= Calibration Review Assignment =&lt;br /&gt;
&lt;br /&gt;
== Calibration Round‑Robin ==&lt;br /&gt;
'''Controller action:''' assign_calibration_artifacts&lt;br /&gt;
&lt;br /&gt;
This assigns calibration reviews to all reviewers using a round‑robin strategy. Calibration reviews are typically instructor‑provided artifacts used to train or benchmark reviewers.&lt;br /&gt;
&lt;br /&gt;
= Deleting Review Mappings =&lt;br /&gt;
&lt;br /&gt;
== Delete a Single Mapping ==&lt;br /&gt;
'''Controller action:''' destroy&lt;br /&gt;
&lt;br /&gt;
Deletes a specific review mapping by ID.&lt;br /&gt;
&lt;br /&gt;
== Delete All Reviews for a Reviewer ==&lt;br /&gt;
'''Controller action:''' delete_all_for_reviewer&lt;br /&gt;
&lt;br /&gt;
Removes all review mappings associated with a given reviewer. Useful when resetting or reassigning work.&lt;br /&gt;
&lt;br /&gt;
= Instructor Grading of Reviews =&lt;br /&gt;
&lt;br /&gt;
'''Controller action:''' grade_review&lt;br /&gt;
&lt;br /&gt;
Instructors may grade individual reviews. This action:&lt;br /&gt;
&lt;br /&gt;
*Locates the review mapping&lt;br /&gt;
*Records the instructor’s grade and comment&lt;br /&gt;
*Returns a success message&lt;br /&gt;
&lt;br /&gt;
= Strategy Classes =&lt;br /&gt;
&lt;br /&gt;
All strategies inherit from ReviewMappingStrategies::BaseStrategy, which defines two abstract methods:&lt;br /&gt;
&lt;br /&gt;
*each_review_pair — for static strategies&lt;br /&gt;
&lt;br /&gt;
*assign_one — for dynamic strategies&lt;br /&gt;
&lt;br /&gt;
Each concrete strategy implements one of these depending on its purpose.&lt;br /&gt;
&lt;br /&gt;
== CsvImportStrategy ==&lt;br /&gt;
Implements each_review_pair by reading reviewer–team pairs from a CSV file.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedSubmissionStrategy ==&lt;br /&gt;
Implements assign_one by selecting the eligible team with the fewest reviews.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedTopicStrategy ==&lt;br /&gt;
Implements assign_one with topic‑level fairness constraints and a threshold k.&lt;br /&gt;
&lt;br /&gt;
== RoundRobinStrategy ==&lt;br /&gt;
Implements each_review_pair by cycling through reviewers and pairing them with teams.&lt;br /&gt;
&lt;br /&gt;
= Common Eligibility Rules =&lt;br /&gt;
&lt;br /&gt;
Across all dynamic strategies, a reviewer is never assigned to:&lt;br /&gt;
&lt;br /&gt;
*Their own team&lt;br /&gt;
*A team they have already reviewed&lt;br /&gt;
*A team outside the selected topic (for topic‑based assignment)&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167526</id>
		<title>Reviewer Assignment implementation</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167526"/>
		<updated>2026-03-15T20:03:09Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Common Eligibility Rules */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Review Assignment System Overview&lt;br /&gt;
The review‑assignment subsystem supports both '''static''' and '''dynamic''' allocation of reviews within an assignment. The goal is to give instructors flexible control over how reviewers are matched to submissions, while also supporting on‑demand, fairness‑aware assignment for students who request reviews dynamically.&lt;br /&gt;
&lt;br /&gt;
This page documents the behavior implemented in ReviewMappingsController and the strategy classes under ReviewMappingStrategies.&lt;br /&gt;
&lt;br /&gt;
= Static Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Static assignment is initiated by the instructor and produces a complete set of review mappings in a single operation. Three static strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Round‑Robin Assignment ==&lt;br /&gt;
'''Controller action:''' assign_round_robin&lt;br /&gt;
&lt;br /&gt;
This strategy cycles through all reviewers in order and assigns each team to the next reviewer in the cycle. Only participants who are eligible to review (can_review?) are included.&lt;br /&gt;
&lt;br /&gt;
'''Characteristics:'''&lt;br /&gt;
&lt;br /&gt;
*Deterministic ordering&lt;br /&gt;
*Each team receives exactly one reviewer&lt;br /&gt;
*Reviewers are reused as needed&lt;br /&gt;
*No workload balancing beyond the simple cycle&lt;br /&gt;
&lt;br /&gt;
== Random Assignment ==&lt;br /&gt;
'''Controller action:''' assign_random&lt;br /&gt;
&lt;br /&gt;
This strategy assigns reviewers to teams randomly. The distribution is determined by the logic inside ReviewMappingHandler.&lt;br /&gt;
&lt;br /&gt;
== CSV‑Based Assignment ==&lt;br /&gt;
'''Controller action:''' assign_from_csv&lt;br /&gt;
&lt;br /&gt;
This strategy imports reviewer–team pairs from an uploaded CSV file.&lt;br /&gt;
&lt;br /&gt;
'''Expected CSV format:'''&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
reviewer_email,team_name&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
For each row:&lt;br /&gt;
&lt;br /&gt;
*The reviewer is located by email and matched to an AssignmentParticipant.&lt;br /&gt;
*The team is located by name within the assignment.&lt;br /&gt;
*A mapping is created if both are found.&lt;br /&gt;
&lt;br /&gt;
This allows instructors to pre‑compute assignments externally and import them directly.&lt;br /&gt;
&lt;br /&gt;
= Dynamic Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Dynamic assignment is initiated by a reviewer requesting a new review. Instead of pre‑assigning all reviews, the system allocates reviews on demand, which helps avoid the common problem of students failing to complete their assigned reviews.&lt;br /&gt;
&lt;br /&gt;
Two dynamic strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Least‑Reviewed Submission Strategy ==&lt;br /&gt;
'''Controller action:''' request_review_fewest  &lt;br /&gt;
'''Strategy:''' LeastReviewedSubmissionStrategy&lt;br /&gt;
&lt;br /&gt;
This strategy assigns the reviewer to the team whose submission has received the fewest completed reviews so far.&lt;br /&gt;
&lt;br /&gt;
'''Eligibility rules:'''&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to their own team.&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to a team they have already reviewed.&lt;br /&gt;
&lt;br /&gt;
Among eligible teams, the one with the lowest review count is selected.&lt;br /&gt;
&lt;br /&gt;
This ensures a balanced distribution of reviews across submissions.&lt;br /&gt;
&lt;br /&gt;
== Topic‑Balanced Dynamic Assignment ==&lt;br /&gt;
'''Controller action:''' request_review_topic_balance  &lt;br /&gt;
'''Strategy:''' LeastReviewedTopicStrategy&lt;br /&gt;
&lt;br /&gt;
When an assignment uses topics, reviewers may choose which topics they are willing to review. The system then assigns a team within an eligible topic, subject to fairness constraints.&lt;br /&gt;
&lt;br /&gt;
'''Algorithm:'''&lt;br /&gt;
&lt;br /&gt;
Review counts are aggregated per topic.&lt;br /&gt;
Let min_count be the smallest number of reviews received by any topic.&lt;br /&gt;
A topic is eligible if its review count is within '''k''' of min_count.&lt;br /&gt;
The first eligible topic (deterministically) is selected.&lt;br /&gt;
Within that topic, the reviewer is assigned to a team that:&lt;br /&gt;
*is not their own team,&lt;br /&gt;
*has not already been reviewed by them,&lt;br /&gt;
*has the fewest reviews among eligible teams.&lt;br /&gt;
&lt;br /&gt;
This ensures:&lt;br /&gt;
*Reviewers can restrict themselves to topics they prefer.&lt;br /&gt;
*Topics remain approximately balanced, with fairness controlled by the threshold k.&lt;br /&gt;
&lt;br /&gt;
= Calibration Review Assignment =&lt;br /&gt;
&lt;br /&gt;
== Calibration Round‑Robin ==&lt;br /&gt;
'''Controller action:''' assign_calibration_artifacts&lt;br /&gt;
&lt;br /&gt;
This assigns calibration reviews to all reviewers using a round‑robin strategy. Calibration reviews are typically instructor‑provided artifacts used to train or benchmark reviewers.&lt;br /&gt;
&lt;br /&gt;
= Deleting Review Mappings =&lt;br /&gt;
&lt;br /&gt;
== Delete a Single Mapping ==&lt;br /&gt;
'''Controller action:''' destroy&lt;br /&gt;
&lt;br /&gt;
Deletes a specific review mapping by ID.&lt;br /&gt;
&lt;br /&gt;
== Delete All Reviews for a Reviewer ==&lt;br /&gt;
'''Controller action:''' delete_all_for_reviewer&lt;br /&gt;
&lt;br /&gt;
Removes all review mappings associated with a given reviewer. Useful when resetting or reassigning work.&lt;br /&gt;
&lt;br /&gt;
= Instructor Grading of Reviews =&lt;br /&gt;
&lt;br /&gt;
'''Controller action:''' grade_review&lt;br /&gt;
&lt;br /&gt;
Instructors may grade individual reviews. This action:&lt;br /&gt;
&lt;br /&gt;
*Locates the review mapping&lt;br /&gt;
*Records the instructor’s grade and comment&lt;br /&gt;
*Returns a success message&lt;br /&gt;
&lt;br /&gt;
= Strategy Classes =&lt;br /&gt;
&lt;br /&gt;
All strategies inherit from ReviewMappingStrategies::BaseStrategy, which defines two abstract methods:&lt;br /&gt;
&lt;br /&gt;
*each_review_pair — for static strategies&lt;br /&gt;
&lt;br /&gt;
*assign_one — for dynamic strategies&lt;br /&gt;
&lt;br /&gt;
Each concrete strategy implements one of these depending on its purpose.&lt;br /&gt;
&lt;br /&gt;
== CsvImportStrategy ==&lt;br /&gt;
Implements each_review_pair by reading reviewer–team pairs from a CSV file.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedSubmissionStrategy ==&lt;br /&gt;
Implements assign_one by selecting the eligible team with the fewest reviews.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedTopicStrategy ==&lt;br /&gt;
Implements assign_one with topic‑level fairness constraints and a threshold k.&lt;br /&gt;
&lt;br /&gt;
== RoundRobinStrategy ==&lt;br /&gt;
Implements each_review_pair by cycling through reviewers and pairing them with teams.&lt;br /&gt;
&lt;br /&gt;
= Common Eligibility Rules =&lt;br /&gt;
&lt;br /&gt;
Across all dynamic strategies, a reviewer is never assigned to:&lt;br /&gt;
&lt;br /&gt;
*Their own team&lt;br /&gt;
*A team they have already reviewed&lt;br /&gt;
*A team outside the selected topic (for topic‑based assignment)&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167525</id>
		<title>Reviewer Assignment implementation</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167525"/>
		<updated>2026-03-15T20:02:49Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Strategy Classes */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Review Assignment System Overview&lt;br /&gt;
The review‑assignment subsystem supports both '''static''' and '''dynamic''' allocation of reviews within an assignment. The goal is to give instructors flexible control over how reviewers are matched to submissions, while also supporting on‑demand, fairness‑aware assignment for students who request reviews dynamically.&lt;br /&gt;
&lt;br /&gt;
This page documents the behavior implemented in ReviewMappingsController and the strategy classes under ReviewMappingStrategies.&lt;br /&gt;
&lt;br /&gt;
= Static Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Static assignment is initiated by the instructor and produces a complete set of review mappings in a single operation. Three static strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Round‑Robin Assignment ==&lt;br /&gt;
'''Controller action:''' assign_round_robin&lt;br /&gt;
&lt;br /&gt;
This strategy cycles through all reviewers in order and assigns each team to the next reviewer in the cycle. Only participants who are eligible to review (can_review?) are included.&lt;br /&gt;
&lt;br /&gt;
'''Characteristics:'''&lt;br /&gt;
&lt;br /&gt;
*Deterministic ordering&lt;br /&gt;
*Each team receives exactly one reviewer&lt;br /&gt;
*Reviewers are reused as needed&lt;br /&gt;
*No workload balancing beyond the simple cycle&lt;br /&gt;
&lt;br /&gt;
== Random Assignment ==&lt;br /&gt;
'''Controller action:''' assign_random&lt;br /&gt;
&lt;br /&gt;
This strategy assigns reviewers to teams randomly. The distribution is determined by the logic inside ReviewMappingHandler.&lt;br /&gt;
&lt;br /&gt;
== CSV‑Based Assignment ==&lt;br /&gt;
'''Controller action:''' assign_from_csv&lt;br /&gt;
&lt;br /&gt;
This strategy imports reviewer–team pairs from an uploaded CSV file.&lt;br /&gt;
&lt;br /&gt;
'''Expected CSV format:'''&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
reviewer_email,team_name&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
For each row:&lt;br /&gt;
&lt;br /&gt;
*The reviewer is located by email and matched to an AssignmentParticipant.&lt;br /&gt;
*The team is located by name within the assignment.&lt;br /&gt;
*A mapping is created if both are found.&lt;br /&gt;
&lt;br /&gt;
This allows instructors to pre‑compute assignments externally and import them directly.&lt;br /&gt;
&lt;br /&gt;
= Dynamic Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Dynamic assignment is initiated by a reviewer requesting a new review. Instead of pre‑assigning all reviews, the system allocates reviews on demand, which helps avoid the common problem of students failing to complete their assigned reviews.&lt;br /&gt;
&lt;br /&gt;
Two dynamic strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Least‑Reviewed Submission Strategy ==&lt;br /&gt;
'''Controller action:''' request_review_fewest  &lt;br /&gt;
'''Strategy:''' LeastReviewedSubmissionStrategy&lt;br /&gt;
&lt;br /&gt;
This strategy assigns the reviewer to the team whose submission has received the fewest completed reviews so far.&lt;br /&gt;
&lt;br /&gt;
'''Eligibility rules:'''&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to their own team.&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to a team they have already reviewed.&lt;br /&gt;
&lt;br /&gt;
Among eligible teams, the one with the lowest review count is selected.&lt;br /&gt;
&lt;br /&gt;
This ensures a balanced distribution of reviews across submissions.&lt;br /&gt;
&lt;br /&gt;
== Topic‑Balanced Dynamic Assignment ==&lt;br /&gt;
'''Controller action:''' request_review_topic_balance  &lt;br /&gt;
'''Strategy:''' LeastReviewedTopicStrategy&lt;br /&gt;
&lt;br /&gt;
When an assignment uses topics, reviewers may choose which topics they are willing to review. The system then assigns a team within an eligible topic, subject to fairness constraints.&lt;br /&gt;
&lt;br /&gt;
'''Algorithm:'''&lt;br /&gt;
&lt;br /&gt;
Review counts are aggregated per topic.&lt;br /&gt;
Let min_count be the smallest number of reviews received by any topic.&lt;br /&gt;
A topic is eligible if its review count is within '''k''' of min_count.&lt;br /&gt;
The first eligible topic (deterministically) is selected.&lt;br /&gt;
Within that topic, the reviewer is assigned to a team that:&lt;br /&gt;
*is not their own team,&lt;br /&gt;
*has not already been reviewed by them,&lt;br /&gt;
*has the fewest reviews among eligible teams.&lt;br /&gt;
&lt;br /&gt;
This ensures:&lt;br /&gt;
*Reviewers can restrict themselves to topics they prefer.&lt;br /&gt;
*Topics remain approximately balanced, with fairness controlled by the threshold k.&lt;br /&gt;
&lt;br /&gt;
= Calibration Review Assignment =&lt;br /&gt;
&lt;br /&gt;
== Calibration Round‑Robin ==&lt;br /&gt;
'''Controller action:''' assign_calibration_artifacts&lt;br /&gt;
&lt;br /&gt;
This assigns calibration reviews to all reviewers using a round‑robin strategy. Calibration reviews are typically instructor‑provided artifacts used to train or benchmark reviewers.&lt;br /&gt;
&lt;br /&gt;
= Deleting Review Mappings =&lt;br /&gt;
&lt;br /&gt;
== Delete a Single Mapping ==&lt;br /&gt;
'''Controller action:''' destroy&lt;br /&gt;
&lt;br /&gt;
Deletes a specific review mapping by ID.&lt;br /&gt;
&lt;br /&gt;
== Delete All Reviews for a Reviewer ==&lt;br /&gt;
'''Controller action:''' delete_all_for_reviewer&lt;br /&gt;
&lt;br /&gt;
Removes all review mappings associated with a given reviewer. Useful when resetting or reassigning work.&lt;br /&gt;
&lt;br /&gt;
= Instructor Grading of Reviews =&lt;br /&gt;
&lt;br /&gt;
'''Controller action:''' grade_review&lt;br /&gt;
&lt;br /&gt;
Instructors may grade individual reviews. This action:&lt;br /&gt;
&lt;br /&gt;
*Locates the review mapping&lt;br /&gt;
*Records the instructor’s grade and comment&lt;br /&gt;
*Returns a success message&lt;br /&gt;
&lt;br /&gt;
= Strategy Classes =&lt;br /&gt;
&lt;br /&gt;
All strategies inherit from ReviewMappingStrategies::BaseStrategy, which defines two abstract methods:&lt;br /&gt;
&lt;br /&gt;
*each_review_pair — for static strategies&lt;br /&gt;
&lt;br /&gt;
*assign_one — for dynamic strategies&lt;br /&gt;
&lt;br /&gt;
Each concrete strategy implements one of these depending on its purpose.&lt;br /&gt;
&lt;br /&gt;
== CsvImportStrategy ==&lt;br /&gt;
Implements each_review_pair by reading reviewer–team pairs from a CSV file.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedSubmissionStrategy ==&lt;br /&gt;
Implements assign_one by selecting the eligible team with the fewest reviews.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedTopicStrategy ==&lt;br /&gt;
Implements assign_one with topic‑level fairness constraints and a threshold k.&lt;br /&gt;
&lt;br /&gt;
== RoundRobinStrategy ==&lt;br /&gt;
Implements each_review_pair by cycling through reviewers and pairing them with teams.&lt;br /&gt;
&lt;br /&gt;
= Common Eligibility Rules =&lt;br /&gt;
&lt;br /&gt;
Across all dynamic strategies, a reviewer is never assigned to:&lt;br /&gt;
&lt;br /&gt;
Their own team&lt;br /&gt;
&lt;br /&gt;
A team they have already reviewed&lt;br /&gt;
&lt;br /&gt;
A team outside the selected topic (for topic‑based assignment)&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167524</id>
		<title>Reviewer Assignment implementation</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167524"/>
		<updated>2026-03-15T20:02:25Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Instructor Grading of Reviews */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Review Assignment System Overview&lt;br /&gt;
The review‑assignment subsystem supports both '''static''' and '''dynamic''' allocation of reviews within an assignment. The goal is to give instructors flexible control over how reviewers are matched to submissions, while also supporting on‑demand, fairness‑aware assignment for students who request reviews dynamically.&lt;br /&gt;
&lt;br /&gt;
This page documents the behavior implemented in ReviewMappingsController and the strategy classes under ReviewMappingStrategies.&lt;br /&gt;
&lt;br /&gt;
= Static Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Static assignment is initiated by the instructor and produces a complete set of review mappings in a single operation. Three static strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Round‑Robin Assignment ==&lt;br /&gt;
'''Controller action:''' assign_round_robin&lt;br /&gt;
&lt;br /&gt;
This strategy cycles through all reviewers in order and assigns each team to the next reviewer in the cycle. Only participants who are eligible to review (can_review?) are included.&lt;br /&gt;
&lt;br /&gt;
'''Characteristics:'''&lt;br /&gt;
&lt;br /&gt;
*Deterministic ordering&lt;br /&gt;
*Each team receives exactly one reviewer&lt;br /&gt;
*Reviewers are reused as needed&lt;br /&gt;
*No workload balancing beyond the simple cycle&lt;br /&gt;
&lt;br /&gt;
== Random Assignment ==&lt;br /&gt;
'''Controller action:''' assign_random&lt;br /&gt;
&lt;br /&gt;
This strategy assigns reviewers to teams randomly. The distribution is determined by the logic inside ReviewMappingHandler.&lt;br /&gt;
&lt;br /&gt;
== CSV‑Based Assignment ==&lt;br /&gt;
'''Controller action:''' assign_from_csv&lt;br /&gt;
&lt;br /&gt;
This strategy imports reviewer–team pairs from an uploaded CSV file.&lt;br /&gt;
&lt;br /&gt;
'''Expected CSV format:'''&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
reviewer_email,team_name&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
For each row:&lt;br /&gt;
&lt;br /&gt;
*The reviewer is located by email and matched to an AssignmentParticipant.&lt;br /&gt;
*The team is located by name within the assignment.&lt;br /&gt;
*A mapping is created if both are found.&lt;br /&gt;
&lt;br /&gt;
This allows instructors to pre‑compute assignments externally and import them directly.&lt;br /&gt;
&lt;br /&gt;
= Dynamic Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Dynamic assignment is initiated by a reviewer requesting a new review. Instead of pre‑assigning all reviews, the system allocates reviews on demand, which helps avoid the common problem of students failing to complete their assigned reviews.&lt;br /&gt;
&lt;br /&gt;
Two dynamic strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Least‑Reviewed Submission Strategy ==&lt;br /&gt;
'''Controller action:''' request_review_fewest  &lt;br /&gt;
'''Strategy:''' LeastReviewedSubmissionStrategy&lt;br /&gt;
&lt;br /&gt;
This strategy assigns the reviewer to the team whose submission has received the fewest completed reviews so far.&lt;br /&gt;
&lt;br /&gt;
'''Eligibility rules:'''&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to their own team.&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to a team they have already reviewed.&lt;br /&gt;
&lt;br /&gt;
Among eligible teams, the one with the lowest review count is selected.&lt;br /&gt;
&lt;br /&gt;
This ensures a balanced distribution of reviews across submissions.&lt;br /&gt;
&lt;br /&gt;
== Topic‑Balanced Dynamic Assignment ==&lt;br /&gt;
'''Controller action:''' request_review_topic_balance  &lt;br /&gt;
'''Strategy:''' LeastReviewedTopicStrategy&lt;br /&gt;
&lt;br /&gt;
When an assignment uses topics, reviewers may choose which topics they are willing to review. The system then assigns a team within an eligible topic, subject to fairness constraints.&lt;br /&gt;
&lt;br /&gt;
'''Algorithm:'''&lt;br /&gt;
&lt;br /&gt;
Review counts are aggregated per topic.&lt;br /&gt;
Let min_count be the smallest number of reviews received by any topic.&lt;br /&gt;
A topic is eligible if its review count is within '''k''' of min_count.&lt;br /&gt;
The first eligible topic (deterministically) is selected.&lt;br /&gt;
Within that topic, the reviewer is assigned to a team that:&lt;br /&gt;
*is not their own team,&lt;br /&gt;
*has not already been reviewed by them,&lt;br /&gt;
*has the fewest reviews among eligible teams.&lt;br /&gt;
&lt;br /&gt;
This ensures:&lt;br /&gt;
*Reviewers can restrict themselves to topics they prefer.&lt;br /&gt;
*Topics remain approximately balanced, with fairness controlled by the threshold k.&lt;br /&gt;
&lt;br /&gt;
= Calibration Review Assignment =&lt;br /&gt;
&lt;br /&gt;
== Calibration Round‑Robin ==&lt;br /&gt;
'''Controller action:''' assign_calibration_artifacts&lt;br /&gt;
&lt;br /&gt;
This assigns calibration reviews to all reviewers using a round‑robin strategy. Calibration reviews are typically instructor‑provided artifacts used to train or benchmark reviewers.&lt;br /&gt;
&lt;br /&gt;
= Deleting Review Mappings =&lt;br /&gt;
&lt;br /&gt;
== Delete a Single Mapping ==&lt;br /&gt;
'''Controller action:''' destroy&lt;br /&gt;
&lt;br /&gt;
Deletes a specific review mapping by ID.&lt;br /&gt;
&lt;br /&gt;
== Delete All Reviews for a Reviewer ==&lt;br /&gt;
'''Controller action:''' delete_all_for_reviewer&lt;br /&gt;
&lt;br /&gt;
Removes all review mappings associated with a given reviewer. Useful when resetting or reassigning work.&lt;br /&gt;
&lt;br /&gt;
= Instructor Grading of Reviews =&lt;br /&gt;
&lt;br /&gt;
'''Controller action:''' grade_review&lt;br /&gt;
&lt;br /&gt;
Instructors may grade individual reviews. This action:&lt;br /&gt;
&lt;br /&gt;
*Locates the review mapping&lt;br /&gt;
*Records the instructor’s grade and comment&lt;br /&gt;
*Returns a success message&lt;br /&gt;
&lt;br /&gt;
= Strategy Classes =&lt;br /&gt;
&lt;br /&gt;
All strategies inherit from ReviewMappingStrategies::BaseStrategy, which defines two abstract methods:&lt;br /&gt;
&lt;br /&gt;
each_review_pair — for static strategies&lt;br /&gt;
&lt;br /&gt;
assign_one — for dynamic strategies&lt;br /&gt;
&lt;br /&gt;
Each concrete strategy implements one of these depending on its purpose.&lt;br /&gt;
&lt;br /&gt;
== CsvImportStrategy ==&lt;br /&gt;
Implements each_review_pair by reading reviewer–team pairs from a CSV file.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedSubmissionStrategy ==&lt;br /&gt;
Implements assign_one by selecting the eligible team with the fewest reviews.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedTopicStrategy ==&lt;br /&gt;
Implements assign_one with topic‑level fairness constraints and a threshold k.&lt;br /&gt;
&lt;br /&gt;
== RoundRobinStrategy ==&lt;br /&gt;
Implements each_review_pair by cycling through reviewers and pairing them with teams.&lt;br /&gt;
&lt;br /&gt;
= Common Eligibility Rules =&lt;br /&gt;
&lt;br /&gt;
Across all dynamic strategies, a reviewer is never assigned to:&lt;br /&gt;
&lt;br /&gt;
Their own team&lt;br /&gt;
&lt;br /&gt;
A team they have already reviewed&lt;br /&gt;
&lt;br /&gt;
A team outside the selected topic (for topic‑based assignment)&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167523</id>
		<title>Reviewer Assignment implementation</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167523"/>
		<updated>2026-03-15T20:02:03Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Topic‑Balanced Dynamic Assignment */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Review Assignment System Overview&lt;br /&gt;
The review‑assignment subsystem supports both '''static''' and '''dynamic''' allocation of reviews within an assignment. The goal is to give instructors flexible control over how reviewers are matched to submissions, while also supporting on‑demand, fairness‑aware assignment for students who request reviews dynamically.&lt;br /&gt;
&lt;br /&gt;
This page documents the behavior implemented in ReviewMappingsController and the strategy classes under ReviewMappingStrategies.&lt;br /&gt;
&lt;br /&gt;
= Static Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Static assignment is initiated by the instructor and produces a complete set of review mappings in a single operation. Three static strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Round‑Robin Assignment ==&lt;br /&gt;
'''Controller action:''' assign_round_robin&lt;br /&gt;
&lt;br /&gt;
This strategy cycles through all reviewers in order and assigns each team to the next reviewer in the cycle. Only participants who are eligible to review (can_review?) are included.&lt;br /&gt;
&lt;br /&gt;
'''Characteristics:'''&lt;br /&gt;
&lt;br /&gt;
*Deterministic ordering&lt;br /&gt;
*Each team receives exactly one reviewer&lt;br /&gt;
*Reviewers are reused as needed&lt;br /&gt;
*No workload balancing beyond the simple cycle&lt;br /&gt;
&lt;br /&gt;
== Random Assignment ==&lt;br /&gt;
'''Controller action:''' assign_random&lt;br /&gt;
&lt;br /&gt;
This strategy assigns reviewers to teams randomly. The distribution is determined by the logic inside ReviewMappingHandler.&lt;br /&gt;
&lt;br /&gt;
== CSV‑Based Assignment ==&lt;br /&gt;
'''Controller action:''' assign_from_csv&lt;br /&gt;
&lt;br /&gt;
This strategy imports reviewer–team pairs from an uploaded CSV file.&lt;br /&gt;
&lt;br /&gt;
'''Expected CSV format:'''&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
reviewer_email,team_name&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
For each row:&lt;br /&gt;
&lt;br /&gt;
*The reviewer is located by email and matched to an AssignmentParticipant.&lt;br /&gt;
*The team is located by name within the assignment.&lt;br /&gt;
*A mapping is created if both are found.&lt;br /&gt;
&lt;br /&gt;
This allows instructors to pre‑compute assignments externally and import them directly.&lt;br /&gt;
&lt;br /&gt;
= Dynamic Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Dynamic assignment is initiated by a reviewer requesting a new review. Instead of pre‑assigning all reviews, the system allocates reviews on demand, which helps avoid the common problem of students failing to complete their assigned reviews.&lt;br /&gt;
&lt;br /&gt;
Two dynamic strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Least‑Reviewed Submission Strategy ==&lt;br /&gt;
'''Controller action:''' request_review_fewest  &lt;br /&gt;
'''Strategy:''' LeastReviewedSubmissionStrategy&lt;br /&gt;
&lt;br /&gt;
This strategy assigns the reviewer to the team whose submission has received the fewest completed reviews so far.&lt;br /&gt;
&lt;br /&gt;
'''Eligibility rules:'''&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to their own team.&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to a team they have already reviewed.&lt;br /&gt;
&lt;br /&gt;
Among eligible teams, the one with the lowest review count is selected.&lt;br /&gt;
&lt;br /&gt;
This ensures a balanced distribution of reviews across submissions.&lt;br /&gt;
&lt;br /&gt;
== Topic‑Balanced Dynamic Assignment ==&lt;br /&gt;
'''Controller action:''' request_review_topic_balance  &lt;br /&gt;
'''Strategy:''' LeastReviewedTopicStrategy&lt;br /&gt;
&lt;br /&gt;
When an assignment uses topics, reviewers may choose which topics they are willing to review. The system then assigns a team within an eligible topic, subject to fairness constraints.&lt;br /&gt;
&lt;br /&gt;
'''Algorithm:'''&lt;br /&gt;
&lt;br /&gt;
Review counts are aggregated per topic.&lt;br /&gt;
Let min_count be the smallest number of reviews received by any topic.&lt;br /&gt;
A topic is eligible if its review count is within '''k''' of min_count.&lt;br /&gt;
The first eligible topic (deterministically) is selected.&lt;br /&gt;
Within that topic, the reviewer is assigned to a team that:&lt;br /&gt;
*is not their own team,&lt;br /&gt;
*has not already been reviewed by them,&lt;br /&gt;
*has the fewest reviews among eligible teams.&lt;br /&gt;
&lt;br /&gt;
This ensures:&lt;br /&gt;
*Reviewers can restrict themselves to topics they prefer.&lt;br /&gt;
*Topics remain approximately balanced, with fairness controlled by the threshold k.&lt;br /&gt;
&lt;br /&gt;
= Calibration Review Assignment =&lt;br /&gt;
&lt;br /&gt;
== Calibration Round‑Robin ==&lt;br /&gt;
'''Controller action:''' assign_calibration_artifacts&lt;br /&gt;
&lt;br /&gt;
This assigns calibration reviews to all reviewers using a round‑robin strategy. Calibration reviews are typically instructor‑provided artifacts used to train or benchmark reviewers.&lt;br /&gt;
&lt;br /&gt;
= Deleting Review Mappings =&lt;br /&gt;
&lt;br /&gt;
== Delete a Single Mapping ==&lt;br /&gt;
'''Controller action:''' destroy&lt;br /&gt;
&lt;br /&gt;
Deletes a specific review mapping by ID.&lt;br /&gt;
&lt;br /&gt;
== Delete All Reviews for a Reviewer ==&lt;br /&gt;
'''Controller action:''' delete_all_for_reviewer&lt;br /&gt;
&lt;br /&gt;
Removes all review mappings associated with a given reviewer. Useful when resetting or reassigning work.&lt;br /&gt;
&lt;br /&gt;
= Instructor Grading of Reviews =&lt;br /&gt;
&lt;br /&gt;
'''Controller action:''' grade_review&lt;br /&gt;
&lt;br /&gt;
Instructors may grade individual reviews. This action:&lt;br /&gt;
&lt;br /&gt;
Locates the review mapping&lt;br /&gt;
&lt;br /&gt;
Records the instructor’s grade and comment&lt;br /&gt;
&lt;br /&gt;
Returns a success message&lt;br /&gt;
&lt;br /&gt;
= Strategy Classes =&lt;br /&gt;
&lt;br /&gt;
All strategies inherit from ReviewMappingStrategies::BaseStrategy, which defines two abstract methods:&lt;br /&gt;
&lt;br /&gt;
each_review_pair — for static strategies&lt;br /&gt;
&lt;br /&gt;
assign_one — for dynamic strategies&lt;br /&gt;
&lt;br /&gt;
Each concrete strategy implements one of these depending on its purpose.&lt;br /&gt;
&lt;br /&gt;
== CsvImportStrategy ==&lt;br /&gt;
Implements each_review_pair by reading reviewer–team pairs from a CSV file.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedSubmissionStrategy ==&lt;br /&gt;
Implements assign_one by selecting the eligible team with the fewest reviews.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedTopicStrategy ==&lt;br /&gt;
Implements assign_one with topic‑level fairness constraints and a threshold k.&lt;br /&gt;
&lt;br /&gt;
== RoundRobinStrategy ==&lt;br /&gt;
Implements each_review_pair by cycling through reviewers and pairing them with teams.&lt;br /&gt;
&lt;br /&gt;
= Common Eligibility Rules =&lt;br /&gt;
&lt;br /&gt;
Across all dynamic strategies, a reviewer is never assigned to:&lt;br /&gt;
&lt;br /&gt;
Their own team&lt;br /&gt;
&lt;br /&gt;
A team they have already reviewed&lt;br /&gt;
&lt;br /&gt;
A team outside the selected topic (for topic‑based assignment)&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167522</id>
		<title>Reviewer Assignment implementation</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167522"/>
		<updated>2026-03-15T20:01:43Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Topic‑Balanced Dynamic Assignment */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Review Assignment System Overview&lt;br /&gt;
The review‑assignment subsystem supports both '''static''' and '''dynamic''' allocation of reviews within an assignment. The goal is to give instructors flexible control over how reviewers are matched to submissions, while also supporting on‑demand, fairness‑aware assignment for students who request reviews dynamically.&lt;br /&gt;
&lt;br /&gt;
This page documents the behavior implemented in ReviewMappingsController and the strategy classes under ReviewMappingStrategies.&lt;br /&gt;
&lt;br /&gt;
= Static Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Static assignment is initiated by the instructor and produces a complete set of review mappings in a single operation. Three static strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Round‑Robin Assignment ==&lt;br /&gt;
'''Controller action:''' assign_round_robin&lt;br /&gt;
&lt;br /&gt;
This strategy cycles through all reviewers in order and assigns each team to the next reviewer in the cycle. Only participants who are eligible to review (can_review?) are included.&lt;br /&gt;
&lt;br /&gt;
'''Characteristics:'''&lt;br /&gt;
&lt;br /&gt;
*Deterministic ordering&lt;br /&gt;
*Each team receives exactly one reviewer&lt;br /&gt;
*Reviewers are reused as needed&lt;br /&gt;
*No workload balancing beyond the simple cycle&lt;br /&gt;
&lt;br /&gt;
== Random Assignment ==&lt;br /&gt;
'''Controller action:''' assign_random&lt;br /&gt;
&lt;br /&gt;
This strategy assigns reviewers to teams randomly. The distribution is determined by the logic inside ReviewMappingHandler.&lt;br /&gt;
&lt;br /&gt;
== CSV‑Based Assignment ==&lt;br /&gt;
'''Controller action:''' assign_from_csv&lt;br /&gt;
&lt;br /&gt;
This strategy imports reviewer–team pairs from an uploaded CSV file.&lt;br /&gt;
&lt;br /&gt;
'''Expected CSV format:'''&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
reviewer_email,team_name&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
For each row:&lt;br /&gt;
&lt;br /&gt;
*The reviewer is located by email and matched to an AssignmentParticipant.&lt;br /&gt;
*The team is located by name within the assignment.&lt;br /&gt;
*A mapping is created if both are found.&lt;br /&gt;
&lt;br /&gt;
This allows instructors to pre‑compute assignments externally and import them directly.&lt;br /&gt;
&lt;br /&gt;
= Dynamic Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Dynamic assignment is initiated by a reviewer requesting a new review. Instead of pre‑assigning all reviews, the system allocates reviews on demand, which helps avoid the common problem of students failing to complete their assigned reviews.&lt;br /&gt;
&lt;br /&gt;
Two dynamic strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Least‑Reviewed Submission Strategy ==&lt;br /&gt;
'''Controller action:''' request_review_fewest  &lt;br /&gt;
'''Strategy:''' LeastReviewedSubmissionStrategy&lt;br /&gt;
&lt;br /&gt;
This strategy assigns the reviewer to the team whose submission has received the fewest completed reviews so far.&lt;br /&gt;
&lt;br /&gt;
'''Eligibility rules:'''&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to their own team.&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to a team they have already reviewed.&lt;br /&gt;
&lt;br /&gt;
Among eligible teams, the one with the lowest review count is selected.&lt;br /&gt;
&lt;br /&gt;
This ensures a balanced distribution of reviews across submissions.&lt;br /&gt;
&lt;br /&gt;
== Topic‑Balanced Dynamic Assignment ==&lt;br /&gt;
'''Controller action:''' request_review_topic_balance  &lt;br /&gt;
'''Strategy:''' LeastReviewedTopicStrategy&lt;br /&gt;
&lt;br /&gt;
When an assignment uses topics, reviewers may choose which topics they are willing to review. The system then assigns a team within an eligible topic, subject to fairness constraints.&lt;br /&gt;
&lt;br /&gt;
'''Algorithm:'''&lt;br /&gt;
&lt;br /&gt;
Review counts are aggregated per topic.&lt;br /&gt;
Let min_count be the smallest number of reviews received by any topic.&lt;br /&gt;
A topic is eligible if its review count is within '''k''' of min_count.&lt;br /&gt;
The first eligible topic (deterministically) is selected.&lt;br /&gt;
Within that topic, the reviewer is assigned to a team that:&lt;br /&gt;
*is not their own team,&lt;br /&gt;
*has not already been reviewed by them,&lt;br /&gt;
*has the fewest reviews among eligible teams.&lt;br /&gt;
&lt;br /&gt;
This ensures:&lt;br /&gt;
&lt;br /&gt;
Reviewers can restrict themselves to topics they prefer.&lt;br /&gt;
&lt;br /&gt;
Topics remain approximately balanced, with fairness controlled by the threshold k.&lt;br /&gt;
&lt;br /&gt;
= Calibration Review Assignment =&lt;br /&gt;
&lt;br /&gt;
== Calibration Round‑Robin ==&lt;br /&gt;
'''Controller action:''' assign_calibration_artifacts&lt;br /&gt;
&lt;br /&gt;
This assigns calibration reviews to all reviewers using a round‑robin strategy. Calibration reviews are typically instructor‑provided artifacts used to train or benchmark reviewers.&lt;br /&gt;
&lt;br /&gt;
= Deleting Review Mappings =&lt;br /&gt;
&lt;br /&gt;
== Delete a Single Mapping ==&lt;br /&gt;
'''Controller action:''' destroy&lt;br /&gt;
&lt;br /&gt;
Deletes a specific review mapping by ID.&lt;br /&gt;
&lt;br /&gt;
== Delete All Reviews for a Reviewer ==&lt;br /&gt;
'''Controller action:''' delete_all_for_reviewer&lt;br /&gt;
&lt;br /&gt;
Removes all review mappings associated with a given reviewer. Useful when resetting or reassigning work.&lt;br /&gt;
&lt;br /&gt;
= Instructor Grading of Reviews =&lt;br /&gt;
&lt;br /&gt;
'''Controller action:''' grade_review&lt;br /&gt;
&lt;br /&gt;
Instructors may grade individual reviews. This action:&lt;br /&gt;
&lt;br /&gt;
Locates the review mapping&lt;br /&gt;
&lt;br /&gt;
Records the instructor’s grade and comment&lt;br /&gt;
&lt;br /&gt;
Returns a success message&lt;br /&gt;
&lt;br /&gt;
= Strategy Classes =&lt;br /&gt;
&lt;br /&gt;
All strategies inherit from ReviewMappingStrategies::BaseStrategy, which defines two abstract methods:&lt;br /&gt;
&lt;br /&gt;
each_review_pair — for static strategies&lt;br /&gt;
&lt;br /&gt;
assign_one — for dynamic strategies&lt;br /&gt;
&lt;br /&gt;
Each concrete strategy implements one of these depending on its purpose.&lt;br /&gt;
&lt;br /&gt;
== CsvImportStrategy ==&lt;br /&gt;
Implements each_review_pair by reading reviewer–team pairs from a CSV file.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedSubmissionStrategy ==&lt;br /&gt;
Implements assign_one by selecting the eligible team with the fewest reviews.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedTopicStrategy ==&lt;br /&gt;
Implements assign_one with topic‑level fairness constraints and a threshold k.&lt;br /&gt;
&lt;br /&gt;
== RoundRobinStrategy ==&lt;br /&gt;
Implements each_review_pair by cycling through reviewers and pairing them with teams.&lt;br /&gt;
&lt;br /&gt;
= Common Eligibility Rules =&lt;br /&gt;
&lt;br /&gt;
Across all dynamic strategies, a reviewer is never assigned to:&lt;br /&gt;
&lt;br /&gt;
Their own team&lt;br /&gt;
&lt;br /&gt;
A team they have already reviewed&lt;br /&gt;
&lt;br /&gt;
A team outside the selected topic (for topic‑based assignment)&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167521</id>
		<title>Reviewer Assignment implementation</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167521"/>
		<updated>2026-03-15T20:01:16Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* CSV‑Based Assignment */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Review Assignment System Overview&lt;br /&gt;
The review‑assignment subsystem supports both '''static''' and '''dynamic''' allocation of reviews within an assignment. The goal is to give instructors flexible control over how reviewers are matched to submissions, while also supporting on‑demand, fairness‑aware assignment for students who request reviews dynamically.&lt;br /&gt;
&lt;br /&gt;
This page documents the behavior implemented in ReviewMappingsController and the strategy classes under ReviewMappingStrategies.&lt;br /&gt;
&lt;br /&gt;
= Static Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Static assignment is initiated by the instructor and produces a complete set of review mappings in a single operation. Three static strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Round‑Robin Assignment ==&lt;br /&gt;
'''Controller action:''' assign_round_robin&lt;br /&gt;
&lt;br /&gt;
This strategy cycles through all reviewers in order and assigns each team to the next reviewer in the cycle. Only participants who are eligible to review (can_review?) are included.&lt;br /&gt;
&lt;br /&gt;
'''Characteristics:'''&lt;br /&gt;
&lt;br /&gt;
*Deterministic ordering&lt;br /&gt;
*Each team receives exactly one reviewer&lt;br /&gt;
*Reviewers are reused as needed&lt;br /&gt;
*No workload balancing beyond the simple cycle&lt;br /&gt;
&lt;br /&gt;
== Random Assignment ==&lt;br /&gt;
'''Controller action:''' assign_random&lt;br /&gt;
&lt;br /&gt;
This strategy assigns reviewers to teams randomly. The distribution is determined by the logic inside ReviewMappingHandler.&lt;br /&gt;
&lt;br /&gt;
== CSV‑Based Assignment ==&lt;br /&gt;
'''Controller action:''' assign_from_csv&lt;br /&gt;
&lt;br /&gt;
This strategy imports reviewer–team pairs from an uploaded CSV file.&lt;br /&gt;
&lt;br /&gt;
'''Expected CSV format:'''&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
reviewer_email,team_name&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
For each row:&lt;br /&gt;
&lt;br /&gt;
*The reviewer is located by email and matched to an AssignmentParticipant.&lt;br /&gt;
*The team is located by name within the assignment.&lt;br /&gt;
*A mapping is created if both are found.&lt;br /&gt;
&lt;br /&gt;
This allows instructors to pre‑compute assignments externally and import them directly.&lt;br /&gt;
&lt;br /&gt;
= Dynamic Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Dynamic assignment is initiated by a reviewer requesting a new review. Instead of pre‑assigning all reviews, the system allocates reviews on demand, which helps avoid the common problem of students failing to complete their assigned reviews.&lt;br /&gt;
&lt;br /&gt;
Two dynamic strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Least‑Reviewed Submission Strategy ==&lt;br /&gt;
'''Controller action:''' request_review_fewest  &lt;br /&gt;
'''Strategy:''' LeastReviewedSubmissionStrategy&lt;br /&gt;
&lt;br /&gt;
This strategy assigns the reviewer to the team whose submission has received the fewest completed reviews so far.&lt;br /&gt;
&lt;br /&gt;
'''Eligibility rules:'''&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to their own team.&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to a team they have already reviewed.&lt;br /&gt;
&lt;br /&gt;
Among eligible teams, the one with the lowest review count is selected.&lt;br /&gt;
&lt;br /&gt;
This ensures a balanced distribution of reviews across submissions.&lt;br /&gt;
&lt;br /&gt;
== Topic‑Balanced Dynamic Assignment ==&lt;br /&gt;
'''Controller action:''' request_review_topic_balance  &lt;br /&gt;
'''Strategy:''' LeastReviewedTopicStrategy&lt;br /&gt;
&lt;br /&gt;
When an assignment uses topics, reviewers may choose which topics they are willing to review. The system then assigns a team within an eligible topic, subject to fairness constraints.&lt;br /&gt;
&lt;br /&gt;
'''Algorithm:'''&lt;br /&gt;
&lt;br /&gt;
Review counts are aggregated per topic.&lt;br /&gt;
Let min_count be the smallest number of reviews received by any topic.&lt;br /&gt;
A topic is eligible if its review count is within '''k''' of min_count.&lt;br /&gt;
The first eligible topic (deterministically) is selected.&lt;br /&gt;
Within that topic, the reviewer is assigned to a team that:&lt;br /&gt;
** is not their own team,&lt;br /&gt;
** has not already been reviewed by them,&lt;br /&gt;
** has the fewest reviews among eligible teams.&lt;br /&gt;
&lt;br /&gt;
This ensures:&lt;br /&gt;
&lt;br /&gt;
Reviewers can restrict themselves to topics they prefer.&lt;br /&gt;
&lt;br /&gt;
Topics remain approximately balanced, with fairness controlled by the threshold k.&lt;br /&gt;
&lt;br /&gt;
= Calibration Review Assignment =&lt;br /&gt;
&lt;br /&gt;
== Calibration Round‑Robin ==&lt;br /&gt;
'''Controller action:''' assign_calibration_artifacts&lt;br /&gt;
&lt;br /&gt;
This assigns calibration reviews to all reviewers using a round‑robin strategy. Calibration reviews are typically instructor‑provided artifacts used to train or benchmark reviewers.&lt;br /&gt;
&lt;br /&gt;
= Deleting Review Mappings =&lt;br /&gt;
&lt;br /&gt;
== Delete a Single Mapping ==&lt;br /&gt;
'''Controller action:''' destroy&lt;br /&gt;
&lt;br /&gt;
Deletes a specific review mapping by ID.&lt;br /&gt;
&lt;br /&gt;
== Delete All Reviews for a Reviewer ==&lt;br /&gt;
'''Controller action:''' delete_all_for_reviewer&lt;br /&gt;
&lt;br /&gt;
Removes all review mappings associated with a given reviewer. Useful when resetting or reassigning work.&lt;br /&gt;
&lt;br /&gt;
= Instructor Grading of Reviews =&lt;br /&gt;
&lt;br /&gt;
'''Controller action:''' grade_review&lt;br /&gt;
&lt;br /&gt;
Instructors may grade individual reviews. This action:&lt;br /&gt;
&lt;br /&gt;
Locates the review mapping&lt;br /&gt;
&lt;br /&gt;
Records the instructor’s grade and comment&lt;br /&gt;
&lt;br /&gt;
Returns a success message&lt;br /&gt;
&lt;br /&gt;
= Strategy Classes =&lt;br /&gt;
&lt;br /&gt;
All strategies inherit from ReviewMappingStrategies::BaseStrategy, which defines two abstract methods:&lt;br /&gt;
&lt;br /&gt;
each_review_pair — for static strategies&lt;br /&gt;
&lt;br /&gt;
assign_one — for dynamic strategies&lt;br /&gt;
&lt;br /&gt;
Each concrete strategy implements one of these depending on its purpose.&lt;br /&gt;
&lt;br /&gt;
== CsvImportStrategy ==&lt;br /&gt;
Implements each_review_pair by reading reviewer–team pairs from a CSV file.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedSubmissionStrategy ==&lt;br /&gt;
Implements assign_one by selecting the eligible team with the fewest reviews.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedTopicStrategy ==&lt;br /&gt;
Implements assign_one with topic‑level fairness constraints and a threshold k.&lt;br /&gt;
&lt;br /&gt;
== RoundRobinStrategy ==&lt;br /&gt;
Implements each_review_pair by cycling through reviewers and pairing them with teams.&lt;br /&gt;
&lt;br /&gt;
= Common Eligibility Rules =&lt;br /&gt;
&lt;br /&gt;
Across all dynamic strategies, a reviewer is never assigned to:&lt;br /&gt;
&lt;br /&gt;
Their own team&lt;br /&gt;
&lt;br /&gt;
A team they have already reviewed&lt;br /&gt;
&lt;br /&gt;
A team outside the selected topic (for topic‑based assignment)&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167520</id>
		<title>Reviewer Assignment implementation</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167520"/>
		<updated>2026-03-15T20:00:31Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Round‑Robin Assignment */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Review Assignment System Overview&lt;br /&gt;
The review‑assignment subsystem supports both '''static''' and '''dynamic''' allocation of reviews within an assignment. The goal is to give instructors flexible control over how reviewers are matched to submissions, while also supporting on‑demand, fairness‑aware assignment for students who request reviews dynamically.&lt;br /&gt;
&lt;br /&gt;
This page documents the behavior implemented in ReviewMappingsController and the strategy classes under ReviewMappingStrategies.&lt;br /&gt;
&lt;br /&gt;
= Static Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Static assignment is initiated by the instructor and produces a complete set of review mappings in a single operation. Three static strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Round‑Robin Assignment ==&lt;br /&gt;
'''Controller action:''' assign_round_robin&lt;br /&gt;
&lt;br /&gt;
This strategy cycles through all reviewers in order and assigns each team to the next reviewer in the cycle. Only participants who are eligible to review (can_review?) are included.&lt;br /&gt;
&lt;br /&gt;
'''Characteristics:'''&lt;br /&gt;
&lt;br /&gt;
*Deterministic ordering&lt;br /&gt;
*Each team receives exactly one reviewer&lt;br /&gt;
*Reviewers are reused as needed&lt;br /&gt;
*No workload balancing beyond the simple cycle&lt;br /&gt;
&lt;br /&gt;
== Random Assignment ==&lt;br /&gt;
'''Controller action:''' assign_random&lt;br /&gt;
&lt;br /&gt;
This strategy assigns reviewers to teams randomly. The distribution is determined by the logic inside ReviewMappingHandler.&lt;br /&gt;
&lt;br /&gt;
== CSV‑Based Assignment ==&lt;br /&gt;
'''Controller action:''' assign_from_csv&lt;br /&gt;
&lt;br /&gt;
This strategy imports reviewer–team pairs from an uploaded CSV file.&lt;br /&gt;
&lt;br /&gt;
'''Expected CSV format:'''&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
reviewer_email,team_name&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
For each row:&lt;br /&gt;
&lt;br /&gt;
The reviewer is located by email and matched to an AssignmentParticipant.&lt;br /&gt;
&lt;br /&gt;
The team is located by name within the assignment.&lt;br /&gt;
&lt;br /&gt;
A mapping is created if both are found.&lt;br /&gt;
&lt;br /&gt;
This allows instructors to pre‑compute assignments externally and import them directly.&lt;br /&gt;
&lt;br /&gt;
= Dynamic Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Dynamic assignment is initiated by a reviewer requesting a new review. Instead of pre‑assigning all reviews, the system allocates reviews on demand, which helps avoid the common problem of students failing to complete their assigned reviews.&lt;br /&gt;
&lt;br /&gt;
Two dynamic strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Least‑Reviewed Submission Strategy ==&lt;br /&gt;
'''Controller action:''' request_review_fewest  &lt;br /&gt;
'''Strategy:''' LeastReviewedSubmissionStrategy&lt;br /&gt;
&lt;br /&gt;
This strategy assigns the reviewer to the team whose submission has received the fewest completed reviews so far.&lt;br /&gt;
&lt;br /&gt;
'''Eligibility rules:'''&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to their own team.&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to a team they have already reviewed.&lt;br /&gt;
&lt;br /&gt;
Among eligible teams, the one with the lowest review count is selected.&lt;br /&gt;
&lt;br /&gt;
This ensures a balanced distribution of reviews across submissions.&lt;br /&gt;
&lt;br /&gt;
== Topic‑Balanced Dynamic Assignment ==&lt;br /&gt;
'''Controller action:''' request_review_topic_balance  &lt;br /&gt;
'''Strategy:''' LeastReviewedTopicStrategy&lt;br /&gt;
&lt;br /&gt;
When an assignment uses topics, reviewers may choose which topics they are willing to review. The system then assigns a team within an eligible topic, subject to fairness constraints.&lt;br /&gt;
&lt;br /&gt;
'''Algorithm:'''&lt;br /&gt;
&lt;br /&gt;
Review counts are aggregated per topic.&lt;br /&gt;
Let min_count be the smallest number of reviews received by any topic.&lt;br /&gt;
A topic is eligible if its review count is within '''k''' of min_count.&lt;br /&gt;
The first eligible topic (deterministically) is selected.&lt;br /&gt;
Within that topic, the reviewer is assigned to a team that:&lt;br /&gt;
** is not their own team,&lt;br /&gt;
** has not already been reviewed by them,&lt;br /&gt;
** has the fewest reviews among eligible teams.&lt;br /&gt;
&lt;br /&gt;
This ensures:&lt;br /&gt;
&lt;br /&gt;
Reviewers can restrict themselves to topics they prefer.&lt;br /&gt;
&lt;br /&gt;
Topics remain approximately balanced, with fairness controlled by the threshold k.&lt;br /&gt;
&lt;br /&gt;
= Calibration Review Assignment =&lt;br /&gt;
&lt;br /&gt;
== Calibration Round‑Robin ==&lt;br /&gt;
'''Controller action:''' assign_calibration_artifacts&lt;br /&gt;
&lt;br /&gt;
This assigns calibration reviews to all reviewers using a round‑robin strategy. Calibration reviews are typically instructor‑provided artifacts used to train or benchmark reviewers.&lt;br /&gt;
&lt;br /&gt;
= Deleting Review Mappings =&lt;br /&gt;
&lt;br /&gt;
== Delete a Single Mapping ==&lt;br /&gt;
'''Controller action:''' destroy&lt;br /&gt;
&lt;br /&gt;
Deletes a specific review mapping by ID.&lt;br /&gt;
&lt;br /&gt;
== Delete All Reviews for a Reviewer ==&lt;br /&gt;
'''Controller action:''' delete_all_for_reviewer&lt;br /&gt;
&lt;br /&gt;
Removes all review mappings associated with a given reviewer. Useful when resetting or reassigning work.&lt;br /&gt;
&lt;br /&gt;
= Instructor Grading of Reviews =&lt;br /&gt;
&lt;br /&gt;
'''Controller action:''' grade_review&lt;br /&gt;
&lt;br /&gt;
Instructors may grade individual reviews. This action:&lt;br /&gt;
&lt;br /&gt;
Locates the review mapping&lt;br /&gt;
&lt;br /&gt;
Records the instructor’s grade and comment&lt;br /&gt;
&lt;br /&gt;
Returns a success message&lt;br /&gt;
&lt;br /&gt;
= Strategy Classes =&lt;br /&gt;
&lt;br /&gt;
All strategies inherit from ReviewMappingStrategies::BaseStrategy, which defines two abstract methods:&lt;br /&gt;
&lt;br /&gt;
each_review_pair — for static strategies&lt;br /&gt;
&lt;br /&gt;
assign_one — for dynamic strategies&lt;br /&gt;
&lt;br /&gt;
Each concrete strategy implements one of these depending on its purpose.&lt;br /&gt;
&lt;br /&gt;
== CsvImportStrategy ==&lt;br /&gt;
Implements each_review_pair by reading reviewer–team pairs from a CSV file.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedSubmissionStrategy ==&lt;br /&gt;
Implements assign_one by selecting the eligible team with the fewest reviews.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedTopicStrategy ==&lt;br /&gt;
Implements assign_one with topic‑level fairness constraints and a threshold k.&lt;br /&gt;
&lt;br /&gt;
== RoundRobinStrategy ==&lt;br /&gt;
Implements each_review_pair by cycling through reviewers and pairing them with teams.&lt;br /&gt;
&lt;br /&gt;
= Common Eligibility Rules =&lt;br /&gt;
&lt;br /&gt;
Across all dynamic strategies, a reviewer is never assigned to:&lt;br /&gt;
&lt;br /&gt;
Their own team&lt;br /&gt;
&lt;br /&gt;
A team they have already reviewed&lt;br /&gt;
&lt;br /&gt;
A team outside the selected topic (for topic‑based assignment)&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167519</id>
		<title>Reviewer Assignment implementation</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Reviewer_Assignment_implementation&amp;diff=167519"/>
		<updated>2026-03-15T19:59:10Z</updated>

		<summary type="html">&lt;p&gt;Admin: Created page with &amp;quot;Review Assignment System Overview The review‑assignment subsystem supports both '''static''' and '''dynamic''' allocation of reviews within an assignment. The goal is to give instructors flexible control over how reviewers are matched to submissions, while also supporting on‑demand, fairness‑aware assignment for students who request reviews dynamically.  This page documents the behavior implemented in ReviewMappingsController and the strategy classes under ReviewMa...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Review Assignment System Overview&lt;br /&gt;
The review‑assignment subsystem supports both '''static''' and '''dynamic''' allocation of reviews within an assignment. The goal is to give instructors flexible control over how reviewers are matched to submissions, while also supporting on‑demand, fairness‑aware assignment for students who request reviews dynamically.&lt;br /&gt;
&lt;br /&gt;
This page documents the behavior implemented in ReviewMappingsController and the strategy classes under ReviewMappingStrategies.&lt;br /&gt;
&lt;br /&gt;
= Static Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Static assignment is initiated by the instructor and produces a complete set of review mappings in a single operation. Three static strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Round‑Robin Assignment ==&lt;br /&gt;
'''Controller action:''' assign_round_robin&lt;br /&gt;
&lt;br /&gt;
This strategy cycles through all reviewers in order and assigns each team to the next reviewer in the cycle. Only participants who are eligible to review (can_review?) are included.&lt;br /&gt;
&lt;br /&gt;
'''Characteristics:'''&lt;br /&gt;
&lt;br /&gt;
Deterministic ordering&lt;br /&gt;
&lt;br /&gt;
Each team receives exactly one reviewer&lt;br /&gt;
&lt;br /&gt;
Reviewers are reused as needed&lt;br /&gt;
&lt;br /&gt;
No workload balancing beyond the simple cycle&lt;br /&gt;
&lt;br /&gt;
== Random Assignment ==&lt;br /&gt;
'''Controller action:''' assign_random&lt;br /&gt;
&lt;br /&gt;
This strategy assigns reviewers to teams randomly. The distribution is determined by the logic inside ReviewMappingHandler.&lt;br /&gt;
&lt;br /&gt;
== CSV‑Based Assignment ==&lt;br /&gt;
'''Controller action:''' assign_from_csv&lt;br /&gt;
&lt;br /&gt;
This strategy imports reviewer–team pairs from an uploaded CSV file.&lt;br /&gt;
&lt;br /&gt;
'''Expected CSV format:'''&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
reviewer_email,team_name&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
For each row:&lt;br /&gt;
&lt;br /&gt;
The reviewer is located by email and matched to an AssignmentParticipant.&lt;br /&gt;
&lt;br /&gt;
The team is located by name within the assignment.&lt;br /&gt;
&lt;br /&gt;
A mapping is created if both are found.&lt;br /&gt;
&lt;br /&gt;
This allows instructors to pre‑compute assignments externally and import them directly.&lt;br /&gt;
&lt;br /&gt;
= Dynamic Review Assignment =&lt;br /&gt;
&lt;br /&gt;
Dynamic assignment is initiated by a reviewer requesting a new review. Instead of pre‑assigning all reviews, the system allocates reviews on demand, which helps avoid the common problem of students failing to complete their assigned reviews.&lt;br /&gt;
&lt;br /&gt;
Two dynamic strategies are supported.&lt;br /&gt;
&lt;br /&gt;
== Least‑Reviewed Submission Strategy ==&lt;br /&gt;
'''Controller action:''' request_review_fewest  &lt;br /&gt;
'''Strategy:''' LeastReviewedSubmissionStrategy&lt;br /&gt;
&lt;br /&gt;
This strategy assigns the reviewer to the team whose submission has received the fewest completed reviews so far.&lt;br /&gt;
&lt;br /&gt;
'''Eligibility rules:'''&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to their own team.&lt;br /&gt;
&lt;br /&gt;
A reviewer may not be assigned to a team they have already reviewed.&lt;br /&gt;
&lt;br /&gt;
Among eligible teams, the one with the lowest review count is selected.&lt;br /&gt;
&lt;br /&gt;
This ensures a balanced distribution of reviews across submissions.&lt;br /&gt;
&lt;br /&gt;
== Topic‑Balanced Dynamic Assignment ==&lt;br /&gt;
'''Controller action:''' request_review_topic_balance  &lt;br /&gt;
'''Strategy:''' LeastReviewedTopicStrategy&lt;br /&gt;
&lt;br /&gt;
When an assignment uses topics, reviewers may choose which topics they are willing to review. The system then assigns a team within an eligible topic, subject to fairness constraints.&lt;br /&gt;
&lt;br /&gt;
'''Algorithm:'''&lt;br /&gt;
&lt;br /&gt;
Review counts are aggregated per topic.&lt;br /&gt;
Let min_count be the smallest number of reviews received by any topic.&lt;br /&gt;
A topic is eligible if its review count is within '''k''' of min_count.&lt;br /&gt;
The first eligible topic (deterministically) is selected.&lt;br /&gt;
Within that topic, the reviewer is assigned to a team that:&lt;br /&gt;
** is not their own team,&lt;br /&gt;
** has not already been reviewed by them,&lt;br /&gt;
** has the fewest reviews among eligible teams.&lt;br /&gt;
&lt;br /&gt;
This ensures:&lt;br /&gt;
&lt;br /&gt;
Reviewers can restrict themselves to topics they prefer.&lt;br /&gt;
&lt;br /&gt;
Topics remain approximately balanced, with fairness controlled by the threshold k.&lt;br /&gt;
&lt;br /&gt;
= Calibration Review Assignment =&lt;br /&gt;
&lt;br /&gt;
== Calibration Round‑Robin ==&lt;br /&gt;
'''Controller action:''' assign_calibration_artifacts&lt;br /&gt;
&lt;br /&gt;
This assigns calibration reviews to all reviewers using a round‑robin strategy. Calibration reviews are typically instructor‑provided artifacts used to train or benchmark reviewers.&lt;br /&gt;
&lt;br /&gt;
= Deleting Review Mappings =&lt;br /&gt;
&lt;br /&gt;
== Delete a Single Mapping ==&lt;br /&gt;
'''Controller action:''' destroy&lt;br /&gt;
&lt;br /&gt;
Deletes a specific review mapping by ID.&lt;br /&gt;
&lt;br /&gt;
== Delete All Reviews for a Reviewer ==&lt;br /&gt;
'''Controller action:''' delete_all_for_reviewer&lt;br /&gt;
&lt;br /&gt;
Removes all review mappings associated with a given reviewer. Useful when resetting or reassigning work.&lt;br /&gt;
&lt;br /&gt;
= Instructor Grading of Reviews =&lt;br /&gt;
&lt;br /&gt;
'''Controller action:''' grade_review&lt;br /&gt;
&lt;br /&gt;
Instructors may grade individual reviews. This action:&lt;br /&gt;
&lt;br /&gt;
Locates the review mapping&lt;br /&gt;
&lt;br /&gt;
Records the instructor’s grade and comment&lt;br /&gt;
&lt;br /&gt;
Returns a success message&lt;br /&gt;
&lt;br /&gt;
= Strategy Classes =&lt;br /&gt;
&lt;br /&gt;
All strategies inherit from ReviewMappingStrategies::BaseStrategy, which defines two abstract methods:&lt;br /&gt;
&lt;br /&gt;
each_review_pair — for static strategies&lt;br /&gt;
&lt;br /&gt;
assign_one — for dynamic strategies&lt;br /&gt;
&lt;br /&gt;
Each concrete strategy implements one of these depending on its purpose.&lt;br /&gt;
&lt;br /&gt;
== CsvImportStrategy ==&lt;br /&gt;
Implements each_review_pair by reading reviewer–team pairs from a CSV file.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedSubmissionStrategy ==&lt;br /&gt;
Implements assign_one by selecting the eligible team with the fewest reviews.&lt;br /&gt;
&lt;br /&gt;
== LeastReviewedTopicStrategy ==&lt;br /&gt;
Implements assign_one with topic‑level fairness constraints and a threshold k.&lt;br /&gt;
&lt;br /&gt;
== RoundRobinStrategy ==&lt;br /&gt;
Implements each_review_pair by cycling through reviewers and pairing them with teams.&lt;br /&gt;
&lt;br /&gt;
= Common Eligibility Rules =&lt;br /&gt;
&lt;br /&gt;
Across all dynamic strategies, a reviewer is never assigned to:&lt;br /&gt;
&lt;br /&gt;
Their own team&lt;br /&gt;
&lt;br /&gt;
A team they have already reviewed&lt;br /&gt;
&lt;br /&gt;
A team outside the selected topic (for topic‑based assignment)&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Users&amp;diff=167503</id>
		<title>Users</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Users&amp;diff=167503"/>
		<updated>2026-03-08T19:28:51Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Users Variable Documentation (Reimplementation) */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Users Variable Documentation (Reimplementation) ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; &lt;br /&gt;
!Name !!Datatype !!Description&lt;br /&gt;
|- &lt;br /&gt;
!id  &lt;br /&gt;
|int(11)  &lt;br /&gt;
|Primary key of the entry in table, auto increments by default&lt;br /&gt;
|- &lt;br /&gt;
!username&lt;br /&gt;
|varchar(255)  &lt;br /&gt;
|User name of the respective user (formerly name)&lt;br /&gt;
|- &lt;br /&gt;
!crypted_password  &lt;br /&gt;
|varchar(40)  &lt;br /&gt;
|Password of the respective user&lt;br /&gt;
|- &lt;br /&gt;
!role_id  &lt;br /&gt;
|int(11)  &lt;br /&gt;
|Role which this user corresponds to FK into ROLES table.  Examples are super_admin, admin, instructor, etc.&lt;br /&gt;
|- &lt;br /&gt;
!password_salt  &lt;br /&gt;
|varchar(255)  &lt;br /&gt;
|&lt;br /&gt;
|- &lt;br /&gt;
!name&lt;br /&gt;
|varchar(255)  &lt;br /&gt;
|Registered full name of the user, e.g., Lastname, firstname (formerly fullname)&lt;br /&gt;
|- &lt;br /&gt;
!email  &lt;br /&gt;
|varchar(255)  &lt;br /&gt;
|Registered email of the respective user.&lt;br /&gt;
|- &lt;br /&gt;
!parent_id  &lt;br /&gt;
|int(11)  &lt;br /&gt;
|Parent of the respective user; corresponding to the user who created this user.  The value of this field is the primary key in the users table for the parent user, e.g., 2 if the parent is the super-admin (username admin).&lt;br /&gt;
|- &lt;br /&gt;
!private_by_default  &lt;br /&gt;
|tinyint(1)  &lt;br /&gt;
|If the user is private by default; essentially Boolean. It seems that this is not used in the system, don't know what it was intended to mean.  Could be removed.&lt;br /&gt;
|- &lt;br /&gt;
!email_on_submission  &lt;br /&gt;
|tiny_int(1)  &lt;br /&gt;
|If user is to be e-mailed when something they reviewed is resubmitted.&lt;br /&gt;
|- &lt;br /&gt;
!is_new_user  &lt;br /&gt;
|tiny_int(1)  &lt;br /&gt;
|Determines whether the user has logged in before; if not, upon login the user should be presented with the new-user agreement.&lt;br /&gt;
|- &lt;br /&gt;
!handle  &lt;br /&gt;
|varchar(255) &lt;br /&gt;
|A handle is an alternate name by which the user would want to be known, e.g., on the wiki, if this assignment involves writing on a wiki.  This allows the user to appear anonymous.  In the code, this is sometimes known as the &amp;quot;default handle&amp;quot;, because users can also create assignment-specific handles.&lt;br /&gt;
|- &lt;br /&gt;
!time_zone_pref&lt;br /&gt;
|varchar(255) &lt;br /&gt;
|Timezone that times should be displayed in when the user logs in.&lt;br /&gt;
|- &lt;br /&gt;
!language_pref&lt;br /&gt;
|varchar(255) &lt;br /&gt;
|Language that the user wants to use in interacting with Expertiza.&lt;br /&gt;
|- &lt;br /&gt;
!public_key&lt;br /&gt;
|mediumtext&lt;br /&gt;
|Public key used for data-encryption by user.  This is used and should stay in the table, though whether the use is meaningful is not clear.&lt;br /&gt;
|- &lt;br /&gt;
!copy_of_emails&lt;br /&gt;
|tinyint(1)&lt;br /&gt;
|Whether this user wants to receive copies of e-mails sent for all assignments they are participating in.  This is only implemented for instructors.  Instructors would enable it so they start getting copies of all emails being sent related to their assignments.  It is a way that the instructor can be reassured that emails are in fact being sent.&lt;br /&gt;
|- &lt;br /&gt;
!institution_id&lt;br /&gt;
|int(11)&lt;br /&gt;
|id of the institution that the user is associated with&lt;br /&gt;
|} &lt;br /&gt;
&lt;br /&gt;
== Users Variable Documentation (2023 version) ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; &lt;br /&gt;
!Name !!Datatype !!Description&lt;br /&gt;
|- &lt;br /&gt;
!id  &lt;br /&gt;
|int(11)  &lt;br /&gt;
|Primary key of the entry in table, auto increments by default&lt;br /&gt;
|- &lt;br /&gt;
!name  &lt;br /&gt;
|varchar(255)  &lt;br /&gt;
|User name of the respective user&lt;br /&gt;
|- &lt;br /&gt;
!crypted_password  &lt;br /&gt;
|varchar(40)  &lt;br /&gt;
|Password of the respective user&lt;br /&gt;
|- &lt;br /&gt;
!role_id  &lt;br /&gt;
|int(11)  &lt;br /&gt;
|Role which this user corresponds to FK into ROLES table.  Examples are super_admin, admin, instructor, etc.&lt;br /&gt;
|- &lt;br /&gt;
!password_salt  &lt;br /&gt;
|varchar(255)  &lt;br /&gt;
|&lt;br /&gt;
|- &lt;br /&gt;
!fullname  &lt;br /&gt;
|varchar(255)  &lt;br /&gt;
|Registered full name of the user, e.g., Lastname, firstname&lt;br /&gt;
|- &lt;br /&gt;
!email  &lt;br /&gt;
|varchar(255)  &lt;br /&gt;
|Registered email of the respective user.&lt;br /&gt;
|- &lt;br /&gt;
!parent_id  &lt;br /&gt;
|int(11)  &lt;br /&gt;
|Parent of the respective user; corresponding to the user who created this user.  The value of this field is the primary key in the users table for the parent user, e.g., 2 if the parent is the super-admin (username admin).&lt;br /&gt;
|- &lt;br /&gt;
!private_by_default  &lt;br /&gt;
|tinyint(1)  &lt;br /&gt;
|If the user is private by default; essentially Boolean. It seems that this is not used in the system, don't know what it was intended to mean.  Could be removed.&lt;br /&gt;
|- &lt;br /&gt;
!mru_directory_path  &lt;br /&gt;
|varchar(128)  &lt;br /&gt;
|To allow the user to log back in to the same directory that (s)he logged out from before.&lt;br /&gt;
|- &lt;br /&gt;
!email_on_review  &lt;br /&gt;
|tiny_int(1)  &lt;br /&gt;
|Whether the user should be e-mailed when a new review is received for their work.&lt;br /&gt;
|- &lt;br /&gt;
!email_on_submission  &lt;br /&gt;
|tiny_int(1)  &lt;br /&gt;
|If user is to be e-mailed when something they reviewed is resubmitted.&lt;br /&gt;
|- &lt;br /&gt;
!email_on_review_of_review  &lt;br /&gt;
|tiny_int(1)  &lt;br /&gt;
|Whether the user should be e-mailed when one of their reviews is meta-reviewed by another reviewer.&lt;br /&gt;
|- &lt;br /&gt;
!is_new_user  &lt;br /&gt;
|tiny_int(1)  &lt;br /&gt;
|Determines whether the user has logged in before; if not, upon login the user should be presented with the new-user agreement.&lt;br /&gt;
|- &lt;br /&gt;
!master_permssion_granted  &lt;br /&gt;
|tiny_int(4)  &lt;br /&gt;
|If the user has the master user permission is granted.  What?! What does master permission mean?!  Well, doesn't matter, because this field is set but not read in existing code.&lt;br /&gt;
|- &lt;br /&gt;
!handle  &lt;br /&gt;
|varchar(255) &lt;br /&gt;
|A handle is an alternate name by which the user would want to be known, e.g., on the wiki, if this assignment involves writing on a wiki.  This allows the user to appear anonymous.  In the code, this is sometimes known as the &amp;quot;default handle&amp;quot;, because users can also create assignment-specific handles.&lt;br /&gt;
|- &lt;br /&gt;
!digital_certificate  &lt;br /&gt;
|mediumtext &lt;br /&gt;
|Digital certificate generated for the user to protect his password.  This is never read and could be removed.&lt;br /&gt;
|- &lt;br /&gt;
!timezonepref&lt;br /&gt;
|varchar(255) &lt;br /&gt;
|Timezone that times should be displayed in when the user logs in.&lt;br /&gt;
|- &lt;br /&gt;
!public_key&lt;br /&gt;
|mediumtext&lt;br /&gt;
|Public key used for data-encryption by user.  This is used and should stay in the table, though whether the use is meaningful is not clear.&lt;br /&gt;
|- &lt;br /&gt;
!copy_of_emails&lt;br /&gt;
|tinyint(1)&lt;br /&gt;
|Whether this user wants to receive copies of e-mails sent for all assignments they are participating in.  This is only implemented for instructors.  Instructors would enable it so they start getting copies of all emails being sent related to their assignments.  It is a way that the instructor can be reassured that emails are in fact being sent.&lt;br /&gt;
|- &lt;br /&gt;
!institution_id&lt;br /&gt;
|int(11)&lt;br /&gt;
|id of the institution that the user is associated with&lt;br /&gt;
|} &lt;br /&gt;
&lt;br /&gt;
== E/R diagram of tables referencing users table  ==&lt;br /&gt;
The following image shows the tables referencing users table&lt;br /&gt;
[[File:Users_export.png]]&lt;br /&gt;
&lt;br /&gt;
== E/R diagram of tables users table is referencing to ==&lt;br /&gt;
The users table is not referenced by any other tables&lt;br /&gt;
&lt;br /&gt;
Back to [http://wiki.expertiza.ncsu.edu/index.php/Documentation_on_Database_Tables Database Tables] Main page.&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Questionnaires&amp;diff=167502</id>
		<title>Questionnaires</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Questionnaires&amp;diff=167502"/>
		<updated>2026-03-06T01:06:33Z</updated>

		<summary type="html">&lt;p&gt;Admin: /* Questionnaires documentation (Reimplementation) */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;==Questionnaires documentation (Reimplementation)==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; &lt;br /&gt;
!Field Name !!Type !!Description&lt;br /&gt;
|-&lt;br /&gt;
!id &lt;br /&gt;
|int(11)  &lt;br /&gt;
|This object's unique ID value. One per object. &lt;br /&gt;
|- &lt;br /&gt;
!name   &lt;br /&gt;
|varchar(64)  &lt;br /&gt;
|name of the questionnaire&lt;br /&gt;
|- &lt;br /&gt;
!instructor_id   &lt;br /&gt;
|int(11)  &lt;br /&gt;
|id of instructor who created the questionnaire&lt;br /&gt;
|- &lt;br /&gt;
!private   &lt;br /&gt;
|tinyint(1)  &lt;br /&gt;
|whether questionnaire is visible to other instructors&lt;br /&gt;
|- &lt;br /&gt;
!min_item_score   &lt;br /&gt;
|int(11)  &lt;br /&gt;
|the minimum score that the reviewer can give for a question in this questionnaire&lt;br /&gt;
|- &lt;br /&gt;
!max_item_score   &lt;br /&gt;
|int(11)  &lt;br /&gt;
|the maximum score that the reviewer can give for a question in this questionnaire&lt;br /&gt;
|- &lt;br /&gt;
!created_at   &lt;br /&gt;
|datetime  &lt;br /&gt;
|the date and time at which the questionnaire was created at&lt;br /&gt;
|- &lt;br /&gt;
!updated_at   &lt;br /&gt;
|datetime  &lt;br /&gt;
|the date and time at which the questionnaire was last updated&lt;br /&gt;
|- &lt;br /&gt;
!default_num_choices   &lt;br /&gt;
|int(11)  &lt;br /&gt;
|default number of scoring increments&lt;br /&gt;
|- &lt;br /&gt;
!type  &lt;br /&gt;
|varchar(255) &lt;br /&gt;
|Subclassing for the questionnaire. Possible types are ReviewQuestionnaire, MetareviewQuestionnaire, AuthorFeedbackQuestionnaire, BookmarkRatingQuestionnaire, QuizQuestionniare, SurveyQuestionnaire, CourseEvaluationQuestionnaire, TeammateReviewQuestionnaire, GlobalSurveyQuestionnaire&lt;br /&gt;
|-&lt;br /&gt;
!instruction_loc&lt;br /&gt;
|TEXT&lt;br /&gt;
|&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==Questionnaires documentation (2023 version)==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; &lt;br /&gt;
!Field Name !!Type !!Description&lt;br /&gt;
|-&lt;br /&gt;
!id &lt;br /&gt;
|int(11)  &lt;br /&gt;
|This object's unique ID value. One per object. &lt;br /&gt;
|- &lt;br /&gt;
!name   &lt;br /&gt;
|varchar(64)  &lt;br /&gt;
|name of the questionnaire&lt;br /&gt;
|- &lt;br /&gt;
!instructor_id   &lt;br /&gt;
|int(11)  &lt;br /&gt;
|id of instructor who created the questionnaire&lt;br /&gt;
|- &lt;br /&gt;
!private   &lt;br /&gt;
|tinyint(1)  &lt;br /&gt;
|whether questionnaire is visible to other instructors&lt;br /&gt;
|- &lt;br /&gt;
!min_question_score   &lt;br /&gt;
|int(11)  &lt;br /&gt;
|the minimum score that the reviewer can give for a question in this questionnaire&lt;br /&gt;
|- &lt;br /&gt;
!max_question_score   &lt;br /&gt;
|int(11)  &lt;br /&gt;
|the maximum score that the reviewer can give for a question in this questionnaire&lt;br /&gt;
|- &lt;br /&gt;
!created_at   &lt;br /&gt;
|datetime  &lt;br /&gt;
|the date and time at which the questionnaire was created at&lt;br /&gt;
|- &lt;br /&gt;
!updated_at   &lt;br /&gt;
|datetime  &lt;br /&gt;
|the date and time at which the questionnaire was last updated&lt;br /&gt;
|- &lt;br /&gt;
!default_num_choices   &lt;br /&gt;
|int(11)  &lt;br /&gt;
|default number of scoring increments&lt;br /&gt;
|- &lt;br /&gt;
!type  &lt;br /&gt;
|varchar(255) &lt;br /&gt;
|Subclassing for the questionnaire. Possible types are ReviewQuestionnaire, MetareviewQuestionnaire, AuthorFeedbackQuestionnaire, QuizQuestionniare, SurveyQuestionnaire, CourseEvaluationQuestionnaire, TeammateReviewQuestionnaire, GlobalSurveyQuestionnaire&lt;br /&gt;
|-&lt;br /&gt;
!display_type  &lt;br /&gt;
|varchar(255) &lt;br /&gt;
|Character representation of type used in tree display. Possible values are Review, Metareview, AuthorFeedback, Survey, CourseEvaluation, TeammateReview GlobalSurvey&lt;br /&gt;
|-&lt;br /&gt;
!instruction_loc&lt;br /&gt;
|TEXT&lt;br /&gt;
|&lt;br /&gt;
|} &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Back to [http://wikis.lib.ncsu.edu/index.php/Documentation_on_Database_Tables Database Tables] Main page.&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
</feed>