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 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.
Tech Stack
- Backend: Ruby on Rails (API-only)
- Frontend: React + TypeScript
- Database: MySQL
- Backend Testing: RSpec
- Frontend Testing: Jest, React Testing Library
Key Improvements
- Eliminated N+1 queries in the score heatgrid (
get_team_scores,get_my_scores_data,accumulate_round_scores,get_answer) via.includes(responses: { scores: :item })and in-memory filtering instead of re-querying preloaded associations - Converted
ReviewTable's stylesheet to CSS Modules, fixing a Bootstrap class collision and scoping color classes (c1–c5) correctly - Redesigned the score/feedback heatgrid with sticky
#/Question columns and a sticky, full-width SectionHeader row that stays fixed during horizontal scroll - Added hover tooltips on score cells showing the reviewer's written feedback
- Rewrote
StudentTasksfrom a (mis-implemented) topic sign-up sheet into a proper course-grouped assignment dashboard - Added participant-level
can_submit/can_reviewpermission checks, hiding "Your work" / "Others' work" task links accordingly - Refactored review grade calculation:
ResponseMap#review_gradenow weights each round by itsquestionnaire_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 numbering 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
Acceptance Criteria:
GET /student_tasks/listreturns all tasks for the logged-in student in one call- Tasks are grouped by course on the dashboard
- Each task shows current stage and days remaining
As a student, I want to view a single assignment's deadlines and details
Acceptance Criteria:
GET /student_tasks/show/:idreturns the task plus a unifieddue_datestimeline- The timeline visually distinguishes completed, current, and pending stages
GET /student_tasks/teamreturns teammates grouped by course
As a student, I want my team's score heatgrid to load fast and stay readable while scrolling
Acceptance Criteria:
- The heatgrid loads from a single
GET /grades/:id/view_our_scorescall - SectionHeaders are excluded from the
#column numbering - SectionHeader rows and the
#/Question columns stay fixed during horizontal scroll - Hovering a score with a comment shows the reviewer's feedback as a tooltip
As a student who isn't permitted to submit or review, I should 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 review grades to reflect each rubric's configured weight
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
No dedicated StudentTask database table exists. StudentTask is a plain Ruby object composed from AssignmentParticipant and its associated Assignment, Course, DueDate, SignedUpTeam/ProjectTopic, and permission fields (can_submit, can_review, permission_granted).
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- Selecting an assignment navigates to the detail route → frontend calls
GET /student_tasks/show/:id StudentTask.get_events_for_assignmentmerges due dates with submitted review/feedback activity into one sorted timeline- For the score heatgrid: frontend calls
GET /grades/:id/view_our_scores→GradesController#get_team_scoreseager-loadsresponses: { scores: :item }and callsinsert_section_headersto splice header sentinels into the response
Score Aggregation Flow
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
StudentTasksController
list— allStudentTaskobjects for the logged-in usershow— single task by participant ID with the unified timelineteam— teammates grouped by course name (StudentTask.all_teammates)
GradesController — N+1 elimination
get_team_scores and get_my_scores_data eager-load responses: { scores: :item } on the ReviewResponseMap/TeammateReviewResponseMap queries. accumulate_round_scores filters SectionHeader scores 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
Splices { type: "header", txt: "..." } sentinel hashes into a round's scores array at positions matching each SectionHeader item in the questionnaire's sequence, so the frontend can render heading rows between score rows without those rows affecting numbering.
Review Grade Calculation
ResponseMap#review_grade— latest submitted response per round, normalized (score/max), weighted by that round'squestionnaire_weightResponseMap.compute_average_reviewer_score/ResponseMap.reviewer_reputation_for— shared weighted-average logic; reviewer reputation currently defaults to1.0(placeholder for a future review-quality grader integration)Questionnaire#total_item_weight— pre-calculates the sum of scored item weights, excludingSectionHeaderitems
Comprehensive Testing
- Model specs for
StudentTask.all_teammates,create_from_participant,get_events_for_assignment - Request specs for
student_tasks/list,student_tasks/show/:id,student_tasks/team, andgrades/: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) |
StudentTasksList.tsx |
Sidebar — not-started tasks, revisions, teammates by course |
StudentTaskDetail.tsx |
Per-assignment detail page — timeline, task links, conditional can_submit/can_review rendering
|
ViewTeamGrades.module.scss |
CSS Modules conversion; scoped c1–c5 color classes, sticky-column and tooltip styles
|
ReviewTable.tsx |
Single-call heatgrid; Scores/Feedback toggle; sticky SectionHeader rows |
ReviewTableRow.tsx |
Sticky #/Question columns; reduced row height; hover tooltip via data-question
|
FeedbackTable.tsx |
Feedback-mode table mirroring ReviewTable's sticky-column layout
|
heatgridUtils.ts |
RoundRow/isHeader() types; fixed # numbering to skip SectionHeader sentinels
|
StudentTasks (List View)
- Single
GET /student_tasks/listcall, grouped by course - Fixed: "Assignments" heading was rendering as body text because its class was referenced as a plain string instead of the CSS Modules export (
styles['assignments-title'])
StudentTaskDetail (Detail View)
- Visual timeline with progress line and stage nodes (completed/current/pending)
- Task links list — "Your work" and "Others' work" conditionally rendered based on
can_submit/can_review - "Send Email To Reviewers" button repositioned to align with the task list rather than floating disconnected at the page top
ViewTeamGrades (Heatgrid)
- Consolidated ~6 sequential API calls into one
GET /grades/:id/view_our_scores - SectionHeader rows split into a sticky label cell + scrolling spacer cell so they stay fixed during horizontal scroll
- Score cells show reviewer feedback as a hover tooltip
- 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_review_grade and ReviewAggregator#compute_average_review_score was consolidated into a single ResponseMap.compute_average_reviewer_score, and the now-dead ReviewAggregator concern was deleted.
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 | StudentTask.all_teammates groups teammates by course |
Returns {course_name => [fullnames]}
|
| 2 | all_teammates excludes calibrated assignments |
Calibrated-assignment teammates excluded |
| 3 | all_teammates excludes the user themself |
Solo team returns empty hash |
| 4 | GET /student_tasks/list returns all tasks for user |
200 OK with task array |
| 5 | GET /student_tasks/show/:id returns due_dates timeline |
200 OK with due_dates array
|
| 6 | GET /student_tasks/team groups teammates by course |
200 OK with grouped hash |
| 7 | Heatgrid # column excludes SectionHeaders from numbering |
Sequential numbering skips header rows |
| 8 | accumulate_round_scores filters SectionHeaders without re-querying |
No additional SQL fired against preloaded association |
| 9 | AssignmentQuestionnaire weight validation |
questionnaire_weight must be 0 when rubric has no scored questions
|
| 10 | ResponseMap.compute_average_reviewer_score |
Weighted average matches Σ(grade × weight) / Σ(weight) |
Frontend Tests
| # | Test | Expected Result |
|---|---|---|
| 1 | StudentTasks renders "Assignments" heading with correct styling |
Heading uses module class, renders bold and large |
| 2 | StudentTaskDetail hides "Your work" when can_submit is false |
List item not rendered |
| 3 | StudentTaskDetail hides "Others' work" when can_review is false |
List item not rendered |
| 4 | heatgridUtils itemNumber skips SectionHeader rows |
Numbering matches scored items only |
| 5 | ReviewTable SectionHeader row stays fixed during horizontal scroll |
Header cell position: sticky
|
| 6 | ReviewTableRow shows tooltip only when a comment exists |
No tooltip box when data-question is undefined
|
| 7 | Heatgrid column borders render as #ddd |
No black borders after tbl_heat class applied
|
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/requests/api/v1/student_tasks_controller_spec.rb bundle exec rspec spec/requests/api/v1/grades_controller_spec.rb
Frontend
Setup & Run
npm install npm start
Running Frontend Tests
npm test -- StudentTaskDetail # run a single suite npm test # run full suite
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 for every reviewer.
Email-the-authors integration
Wire "Send Email To Reviewers" to a real backend endpoint instead of the current placeholder route.
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.