CSC/ECE 517 Spring 2026 - E2601. Reimplement student quizzes
Expertiza Background
Expertiza is an open-source peer review platform built on Ruby on Rails and maintained at North Carolina State University. It is used across multiple courses at NCSU and other universities to facilitate structured peer feedback, team-based assignments, and multi-round review workflows.
In Expertiza, instructors define assignments that walk students through a sequence of tasks — submitting work, reviewing peers, taking quizzes, and providing feedback. The system tracks participants, teams, response maps, and responses across all of these activities.
The ongoing reimplementation effort modernizes the Expertiza codebase into a clean Rails JSON API backend and a React SPA frontend, with a focus on proper separation of concerns, RESTful conventions, and comprehensive test coverage. Each semester, CSC 517 students contribute targeted reimplementations of specific subsystems as part of their graduate coursework in object-oriented design.
Project Description
This project — E2601: Reimplement Student Quizzes — focused on the backend infrastructure that governs how students interact with quiz tasks within an assignment workflow. Quizzes in Expertiza are not standalone — they are one task type within a larger ordered sequence that may also include submission, peer review, and feedback stages.
For quizzes to work correctly in this context, three backend systems were built:
- A task ordering engine — a dedicated
TaskOrderingmodule that knows the correct sequence of tasks for a participant and determines whether a student is eligible to take a quiz at any given moment - A student-facing task API — a
StudentTasksControllerthat surfaces the current state of a student's task queue, including which tasks are complete, which is next, and how to start a quiz task - A response management API — a
ResponsesControllerthat handles the creation and updating of quiz responses with proper authentication, ownership checks, and queue-order enforcement
Without all three of these systems working correctly together, a student could take a quiz before completing required prerequisite tasks, or be blocked from taking a quiz they were legitimately eligible for.
Why We Are Refactoring
After completing the initial implementation, feedback was received that the TaskOrdering module introduced unnecessary complexity. The module spans five separate files and a dedicated namespace — BaseTask, ReviewTask, QuizTask, TaskFactory, and TaskQueue — yet its logic is only ever called from two controllers. This level of abstraction is harder to trace and maintain than the problem warrants.
The planned refactor will remove the TaskOrdering namespace entirely and move all sequencing logic directly into StudentTasksController as private methods, supported by two polymorphic inner classes. Every existing API endpoint, response payload shape, and status code will remain unchanged. The refactor is purely an internal structural simplification.
The sections below describe what we built first and, within each section, how it will change after the refactor.
What We Built
Task Ordering Engine (Current Implementation)
We built a self-contained TaskOrdering module under app/models/task_ordering/ that encapsulates all logic for determining task eligibility and sequence. This module is intentionally decoupled from controllers — it knows nothing about HTTP requests or responses, only about assignments, participants, and response maps.
The module consists of five classes:
| Class | Role |
|---|---|
BaseTask |
Abstract base defining the shared interface: complete? and map_id
|
ReviewTask |
Concrete task for ReviewResponseMap — complete when is_submitted is true
|
QuizTask |
Concrete task for quiz response maps — the central task type for this project |
TaskFactory |
Factory that instantiates the correct task class given a response map |
TaskQueue |
Orchestrator that builds and queries the ordered task list for a participant |
The TaskQueue is the primary interface that controllers use. Given an assignment and a TeamsParticipant, it answers three questions:
- Is this map part of the participant's task queue? (
map_in_queue?) - Have all tasks before this one been completed? (
prior_tasks_complete_for?) - What is the next task the student should work on? (
next_incomplete_task)
What Changed
The TaskOrdering namespace was removed from the application code. Its orchestration responsibilities now live on StudentTask as shared class methods used by both StudentTasksController and ResponsesController.
BaseTask was replaced by StudentTask::BaseTaskItem. QuizTask and ReviewTask were replaced by QuizTaskItem and ReviewTaskItem, implemented as model classes that inherit from StudentTask::BaseTaskItem.
TaskFactory and TaskQueue were eliminated. Their former responsibilities are now handled by StudentTask.build_tasks, StudentTask.find_task_for_map, StudentTask.prior_tasks_complete?, and StudentTask.ensure_response_objects!.
The inner classes will share the following interface via BaseTaskItem:
| Method | Purpose |
|---|---|
response_map |
Returns the associated response map (creates one for QuizTaskItem if needed)
|
ensure_response_map! |
Guarantees a response map record exists |
ensure_response! |
Creates or finds the Response record (round: 1, is_submitted: false by default)
|
completed? |
Returns true when a submitted response exists for this map |
to_h |
Serializes the task to the same stable JSON payload shape as today |
QuizTaskItem will implement response_map as follows: first return a cached map if already loaded; otherwise look for an existing QuizResponseMap by reviewer and reviewee; if none exists and a quiz questionnaire is present, create one with save!(validate: false); if no questionnaire exists either, return nil. ReviewTaskItem simply returns the ReviewResponseMap it was initialized with.
Student Tasks API (Current Implementation)
We implemented StudentTasksController with five endpoints that give students full visibility into their task workload:
| Endpoint | Method | What It Returns |
|---|---|---|
/student_tasks/list |
GET | All tasks for the current user across their assignments |
/student_tasks/view |
GET | Detailed information for a specific participant task |
/student_tasks/queue |
GET | The full ordered task queue for a given assignment |
/student_tasks/next_task |
GET | The next incomplete task the student should work on |
/student_tasks/start_task |
POST | Attempts to start a task — blocked if prerequisites are incomplete |
Currently, every endpoint that involves task ordering resolves the student's AssignmentParticipant and TeamsParticipant records and then constructs a TaskQueue instance to answer eligibility questions.
What Will Change
The list, view, and show actions will remain as-is. The queue, next_task, and start_task actions will be refactored to call private controller methods instead of TaskQueue.
queue after refactor:
- Call
resolve_context_for_assignment(params[:assignment_id])to obtain participant, team membership, assignment, and duty. Return 404 if the participant cannot be found. - Call
build_tasks(context)to construct the ordered task list. - Call
ensure_response_objects!(tasks)to guarantee response maps and response records exist for every task. - Render
tasks.map(&:to_h)as JSON.
next_task after refactor:
- Resolve context and build tasks as above.
- Find the first task where
completed?returns false. - Render that task's
to_hpayload, or render{ message: "All tasks completed" }if all tasks are done.
start_task after refactor:
- Find the
ResponseMapbyparams[:response_map_id]. Return 404 if not found. - Verify
map.reviewer.user_id == current_user.id. Return 403 if not. - Call
resolve_context_for_participant(map.reviewer)to build the context. - Call
build_tasks(context)and locate the task for this map viafind_task_for_map(tasks, map.id). Return 404 if the map is not in the participant's queue. - Call
prior_tasks_complete?(tasks, current_task). Return 403 with "Complete previous task first" if any earlier task is still incomplete. - Call
current_task.ensure_response!and render the task payload.
The key private methods introduced are:
| Method | Role |
|---|---|
StudentTask.resolve_context_for_assignment(user, assignment_id) |
Finds the participant for the current user and assignment, then resolves the team participant and duty |
StudentTask.resolve_context_for_participant(participant) |
Builds the same task context starting from a known participant |
StudentTask.build_tasks(context) |
Builds the ordered quiz/review task list |
StudentTask.ensure_response_objects!(tasks) |
Ensures response maps and response records exist |
StudentTask.find_task_for_map(tasks, map_id) |
Finds the task associated with a response map |
StudentTask.prior_tasks_complete?(tasks, current_task) |
Checks whether all earlier tasks are submitted |
Task ordering rules in build_tasks will be: if review maps exist, append a QuizTaskItem first (when duty allows and a quiz questionnaire or existing quiz map is present) then a ReviewTaskItem (when duty allows); if no review maps exist, append a quiz-only QuizTaskItem when duty allows and a questionnaire exists. Duty permissions are resolved as: duty_allows_quiz? is true for participant, reader, and mentor roles; duty_allows_review? is true for participant, reader, reviewer, and mentor roles.
Response Management API (Current Implementation)
We implemented ResponsesController with three endpoints that handle quiz and review response lifecycle:
| Endpoint | Method | What It Does |
|---|---|---|
/responses |
POST | Creates a new response for a quiz or review map |
/responses/:id |
GET | Retrieves a specific response |
/responses/:id |
PATCH | Updates an existing response |
Response creation currently enforces two layers of protection before any data is written:
- Ownership check — the requesting user must be the reviewer assigned to the response map
- Queue order check —
TaskQueuemust confirm that all prerequisite tasks are complete before this response can be created
What Changed
The GET endpoint is unaffected. The POST and PATCH endpoints now both call enforce_task_order!, which reuses the shared StudentTask task-building and prerequisite-checking methods.
Technical Deep Dive
How Task Ordering Works (Current)
When a student attempts to start a quiz task or create a response today, the following sequence occurs:
StudentTasksController or ResponsesController
|
| TaskQueue.new(assignment, teams_participant)
v
TaskQueue
→ fetches all ResponseMaps for this participant
→ calls TaskFactory.build(map) for each
→ builds ordered array of BaseTask subclass instances
|
| queue.prior_tasks_complete_for?(map_id)
v
→ iterates tasks in order
→ for each task before the target, calls task.complete?
→ returns false if any prior task is incomplete
|
v
Controller either proceeds or renders 403/428
How Task Sequencing Will Work After the Refactor
The same logical flow will execute, but entirely inside the controller with no external module:
StudentTasksController or ResponsesController
|
| resolve_context_for_assignment(assignment_id)
v
→ finds AssignmentParticipant by current_user + assignment_id
→ finds TeamsParticipant by participant_id
→ resolves duty (team_participant.duty_id fallback to participant.duty_id)
|
| build_tasks(context)
v
→ queries ReviewResponseMaps for this participant
→ loads quiz questionnaire via assignment.quiz_questionnaire_for_review_flow
→ checks for existing QuizResponseMaps
→ instantiates QuizTaskItem / ReviewTaskItem in correct order
|
| prior_tasks_complete?(tasks, current_task)
v
→ iterates tasks before the target
→ calls task.completed? on each
→ returns false if any prior task is incomplete
|
v
Controller either proceeds or renders 403/428
The outcome for every request — which status codes are returned, which tasks are blocked, what JSON is rendered — is identical to today. Only the internal path through the code changes.
How Authentication and Authorization Are Layered
This layer is unchanged by the refactor. All requests pass through two concerns registered in ApplicationController before reaching any controller logic:
Incoming Request
|
v
JwtToken concern → authenticate_request!
→ reads Authorization: Bearer <token> header
→ decodes token using RSA public key
→ sets @current_user via User.find(auth_token[:id])
→ halts with 401 if token missing, expired, or invalid
|
v
Authorization concern → authorize
→ calls all_actions_allowed?
→ checks super-admin privileges OR action_allowed?
→ halts with 403 if not permitted
|
v
Controller before_actions (e.g. find_and_authorize_map_for_create)
→ map-level ownership checks using current_user
|
v
Controller action
The critical constraint is that find_and_authorize_map_for_create must remain a standard before_action — not a prepend_before_action — so that current_user is always populated by the time ownership checks run.
Round-Aware Response Handling
This behavior is unchanged by the refactor. When a response is created, the controller scopes its lookup by both map_id and round:
Response.where(map_id: @map.id, round: round)
.order(:created_at)
.last || Response.new(map_id: @map.id, round: round)
If a response already exists for this map and round, it is updated in place. If none exists, a new one is initialized. This supports quiz retakes and multi-round review scenarios.
Design Decisions
Original Design: Separation of Task Logic into a Dedicated Module
The initial implementation encapsulated all task sequencing and eligibility logic within the TaskOrdering module, entirely decoupled from controllers. The reasoning was that controllers should only handle HTTP concerns, while task logic should be independently testable as a pure domain model.
The TaskFactory was introduced so that the correct task class could be instantiated from any response map type without conditional logic in controllers. The TaskQueue served as the single orchestration point so that the same ordering rules were guaranteed to apply whether a student was viewing their queue, starting a task, or submitting a response.
Why the Design Is Being Simplified
In practice, the TaskOrdering module is only ever called from StudentTasksController and ResponsesController. There are no background jobs, rake tasks, or alternative interfaces that reuse it. The five-file namespace therefore adds indirection without providing the reuse benefits that would justify it.
The refactor recognizes that polymorphism belongs where it helps — quiz and review tasks genuinely behave differently, so inner classes are still the right tool. But orchestration does not need its own namespace — a set of private controller methods is simpler to read, easier to test through the request layer, and just as correct.
Refactored Design: Controller-Owned Orchestration with Inner Classes
After the refactor, the design will follow these principles:
- Single orchestration owner — all task sequencing decisions flow through one set of private methods in
StudentTasksController.ResponsesControllerwill reuse the same logic via a shared concern or equivalent private methods. - Polymorphism without over-engineering —
QuizTaskItemandReviewTaskItemare inner classes that share aBaseTaskIteminterface. The difference in behavior is encapsulated; the orchestration logic treats both identically. - No extra namespace — everything lives in
app/controllers/student_tasks_controller.rb. A developer reading thequeueaction can follow the entire flow without opening any other file. - High cohesion — task lifecycle behavior (response map creation, response initialization, completion detection, serialization) is grouped with the task objects themselves.
- Low coupling — the controller works against the shared
BaseTaskIteminterface, so adding a new task type requires only a new inner class with no changes to orchestration methods.
Layered Authorization and Validation
This decision is unchanged. Validation is enforced at three distinct layers: JWT authentication globally in ApplicationController, role-level authorization via the authorize concern, and map-level ownership checks as controller before_action callbacks. Task-order enforcement is the final layer, applied inside the action itself. This defense-in-depth approach remains intact after the refactor.
Round-Aware Response Handling
Unchanged. Responses continue to be scoped by map_id and round, with update-in-place semantics for existing records.
Emphasis on RESTful and Stateless Design
Unchanged. All endpoints remain resource-based with JWT authentication, compatible with the React frontend and stateless horizontal scaling.
Test Coverage
Current Test Suite
The initial implementation is covered by 77 passing examples across model and request specs:
| File | Key Scenarios Tested |
|---|---|
spec/models/task_ordering/base_task_spec.rb |
Default complete? behavior, map_id delegation, interface contract
|
spec/models/task_ordering/review_task_spec.rb |
Completion based on is_submitted, incomplete when not submitted
|
spec/models/task_ordering/quiz_task_spec.rb |
Quiz-specific completion detection and eligibility |
spec/models/task_ordering/task_factory_spec.rb |
Correct class returned for each map type, error on unknown type |
spec/models/task_ordering/task_queue_spec.rb |
Queue ordering, map_in_queue?, prior_tasks_complete_for?, next_incomplete_task
|
spec/requests/api/v1/student_tasks_controller_spec.rb |
list, view, queue, next_task, start_task — response codes 200, 401, 404, 500 |
spec/requests/api/v1/responses_controller_spec.rb |
POST, GET, PATCH /responses — response codes 201, 200, 401, 403, 404 |
To run the current suite:
bundle exec rspec \ spec/requests/api/v1/student_tasks_controller_spec.rb \ spec/requests/api/v1/responses_controller_spec.rb \ spec/models/task_ordering/base_task_spec.rb \ spec/models/task_ordering/review_task_spec.rb \ spec/models/task_ordering/quiz_task_spec.rb \ spec/models/task_ordering/task_factory_spec.rb \ spec/models/task_ordering/task_queue_spec.rb
Expected result: 77 examples, 0 failures
Planned Test Suite After Refactor
The five spec/models/task_ordering/* files will be deleted alongside the source files they test. They will be replaced by the following:
| File | What It Will Cover |
|---|---|
spec/requests/api/v1/student_tasks_controller_spec.rb (extended) |
Queue order — quiz before review when both exist; quiz-only when no review maps; review-only when duty disallows quiz; empty queue; next_task returns first incomplete task, then review after quiz submitted, then completion message; start_task blocks when prior task incomplete, rejects map not in queue, rejects map owned by another user, returns 404 for nonexistent map
|
spec/requests/api/v1/responses_controller_spec.rb (extended) |
POST blocked when prior task incomplete; POST allowed when prerequisites complete; PATCH blocked/allowed under same conditions; ownership checks preserved |
spec/controllers/student_tasks_controller_task_items_spec.rb (new) |
QuizTaskItem: reuses existing quiz map for reviewer/reviewee; creates quiz map when questionnaire exists and none is present; returns nil map when questionnaire absent and no existing map; ensure_response! creates record with round: 1 and is_submitted: false. ReviewTaskItem: returns the given review map; completed? is true only when a submitted response exists. Shared: to_h always includes the stable payload keys
|
The following payload keys must remain present and unchanged across all student task endpoints:
task_typeassignment_idresponse_map_idresponse_map_typereviewee_idteam_participant_id
Response code contracts that must not regress:
| Endpoint Group | Status Codes |
|---|---|
| Student Tasks (list, view, queue, next_task, start_task) | 200, 401, 403, 404, 500 |
| Responses (POST, GET, PATCH) | 201, 200, 401, 403, 404 |
Migration Plan
The refactor will proceed in four phases to avoid breaking the working implementation at any intermediate step.
Phase 1 — Add Inner Task Classes
Implement BaseTaskItem, QuizTaskItem, and ReviewTaskItem as inner classes inside StudentTasksController. The existing TaskOrdering module remains untouched. No controller behavior changes in this phase — the new classes exist but are not yet wired in.
Phase 2 — Move Orchestration Logic into the Controller
Add the private controller methods (resolve_context_for_assignment, resolve_context_for_participant, build_tasks, ensure_response_objects!, find_task_for_map, prior_tasks_complete?) to StudentTasksController. Refactor the queue, next_task, and start_task actions to call these methods instead of TaskQueue. Update ResponsesController#enforce_task_order! to use the same logic. Run the full test suite to confirm no regressions before proceeding.
Phase 3 — Remove the Legacy Layer
Delete:
app/models/task_ordering/base_task.rbapp/models/task_ordering/review_task.rbapp/models/task_ordering/quiz_task.rbapp/models/task_ordering/task_factory.rbapp/models/task_ordering/task_queue.rbspec/models/task_ordering/base_task_spec.rbspec/models/task_ordering/review_task_spec.rbspec/models/task_ordering/quiz_task_spec.rbspec/models/task_ordering/task_factory_spec.rbspec/models/task_ordering/task_queue_spec.rb
Phase 4 — Update Tests and Documentation
Write the new request specs and the student_tasks_controller_task_items_spec.rb unit spec as described in the Planned Test Suite section. Update this wiki page to move the "Planned Refactor" content into the past tense once the work is complete.
Definition of Done
StudentTasksControllerandResponsesControllerno longer referenceTaskOrdering.- Sequencing and gating logic lives in shared
StudentTaskclass methods. - Quiz/review differences are encapsulated by
QuizTaskItemandReviewTaskItem. - Legacy
app/models/task_orderingsource files have been removed.
Demo Video
Demo Video for Old Implementation: https://youtu.be/Zg-fQmIUCSc Demo Video for Refactor: https://youtu.be/fhK0roDhT7k
The demo walks through the current (pre-refactor) implementation:
- The
TaskOrderingengine enforcing sequential task completion for quiz tasks - Live API calls to the student tasks endpoints showing queue state and next task resolution
- Response creation flow including JWT authentication, map ownership verification, and task queue enforcement
- Full RSpec test suite run showing all 77 passing examples
Future Work
- Extend
build_tasksto support deadline-aware ordering — tasks past their due date could be automatically skipped or flagged - Add per-question completion tracking for quiz responses, rather than treating a response as binary submitted/not-submitted
- Expose a participant-level quiz completion percentage endpoint for frontend progress indicators
- Add new inner task classes to
StudentTasksControllerto support additional response map types as new workflow stages are added to Expertiza - Add admin endpoints to inspect or manually override a participant's queue state for debugging and support purposes
References
- Expertiza GitHub Repository
- Ruby on Rails Guides
- RSpec Documentation
- Rswag GitHub Repository
- JWT Ruby Gem
Team
Members:
- Akhil Kumar
- Dev Patel
- Arnav Mejari
Mentor:
- Vihar Manojkumar Shah
Last updated: April 2026 | CSC 517, Spring 2026, NCSU