CSC/ECE 517 Spring 2026 - E2602. Reimplement student task view
CSC/ECE 517 Spring 2026 - E2602. Reimplement Student Task View
About Expertiza
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.
Introduction
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. This iteration of E2602 hardens and extends the Student Task View: consolidating the student dashboard and detail pages into 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.
Tech Stack
- Backend: Ruby on Rails (API-only)
- Frontend: React + TypeScript
- Database: MySQL
- Backend Testing: RSpec
- Frontend Testing: Jest, React Testing Library
Key Improvements over the Previous Implementation and the Existing Expertiza System
The following improvements were made both over the earlier student_task_controller in the existing Expertiza system and over the initial E2602 skeleton handed off to this team:
- Rewrote
StudentTasks.tsxfrom a mis-implemented topic sign-up sheet into a proper course-grouped assignment dashboard, loaded from a singleGET /student_tasks/listcall - Built a new
StudentTaskDetailpage with a visual progress timeline — fetches task summary and timeline events in parallel viaGET /student_tasks/show/:idandGET /participants/:id/timeline; timeline nodes are colour-coded by stage status (completed/current/pending) - Added
StudentTasksListsidebar showing tasks not yet started, assignments in revision, and a course-grouped list of past teammates - Added
AssignmentParticipant#timeline_eventsto build a chronologically sorted array of due dates and submitted review/feedback activity for a participant - Added
AssignmentParticipant.all_teammatesto return teammates grouped by course, excluding calibration assignments and the requesting user; fixed a pre-existing bug whereassignment.is_calibratedalways returnednildue to anattr_accessorshadowing ActiveRecord's column reader - Added
GET /participants/teammatesandGET /participants/:id/timelineendpoints toParticipantsController - Added participant-level
can_submit/can_reviewpermission checks, hiding "Your work" and "Others' work" task links when the student lacks those permissions - Eliminated N+1 queries in the score heatgrid via
.includes(responses: { scores: :item })and in-memory filtering with.rejectinstead of re-querying preloaded associations - Converted
ViewTeamGrades.module.scssto CSS Modules, fixing a Bootstrap class collision and correctly scopingc1–c5color classes - Redesigned the score/feedback heatgrid with sticky Question column and section header rows during horizontal scroll
- Added hover tooltips on score cells showing the reviewer's written feedback
- Refactored review grade calculation:
ResponseMap#review_gradeweights each round by its configuredquestionnaire_weight;ResponseMap.compute_average_reviewer_scorecentralizes the weighted-average logic previously duplicated acrossAssignmentTeamandAssignmentParticipant
Problem Statement
The previous Student Task View had several concrete defects:
- The score heatgrid fired one SQL query per response/score across all reviewers and rounds, producing hundreds of queries for a single page load
- SectionHeader rubric rows (non-scorable dividers) were incorrectly counted in the
#column and lost their fixed position during horizontal scroll StudentTasks.tsxwas previously implemented as a topic sign-up sheet, not a task dashboard, and required a full rewrite- The page did not respect participant-level
can_submit/can_reviewflags — all task links were always shown regardless of permission - Review grade calculation treated every reviewer and every round equally, ignoring each rubric's configured
questionnaire_weight
User Stories
As a student, I want to see all my assignments in one place
This is the home view for a student. It shows all of the student's assignments, arranged by the course they pertain to.

Acceptance Criteria:
GET /student_tasks/listreturns all tasks for the authenticated student in a single call — no sequential per-assignment requests- Each task object in the response contains:
assignment(name),course,topic,current_stage,stage_deadline,permission_granted,active_round,submission_updated,started, and a nestedparticipantobject - Results are pre-sorted by
[course, assignment, stage_deadline]so the frontend can render them in a stable order without additional sorting - The frontend groups the sorted list by course name to produce the course-grouped dashboard view
- Each task displays its current stage and next deadline; tasks with no upcoming due date show "Finished"
- Stage deadline is adjusted to the student's configured timezone preference where available
GET /participants/teammatesreturns a hash of{ course_name => [teammate_full_names] }for all assignment teams the student has been part of- Each course's teammate list is sorted alphabetically
- The current user is excluded from their own teammate lists
- Calibration assignments are excluded — they are practice exercises, not real team collaborations
- Returns an empty hash if the student has no past teammates
As a student, I want to view a single assignment's deadlines and details
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.

Acceptance Criteria:
GET /student_tasks/show/:id(where:idis the participant ID) returns the task summary:assignmentname,current_stage,stage_deadline,permission_granted, and a nestedparticipantobject containingcan_submit,can_review, andparent_idGET /participants/:id/timelinereturns a chronologically sorted array of timeline events — each withid,name,date,type, andround. Events include due dates (withid: null) and submitted peer reviews / author feedback responses (with real IDs that link to/responses/:id)- Both requests are made in parallel on page load; the task summary renders immediately while the timeline loads independently
- The timeline visually distinguishes completed, current, and pending stages
- "Your work" and "Others' work" links are conditionally shown based on
can_submitandcan_reviewrespectively - "Your feedback" links to
/view-team-grades?assignmentId=Xusingparent_idfrom the participant object in the show response - Both endpoints return 403 Forbidden if the authenticated user does not own the requested participant; 404 if the participant does not exist
As a student, I want my team's score heatgrid to load fast and stay readable while scrolling
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.

Clicking on "Feedback" will show all of the feedback along with the scores.

Acceptance Criteria:
- The heatgrid loads from a single
GET /grades/:id/view_our_scorescall - The first two columns (item number "
#" and Item) remain fixed during horizontal scroll, along with section headers - Hovering over a score with a comment shows the corresponding reviewer's feedback as a tooltip
A student who is not permitted to submit or review will not see those task links
Acceptance Criteria:
- "Your work" is hidden when
can_submitisfalse - "Others' work" is hidden when
can_reviewisfalse
As an instructor, I want calculated review grades to reflect each rubric's configured weight
Grades are calculated in Response.rb (score for a particular review by a specific reviewer), ResponseMap.rb (composite score for reviews in all rounds by the same reviewer) and AssignmentTeam.rb (combined score for all reviews by all reviewers).
Acceptance Criteria:
ResponseMap#review_gradeweights each round's normalized score by that round'squestionnaire_weightAssignmentTeam#aggregate_reviewer_scoreandAssignmentParticipant#aggregate_teammate_review_gradeboth compute their averages through the same sharedResponseMap.compute_average_reviewer_score
Design
Student Task Overview
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 StudentTask database table exists. A StudentTask is a plain Ruby object composed from AssignmentParticipant and its associated Assignment, Course, DueDate, SignedUpTeam/ProjectTopic, and permission fields (permission_granted). The per-participant submission and review permissions (can_submit, can_review) are DB columns on the participants table and are surfaced to the frontend through the nested participant object in the show response.
Request / Response Flow
- Student navigates to
/student_tasks→ frontend callsGET /student_tasks/list StudentTask.tasks(user)preloadsassignment: :courseand:userassociations, then maps eachAssignmentParticipantthroughStudentTask.create_from_participant, and sorts by[course, assignment, stage_deadline]- Selecting an assignment navigates to the detail route → frontend fires two requests in parallel:
GET /student_tasks/show/:id— returns task summary (assignment,current_stage,stage_deadline,permission_granted, nestedparticipantwithcan_submit,can_review,parent_id)GET /participants/:id/timeline— callsAssignmentParticipant#timeline_events, which merges the assignment's due dates with timestamps of the participant's submitted peer reviews and author feedback into one chronologically sorted array
- For the heatgrid: frontend calls
GET /grades/:id/view_our_scores→GradesController#get_team_scoreseager-loadsresponses: { scores: :item }and callsinsert_section_headersto splice unscored item markers into the response before it is returned to the frontend
Aggregation of Scores Assigned by a Reviewer
Response#aggregate_questionnaire_score— item-weighted score for one submitted responseResponseMap#review_grade— normalizes a map's score per round and weights it by that round'squestionnaire_weightResponseMap.compute_average_reviewer_score(maps)— weighted average across multiple maps, the single implementation shared byAssignmentTeam#aggregate_reviewer_scoreandAssignmentParticipant#aggregate_teammate_review_grade
Backend Implementation
The backend is structured around three models (AssignmentParticipant, StudentTask, and Assignment) and two controllers. The controllers expose REST endpoints consumed by the React frontend; the models encapsulate business logic including timeline generation, teammate aggregation, and grade computation.
StudentTasksController
Handles two endpoints, both requiring student privileges:
list(GET /student_tasks/list) — callsStudentTask.tasks(current_user), which preloadsassignment: :courseand:userassociations on all of the user'sAssignmentParticipantrecords, maps each throughStudentTask.create_from_participant, and sorts the result by[course, assignment, stage_deadline]show(GET /student_tasks/show/:id) — delegates toStudentTask.from_participant_id(params[:id]), which preloads the same associations astasks; returns 404 if the participant does not exist, 403 if the participant belongs to a different user
ParticipantsController
Two new actions were added to the existing ParticipantsController:
teammates(GET /participants/teammates) — delegates toAssignmentParticipant.all_teammates(current_user)and returns a{ course_name => [sorted_full_names] }hash. Calibration assignments are excluded usingassignment[:is_calibrated]rather thanassignment.is_calibrated—Assignmenthas anattr_accessor :is_calibratedthat shadows ActiveRecord's column reader, returningnilafter a DB load; reading via[]bypasses the accessor and reads directly from AR's attribute hashtimeline(GET /participants/:id/timeline) — callsAssignmentParticipant#timeline_eventson the located participant; returns 404 if not found, 403 if the participant belongs to a different user
AssignmentParticipant model
Two class/instance methods were added:
all_teammates(user)— iterates the user'sAssignmentTeammemberships, skips calibration assignments, and accumulates teammates (excluding the user themselves) grouped by course name, with each course list sorted alphabeticallytimeline_events— builds a chronologically sorted array combining the assignment's due dates (withid: nil) and any submittedReviewResponseMap/FeedbackResponseMapresponses (with real IDs). Previously this logic lived inStudentTask; it was moved here because it operates on participant-level data rather than task summaries
GradesController
get_team_scores and get_my_scores_data eager-load responses: { scores: :item } on the ReviewResponseMap/TeammateReviewResponseMap queries. accumulate_round_scores filters unscored item rows via Ruby .reject instead of .joins/.where — calling .where on an already-preloaded association silently bypasses the cache and re-queries the database. get_answer now accepts the parent response directly instead of re-fetching it via score.response.
insert_section_headers
Inserts { type: "header", txt: "..." } marker objects into the scores array at positions derived from each unscored item's (text areas and text fields) sequence order, so the frontend renders section heading rows between score rows without those rows affecting item numbering.
Review Grade Calculation
For details on how peer review grades are computed, see Score Calculation.
Comprehensive Testing
- Model specs for
AssignmentParticipant.all_teammates(grouping, calibration exclusion, self-exclusion, alphabetical sort) andAssignmentParticipant#timeline_events(due dates, submitted reviews, author feedback, sort order) - Request specs for
GET /student_tasks/list,GET /student_tasks/show/:id,GET /participants/teammates,GET /participants/:id/timeline, andGET /grades/:id/view_our_scores - Routing specs verifying the above routes resolve correctly
Frontend Implementation
New/Updated Files
| File | Purpose |
|---|---|
StudentTasks.tsx |
Course-grouped assignment dashboard rewritten from a topic sign-up sheet; uses TanStack Table with per-course grouping, a "Review Grade" tooltip column, and a "Show as Example?" client-side toggle |
StudentTasks.module.css |
CSS Modules stylesheet scoping the dashboard layout and course-section headings |
StudentTasksList.tsx |
Sidebar ("StudentTasksBox") with three sections: tasks not yet started, revisions (links to /student_review/list/:participantId), and teammates by course fetched independently via GET /participants/teammates
|
StudentTasksList.module.css |
CSS Modules stylesheet for the sidebar layout and teammate count badges |
StudentTaskDetail.tsx |
Per-assignment detail page — fires GET /student_tasks/show/:id and GET /participants/:id/timeline in parallel; renders a visual progress timeline and conditionally shows task links based on can_submit/can_review
|
StudentTaskDetail.module.css |
CSS Modules stylesheet for the timeline track line, stage nodes, and deadline label rows |
ViewTeamGrades.module.scss |
CSS Modules conversion of the heatgrid stylesheet; scopes c1–c5 color classes, sticky-column positioning, and hover tooltip styles
|
ReviewTable.tsx |
Heatgrid loaded from a single GET /grades/:id/view_our_scores call; Scores/Feedback toggle; sticky SectionHeader rows
|
ReviewTableRow.tsx |
Sticky #/Question columns; reduced row height; hover tooltip on score cells that have reviewer comments
|
FeedbackTable.tsx |
Feedback-mode table mirroring ReviewTable's sticky-column layout
|
heatgridUtils.ts |
RoundRow/isHeader() types; normalizeReviewData() adapter translating the legacy questionNumber/questionText field names to itemNumber/itemText; fixed # numbering that skips SectionHeader sentinels
|
StudentTasks (List View)
- Single
GET /student_tasks/listcall;parseStudentTasks()normalises the raw response into a typedTask[]with??fallbacks to handle both flat and nested response shapes tasksGroupedByCoursegroups the sorted task list by course name, rendering a separate TanStack Table per course- "Assignment" column links to
/student_task_detail/:participantId, passing task summary via router state so the detail page can render the header before the API responds - "Review Grade" column renders a
ToolTipwhen a grade exists; shows "NA" otherwise - Sidebar (
StudentTasksList) receives aRevision[]derived from the same task list; fetches teammates independently viaGET /participants/teammates - Fixed: "Assignments" heading was rendering as unstyled body text because its class was referenced as a plain string instead of the CSS Modules export (
styles['assignments-title'])
StudentTaskDetail (Detail View)
- Fires two requests in parallel on mount:
GET /student_tasks/show/:idfor task summary andGET /participants/:id/timelinefor timeline events; task summary renders immediately while the timeline loads independently - Timeline events are sorted by date and rendered as three aligned rows: date labels, a progress track line with coloured stage nodes (completed=red filled, current=pulsing red, pending=grey outline), and deadline name labels
- Progress track line uses a CSS
linear-gradientdriven byprogressPercent, calculated as the midpoint of the current stage node so the red fill ends at the centre of the active dot - Date strings from the API are normalised: ISO 8601 strings are used as-is; legacy
dd-mm-yyyystrings are reformatted toyyyy-mm-ddTHH:MM:SSbefore parsing - "Your work" and "Others' work" links are conditionally rendered based on
can_submitandcan_reviewflags from the nestedparticipantobject in the show response - "Your feedback" links to
/view-team-grades?assignmentId=Xusingparent_idfrom the participant object, falling back toassignmentIdpassed via router state
ViewTeamGrades (Heatgrid)
ReviewTable.tsxconsolidates ~6 sequential API calls into oneGET /grades/:id/view_our_scores;team_membersis now embedded directly in the response so no secondary participant-lookup calls are needed- SectionHeader rows are split into a sticky label cell and a scrolling spacer cell so section headings stay fixed during horizontal scroll
- Score cells show reviewer feedback as a hover tooltip when a comment exists
- Round headings simplified from "Review (Round: 1 of 2)" to "Round 1"
Design Principles
Single Responsibility
Score aggregation is layered: Response owns item-level scoring, ResponseMap owns per-map/per-round weighting, ResponseMap.compute_average_reviewer_score owns cross-map averaging — each level only knows about the one below it.
Don't Repeat Yourself
The weighted-average logic that was duplicated between AssignmentTeam#aggregate_reviewer_score and AssignmentParticipant#aggregate_teammate_review_grade was consolidated into a single shared implementation: ResponseMap.compute_average_reviewer_score(maps). Both callers now delegate to that one method instead of each maintaining their own weighted-average calculation.
Performance by Design
Eager loading (.includes) and in-memory filtering (.reject) were chosen deliberately over additional database round-trips after profiling showed the heatgrid endpoint firing hundreds of queries under the old implementation.
Test Plan
Backend Tests
| # | Test | Expected Result |
|---|---|---|
| 1 | AssignmentParticipant.all_teammates groups teammates by course |
Returns { course_name => [full_names] }
|
| 2 | all_teammates excludes calibrated assignments |
Calibrated-assignment teammates not included in result |
| 3 | all_teammates excludes the user themselves |
Solo team returns empty hash |
| 4 | all_teammates returns teammates sorted alphabetically |
Names within each course are in alphabetical order |
| 5 | AssignmentParticipant#timeline_events includes due dates with id: nil |
Due-date entries have id: null in the response
|
| 6 | timeline_events includes submitted peer review responses with a real id |
Submitted review entries carry the response's DB id |
| 7 | timeline_events excludes unsubmitted/draft peer review responses |
Draft reviews do not appear in the timeline |
| 8 | timeline_events captures both round 1 and round 2 responses for the same map |
Multi-round reviews each appear as separate timeline entries |
| 9 | timeline_events excludes unsubmitted author feedback responses |
Draft feedback does not appear in the timeline |
| 10 | timeline_events returns entries sorted by date |
Timeline array is in ascending chronological order |
| 11 | GET /student_tasks/list returns all tasks for user |
200 OK with task array |
| 12 | GET /student_tasks/show/:id returns task summary |
200 OK with assignment, current_stage, stage_deadline, permission_granted, and nested participant fields
|
| 13 | GET /participants/:id/timeline returns sorted timeline events |
200 OK with array of { id, name, date, type, round }; due-date entries have id: null
|
| 14 | GET /participants/teammates groups teammates by course |
200 OK with { course_name => [sorted_full_names] }; calibration assignments and requesting user excluded
|
| 15 | Heatgrid # column excludes SectionHeaders from numbering |
Sequential numbering skips header rows |
| 16 | accumulate_round_scores filters SectionHeaders without re-querying |
No additional SQL fired against preloaded association |
| 17 | AssignmentQuestionnaire weight validation |
questionnaire_weight must be 0 when rubric has no scored questions
|
| 18 | ResponseMap.compute_average_reviewer_score |
Weighted average matches Σ(grade × weight) / Σ(weight) |
Relevant Links
Repos
- https://github.com/expertiza/reimplementation-back-end
- https://github.com/expertiza/reimplementation-front-end
Pull Requests
- Backend: https://github.com/expertiza/reimplementation-back-end/pull/352
- Frontend: https://github.com/expertiza/reimplementation-front-end/pull/182
Demo Video
- (add link once recorded)
Developer Guidance
Backend
Setup & Run (via Docker)
docker compose up
Within the container
bundle exec rspec spec/models/student_task_spec.rb bundle exec rspec spec/models/assignment_participant_spec.rb bundle exec rspec spec/requests/api/v1/student_tasks_controller_spec.rb bundle exec rspec spec/requests/api/v1/participants_controller_spec.rb bundle exec rspec spec/requests/api/v1/grades_controller_spec.rb
Frontend
Setup & Running
npm install npm start
Running Frontend Tests
npm test
Future Scope
Reviewer reputation-weighted grading
Integrate an external review-quality grader (e.g. an LLM-based reviewer grader) so ResponseMap.reviewer_reputation_for reflects actual review quality instead of the current 1.0 placeholder weight for every reviewer.
Email-the-authors integration
Wire "Send Email To Reviewers" to a real backend endpoint instead of the current /email_the_author placeholder.
Dedicated StudentTask abstraction
StudentTask remains a composed, non-persisted object. A future dedicated table could centralize derived state and simplify queries if the feature's scope grows further.