CSC/ECE 517 Spring 2026 - E2614. Role-based reviewing
About Expertiza
Expertiza is an open-source learning and assessment platform built on Ruby on Rails, designed around collaborative, project-based courses. Instructors use Expertiza to design assignments, define team structures, configure submission and review stages, and customize rubrics for different pedagogical goals. Students use the platform to form teams, submit artifacts, review peer work, and receive iterative feedback.
One of Expertiza's core strengths is that it treats peer review as a configurable workflow rather than a one-size-fits-all form. In real classroom settings, however, student contributions are often role-specific (for example, frontend, backend, testing, documentation). A generic teammate review model does not always capture this nuance. This project addresses that gap by improving how Expertiza models and configures role-based reviewing at assignment level.
Introduction
This project reimplements role-based reviewing in the new Expertiza system, covering both the instructor-facing configuration workflow and the student-facing role declaration capability. The goal is to make role-aware peer review a first-class, reliable feature rather than a partially implemented concept spread across disconnected parts of the codebase.
On the instructor side, the assignment editor is extended so that when role-based reviewing is enabled, an instructor can attach duty definitions to an assignment, set per-role member limits, create new duties inline, and see the Rubrics tab automatically reflect those choices through dynamically generated role-specific teammate review rows. On the student side, a team participant can declare their assigned duty within a team, with the backend enforcing that only duties the instructor has configured for that assignment are selectable.
To support both workflows coherently, the implementation addresses three core problems: role limits must be stored at assignment-duty granularity rather than globally on the duty, the API must provide a complete and predictable assignment-duty lifecycle, and role configuration must live in one place so that rubric rendering is always driven by persisted state rather than ephemeral client-side logic.
Requirements
The reimplementation was shaped by the need to make role-based reviewing functional across data, API, and UI layers, covering both instructors configuring assignments and students participating in teams.
Assignment-Scoped Role Configuration
A duty like "Frontend" carries different meaning across different assignments. In one assignment it may accommodate two team members; in another, only one. Storing role limits globally on the duty record makes it impossible to express this variation without workarounds. The requirement is therefore to model and persist role constraints at the assignment-duty level, not the duty level, so that each assignment's configuration is independent.
Complete Assignment-Duty API Lifecycle
The frontend needs to be able to read which duties are assigned to an assignment, attach a new duty, remove a duty, and update the member limit for an assigned duty — without going through ad hoc endpoints or re-using endpoints built for other purposes. A coherent, RESTful assignment-duty API surface was required so that frontend actions map directly to backend operations.
Assignment Payload Role Context
For the frontend to render role-aware rubric rows deterministically, the assignment serializer must include which duties are attached and what their limits are. Without this, the client would need separate API calls to reconstruct role context on every render — adding latency and creating cache-consistency risk.
Instructor Workflow in Assignment Editor
Instructors should not need to navigate away to a separate roles management page to configure role-based reviewing for an assignment. The assignment editor itself must support enabling role-based mode, selecting from existing duties, creating new duties inline, setting per-role member limits, and removing duties — all from the Review Strategy tab.
Role-Driven Rubric Behavior
When role-based mode is active and duties are assigned, the Rubrics tab should automatically surface per-role teammate review rows (one row per role, named Teammate Review for {RoleName}). If role-based mode is disabled or no duties are assigned, these rows must not appear. The rubric must be a consumer of role configuration, not a place to manage it.
Student Role Validation by Assignment Scope
When a team participant selects their role, the backend must verify that the chosen duty is actually assigned to that participant's assignment. This prevents a student from selecting a role that the instructor never configured, and it ensures that participant duty records remain consistent with the assignment's role setup.
Validation and Regression Coverage
Backend request specs must cover successful operations, invalid input, unauthorized access, and cross-scope validation failures. Frontend tests must confirm that role-based controls and rubric rows appear and disappear correctly as assignment state changes, without breaking existing non-role-based rubric behavior.
Previous Issues
Before this work, the role-based reviewing feature existed in fragments — data model fields, a duty management page, and some controller stubs — but the pieces did not form a working end-to-end workflow. The issues fell into four connected categories.
Data Model Mismatch
The duties table stored a max_members field directly on the duty record, treating role limits as global properties of the role itself. This is semantically wrong: how many "Frontend" developers an assignment needs is an assignment-level decision, not a fact about the Frontend role in general. As a consequence, the same duty could not carry different limits across different assignments without data duplication or workarounds. The assignments_duties join table existed but did not carry the limit field, so the actual per-assignment constraint had nowhere clean to live.
Missing Assignment-Duty Lifecycle API
The application had a DutiesController for managing duty records globally, but there was no scoped controller or endpoint set for managing which duties are attached to a specific assignment. The frontend had no clean path to add, remove, or update the limit for a duty in the context of one assignment. Any integration that existed relied on general-purpose endpoints being used in non-standard ways, making the contract fragile and hard to maintain.
Disconnected Role Configuration and Rubric Rendering
Role setup and rubric behavior were not linked in any defined way. Whether a rubric row appeared for a given role depended on front-end state that was not reliably populated from the assignment payload. Role-related UI controls were scattered, and there was no clear rule about which tab owned role configuration and which tab consumed it. This made it easy for the UI to get into states where role controls were visible but non-functional, or rubric rows appeared when they should not.
Missing Role Data in Assignment Serializer
The assignment serializer returned general assignment metadata but did not include which duties were assigned to the assignment or what their per-assignment limits were. This meant the frontend could not reconstruct role context from the standard assignment payload. Rendering role-aware rubric rows deterministically required either additional API calls or client-side state that could drift out of sync with the backend.
Design / Proposed Solution
Data Model Design
The central design decision is where role limits live. The existing duties table held a global max_members field, which implied a role limit is a property of the role itself. This is incorrect — it is a property of how that role is used within a particular assignment. Two assignments can reuse the same "Frontend" duty and set different limits independently.
The solution is to move the limit into the join table. assignments_duties already models the relationship between assignments and duties; adding max_members_for_duty to this table gives each assignment its own limit per duty without touching the duty record. The duty remains a reusable, shared definition; the assignment-duty mapping carries the assignment-specific constraint.
The assignment_participants table already had a duty_id foreign key column to record a student's declared role. The design retains this and enforces at the API level that any duty_id written to a participant record must correspond to a duty that is actually mapped to that participant's assignment via assignments_duties. This keeps the data consistent without adding extra columns or tables.
API Design
The table below lists every new or modified API endpoint introduced by this project, including request parameters and response shape.
Assignment-Duty Management (AssignmentsDutiesController)
These endpoints are nested under assignments and form the complete lifecycle for managing which roles exist for an assignment.
| Method | Endpoint | Request Body | Response | Notes |
|---|---|---|---|---|
| GET | /assignments/:assignment_id/duties
|
— | [{ duty_id, duty_name, max_members_for_duty }]
|
Returns all duty mappings currently attached to the assignment |
| POST | /assignments/:assignment_id/duties
|
{ duty_id: integer }
|
{ duty_id, duty_name, max_members_for_duty }
|
Attaches an existing duty to the assignment; idempotent — repeated calls do not create duplicates |
| DELETE | /assignments/:assignment_id/duties/:id
|
— | 204 No Content
|
Removes the duty mapping; :id is the duty_id; returns 404 if the duty is not mapped to this assignment
|
| PATCH | /assignments/:assignment_id/duties/:id/limit
|
{ max_members_for_duty: integer }
|
{ duty_id, duty_name, max_members_for_duty }
|
Updates the per-assignment role limit; returns 422 if limit is less than 1; returns 404 if duty not mapped
|
Authorization: all four endpoints require the authenticated user to be the instructor of the assignment. Requests from other roles return 403.
Participant Duty Update (TeamsParticipantsController)
This endpoint allows a student to declare their role within a team.
| Method | Endpoint | Request Body | Response | Notes |
|---|---|---|---|---|
| PATCH | /teams/:team_id/participants/:id/duty
|
{ duty_id: integer }
|
{ id, user_id, duty_id, ... }
|
Updates the participant's declared duty; validates that the selected duty is mapped to the participant's assignment; returns 422 on invalid duty
|
Accessible Duties (DutiesController)
Used by the assignment editor to populate the role picker with duties the current user has access to.
| Method | Endpoint | Request Body | Response | Notes |
|---|---|---|---|---|
| GET | /duties/accessible_duties
|
— | [{ id, name }]
|
Returns duties created by the current instructor or marked as public |
Assignment Serializer Contract
The assignment payload returned by AssignmentsController#show is extended with role context so the frontend does not need additional calls to render role-aware Rubrics:
{
"id": 1,
"name": "Project 1",
...
"has_role_based_review": true,
"assignment_duties": [
{ "duty_id": 3, "duty_name": "Frontend", "max_members_for_duty": 2 },
{ "duty_id": 5, "duty_name": "Backend", "max_members_for_duty": 1 }
]
}
The frontend reads assignment_duties to populate rubric rows and role controls. No separate fetch is needed after the assignment loads.
UI Design
Role configuration is owned entirely by the Review Strategy tab. Enabling the Is role based? toggle reveals the role management panel, where the instructor can select an existing duty from a dropdown, add it to the assignment, create a new duty inline if needed, set a per-role member limit, and remove duties. All of this is contained in one place without leaving the assignment editor.
The Rubrics tab is a consumer of role configuration, but it owns its own rubric variation settings. The review_rubric_varies_by_role checkbox lives in the Rubrics tab alongside the existing round- and topic-variation checkboxes, because it controls rubric structure rather than role assignment. Role rows (Teammate Review for {RoleName}) appear only when both Is role based? is enabled in Review Strategy and review_rubric_varies_by_role is enabled in Rubrics, and at least one duty is assigned. This two-gate design means an instructor explicitly opts into role-specific rubric rows and does not get them as an automatic side-effect of enabling role-based mode.
SOLID Principles Applied
Single Responsibility
Each class and controller has one clearly bounded job. AssignmentsDutiesController handles only the assignment-duty mapping lifecycle — it does not touch the global duty definition or the assignment itself beyond looking it up. TeamsParticipantsController handles participant operations and is extended only to validate and persist the duty selection. The AssignmentSerializer is responsible for shaping the assignment payload; role data is added to it without mixing in any controller logic.
Open/Closed
The system is extended without modifying stable, working code. The AssignmentsController and its core CRUD behavior are untouched — role context is added through the serializer. New endpoints are introduced in a new, nested controller rather than by adding role-specific actions to the existing DutiesController. The Rubrics component in the frontend is extended by reading new fields from the assignment payload, not by coupling it to role management state.
Liskov Substitution
The AssignmentsDuty model behaves as a standard ActiveRecord join model. It can be substituted anywhere an ActiveRecord object is expected without breaking callers. The validation and limit logic lives in the controller layer, keeping the model interface clean and predictable.
Interface Segregation
The assignment-duty API surface is intentionally narrow and separate from the global duties API. Role management for an assignment is entirely self-contained under /assignments/:id/duties. The global DutiesController handles duty creation and listing; the assignment-specific controller handles only the mapping. Frontend components are not forced to consume a large combined interface — the role picker fetches accessible duties, and the assignment editor fetches assignment duties independently.
Dependency Inversion
The Rubrics component does not depend on role management UI state or on specific duty records. It depends on the assignment data contract (specifically the assignment_duties array from the serializer). As long as that contract holds, Rubrics renders correctly regardless of how the underlying duties were created or which controller delivered them. This makes the rubric rendering logic testable in isolation and decoupled from the rest of the editor.
ER Diagram

Sequence Diagram

Implementation
Backend files changed
reimplementation-back-end/app/controllers/assignments_duties_controller.rb- New controller implementing the full assignment-duty lifecycle: index, create, destroy, and update_limit, with authorization checks and input validation on each action.reimplementation-back-end/app/controllers/teams_participants_controller.rb- Extended with anupdate_dutyaction that validates the selected duty belongs to the participant's assignment before persisting the change.reimplementation-back-end/app/serializers/assignment_serializer.rb- Extended to includehas_role_based_reviewand a nestedassignment_dutiesarray carrying each duty's id, name, and per-assignment member limit.reimplementation-back-end/config/routes.rb- Added nested resource routes fordutiesunderassignments, including the custompatch :limitmember route, and theupdate_dutyroute under team participants.reimplementation-back-end/db/schema.rb- Addedmax_members_for_dutycolumn toassignments_dutiesto store per-assignment role limits at the correct level of granularity.reimplementation-back-end/spec/requests/api/v1/assignments_duties_controller_spec.rb- Request specs covering successful operations, invalid input (422), unauthorized access (403), and non-mapped duty lookups (404).reimplementation-back-end/spec/requests/api/v1/teams_participants_controller_spec.rb- Request specs for the participant duty update path, including cross-assignment validation and auth cases.
Frontend files changed
reimplementation-front-end/src/pages/Assignments/AssignmentEditor.tsx- Added role-based controls across two tabs: (1) Review Strategy tab owns role assignment — enabling role mode (Is role based?), selecting duties from a dropdown, adding/removing duties (optimistically), inline duty creation via DutyEditor modal, and editing per-role member limits via an inline number input in the assigned roles table that firesPATCH .../limiton blur; (2) Rubrics tab ownsreview_rubric_varies_by_rolecheckbox alongside the existing round- and topic-variation checkboxes, and dynamically renders one teammate review row per assigned role when both that checkbox and role mode are active. Duplicate duty state declarations introduced by an upstream merge were removed; the entire role assignment flow was consolidated into a single consistent state path.reimplementation-front-end/src/pages/Assignments/AssignmentEditor.test.tsx- Added behavior tests validating conditional role controls and dynamic rubric row rendering for role-based assignments.reimplementation-front-end/src/pages/Student Teams/StudentTeamView.tsx- Added Role column and role selector for each team participant so students can declare or update their duty from assignment-scoped duties.reimplementation-front-end/src/hooks/useStudentTeam.ts- Added API integration method for duty updates and refreshes team state after role change.reimplementation-front-end/src/types(or related team interfaces) - Extended team member payload typing to include role/duty metadata required by the student UI.
Key implementation decisions and their rationale
1) Assignment-duty as the role-limit owner
Role limits are stored at assignment-duty granularity. This supports real course usage where one role can have different limits across assignments.
2) Enforced relation validation on participant duty updates
When a participant duty is updated, backend validation ensures the selected duty is assigned to that participant's assignment. This prevents invalid cross-assignment role references.
3) Inline role creation in assignment editor
Instead of forcing instructors to navigate away to a separate role page, the UI supports creating a role in place. This reduces friction and keeps the assignment configuration flow uninterrupted.
4) Conditional role UI
Role controls appear only when role-based mode is enabled. This keeps the UI minimal in non-role-based assignments and avoids accidental role operations.
5) Rubric rows generated from assignment role data
Rubrics read from configured assignment duties and generate role-specific rows dynamically. This ensures rubric behavior reflects persisted configuration, not stale client-only state.
6) Optimistic add/remove for role assignment
When an instructor adds or removes a duty from an assignment, the UI updates immediately without waiting for the API response. If the request fails, the change is rolled back and the previous state is restored. This keeps the editor responsive for operations that are expected to succeed in the common case.
Testing Plan
Backend testing
Request specs cover functional and defensive cases:
- Assignment payload includes role metadata.
- Limit update success and persistence.
- Invalid limit returns
422. - Non-assigned duty update returns
404. - Unauthorized modification returns
403.
Primary files:
spec/requests/api/v1/assignments_duties_controller_spec.rbspec/requests/api/v1/teams_participants_controller_spec.rb
Frontend testing
Executed test command:
npm run test -- src/pages/Assignments/AssignmentEditor.test.tsx --run
Behavior validated:
- Rubrics round-variation behavior remains correct.
- Role-specific teammate rows appear only when role-based mode is enabled and roles exist.
- Review Strategy role controls are visible only in role-based mode.
- Inline create-role entry flow visibility and control behavior are correct.
UI Test Flow
Use the following instructor flow to validate behavior manually:
- Open Assignment Editor.
- Go to Review Strategy.
- Enable
Is role based?. - Assign one or more existing roles from the dropdown — they appear immediately in the table (optimistic update).
- Click
Create a new roleif needed, fill in the role name, then confirm. - Edit the
Max membersvalue inline in the assigned roles table; click away to save. - Navigate to Rubrics.
- Enable
review_rubric_varies_by_role. - Confirm rows
Teammate Review for {RoleName}are rendered for each assigned role. - Uncheck
review_rubric_varies_by_roleand verify role rows disappear. - Return to Review Strategy, disable
Is role based?, and verify role controls are hidden.
Future Work
- Team-level role-capacity enforcement UX feedback.
- Additional integrated end-to-end tests across instructor and student workflows.
References
- Fall 2016 role-based reviewing: https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2016_E1676_Role-based_reviewing
- Fall 2017 role-based reviewing: https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Fall_2017/E1798_Role-based_Reviewing