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 or significantly repaired:
- A task ordering engine that knows the correct sequence of tasks for a participant and can determine whether a student is eligible to take a quiz at any given moment
- A student-facing task API that 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 that 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.
Following feedback received after the initial implementation, a simplification refactor is planned. The goal is to remove the separate TaskOrdering module and fold sequencing logic directly into StudentTasksController, reducing the number of files and abstraction layers while preserving all existing behavior. This refactor is described in the Planned Refactor section below.
What We Built
Task Ordering Engine
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 can answer 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)
Student Tasks API
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 |
Every endpoint requires a valid JWT token and resolves the student's AssignmentParticipant and TeamsParticipant records before delegating to a TaskQueue instance for eligibility decisions.
Response Management API
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 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 — the
TaskQueuemust confirm that all prerequisite tasks are complete before this response can be created
Technical Deep Dive
How Task Ordering Works
When a student attempts to start a quiz task or create a response, 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 Authentication and Authorization Are Layered
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 insight here is that find_and_authorize_map_for_create must be a standard before_action — not a prepend_before_action — so that current_user is always populated by the time ownership checks run. This was a central bug we fixed (see Bugs We Fixed).
Round-Aware Response Handling
Expertiza supports multi-round assignment workflows. 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)
This means that if a response already exists for this map and round, it is updated in place rather than duplicated. If no response exists yet, a new one is initialized. This supports quiz retakes and multi-round review scenarios cleanly.
Design Decisions
The reimplementation of Student Quizzes emphasized modularity, separation of concerns, and extensibility to ensure the system can evolve alongside future Expertiza features.
Decoupling Task Logic from Controllers
A key architectural decision was to encapsulate all task sequencing and eligibility logic within the TaskOrdering module, rather than embedding it directly in controllers. Controllers are responsible only for handling HTTP requests and responses, while TaskOrdering operates purely on domain objects such as assignments, participants, and response maps.
This separation improves testability, as task logic can be validated independently of the API layer, and ensures that future interfaces (e.g., background jobs or alternative frontends) can reuse the same core logic without duplication.
Use of the Factory Pattern
The introduction of TaskFactory allows the system to dynamically instantiate the correct task type based on the underlying response map. This avoids conditional logic scattered throughout the codebase and centralizes object creation in a single location.
By adhering to the factory pattern, the system becomes easier to extend — new task types can be added with minimal modification to existing code, requiring only the addition of a new task class and a corresponding mapping in the factory.
Centralized Task Orchestration via TaskQueue
The TaskQueue class serves as the single source of truth for task ordering and progression. Instead of allowing each controller to independently determine task eligibility, all such decisions are delegated to TaskQueue.
This design ensures consistency across the system: whether a student is viewing their tasks, starting a quiz, or submitting a response, the same ordering rules are enforced. It also reduces duplication and prevents subtle inconsistencies that could arise if multiple components attempted to implement ordering logic independently.
Layered Authorization and Validation
Another important design decision was to enforce validation at multiple layers of the request lifecycle. Authentication and high-level authorization are handled globally through concerns in ApplicationController, while resource-specific checks (such as map ownership) are implemented as controller before_action callbacks.
Finally, task-order enforcement is delegated to TaskQueue, ensuring that even if a request passes authentication and ownership checks, it cannot violate assignment workflow constraints. This layered approach provides defense in depth and reduces the likelihood of invalid state transitions.
Round-Aware Response Handling
To support Expertiza's multi-round workflow model, responses are scoped by both map_id and round. Instead of creating duplicate responses for the same round, the system updates the most recent response if it exists, or initializes a new one otherwise.
This design avoids redundant data while enabling clean support for quiz retakes and iterative peer review cycles.
Emphasis on RESTful and Stateless Design
All APIs were designed following RESTful principles, with clear resource-based endpoints and appropriate use of HTTP methods. Authentication is handled using JWT tokens, allowing the backend to remain stateless and scalable.
This approach ensures compatibility with modern frontend frameworks such as React and simplifies horizontal scaling by eliminating the need for server-side session storage.
Test Coverage
Model 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
|
Request Specs
| File | Endpoints | Response Codes Tested |
|---|---|---|
spec/requests/api/v1/student_tasks_controller_spec.rb |
list, view, queue, next_task, start_task | 200, 401, 404, 500 |
spec/requests/api/v1/responses_controller_spec.rb |
POST /responses, GET /responses/:id, PATCH /responses/:id | 201, 200, 401, 403, 404 |
Running the Tests
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
Demo Video
Demo Video: https://youtu.be/Zg-fQmIUCSc
The demo walks through:
- The task ordering system 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
Planned Refactor
Following feedback on the initial implementation, the TaskOrdering module will be simplified. Rather than maintaining a separate namespace with five model-layer classes, all sequencing and eligibility logic will be moved directly into StudentTasksController, supported by two polymorphic inner classes. The public API contract and all existing behavior will remain unchanged.
Motivation
The TaskOrdering module introduced five separate files (BaseTask, ReviewTask, QuizTask, TaskFactory, TaskQueue) to solve a problem that does not require a dedicated namespace. Task sequencing is only ever invoked from two controllers — StudentTasksController and ResponsesController — and the added abstraction layers make the flow harder to trace without providing meaningful reuse benefits. The refactor aims to make the code simpler to read and maintain while preserving all correctness guarantees.
Proposed Design
The replacement design uses controller-owned private methods for orchestration, and two inner classes for polymorphic task-type behavior:
| Inner Class | Role |
|---|---|
QuizTaskItem |
Handles quiz-specific completion detection, response map lookup/creation, and serialization |
ReviewTaskItem |
Handles review map completion based on is_submitted
|
Both classes will share a common interface via a parent BaseTaskItem, with the following methods:
| Method | Purpose |
|---|---|
response_map |
Returns the associated response map (creates one for quiz tasks 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 a stable JSON payload shape |
The private controller methods that will orchestrate the task flow are:
| Method | Role |
|---|---|
build_tasks(context) |
Builds the ordered task list (quiz before review) for a participant |
ensure_response_objects!(tasks) |
Ensures response maps and responses exist for all tasks |
prior_tasks_complete?(tasks, target) |
Returns false if any task before the target is incomplete |
find_task_for_map(tasks, map_id) |
Looks up a task by its response map id |
resolve_context_for_assignment(assignment_id) |
Resolves participant, team membership, assignment, and duty from a given assignment id |
How Task Sequencing Will Work After the Refactor
The request flow through StudentTasksController and ResponsesController will change from delegating to TaskQueue to calling private methods directly:
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
→ loads ReviewResponseMaps for this participant
→ loads quiz questionnaire and existing QuizResponseMaps
→ instantiates QuizTaskItem / ReviewTaskItem in 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
Task ordering rules will remain identical:
- If review maps exist: quiz task is added first (when duty allows and quiz is available or an existing quiz map is present), then review task (when duty allows).
- If no review maps exist: a quiz-only task is added when duty allows and a quiz questionnaire exists.
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 QuizTaskItem and ReviewTaskItem as inner classes inside StudentTasksController. The existing TaskOrdering module will remain untouched at this stage.
Phase 2 — Move orchestration logic into controller
Port queue construction and gating logic into private controller methods (build_tasks, prior_tasks_complete?, etc.). Update StudentTasksController and ResponsesController to use the new inner classes and private methods instead of TaskQueue. The JSON payload shape will not change.
Phase 3 — Remove the legacy layer
Delete the following files once all controllers and specs have been updated:
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.rb
Phase 4 — Update tests and documentation
Replace spec/models/task_ordering/* with:
- Request/integration specs for
queue,next_task, andstart_taskcovering all ordering scenarios - Focused controller unit specs covering
QuizTaskItemandReviewTaskItembehavior
Update docs/POSTMAN_STUDENT_TASKS.md if any wording references TaskOrdering.
Planned Test Coverage After Refactor
| File | Key Scenarios to Cover |
|---|---|
spec/requests/api/v1/student_tasks_controller_spec.rb |
Queue order (quiz before review), quiz-only, review-only, empty queue, start_task blocking out-of-order attempts, unauthorized map access
|
spec/requests/api/v1/responses_controller_spec.rb |
Response create/update blocked when prior task incomplete, allowed when prerequisites complete, authorization checks preserved |
spec/controllers/student_tasks_controller_task_items_spec.rb |
QuizTaskItem: reuses existing map, creates map when questionnaire present, returns nil when absent; ReviewTaskItem: returns given map, completion based on submitted response; shared to_h payload contract
|
The following payload keys must remain stable across all student task endpoints after the refactor:
task_typeassignment_idresponse_map_idresponse_map_typereviewee_idteam_participant_id
Definition of Done
StudentTasksControllerandResponsesControllerno longer referenceTaskOrdering.- Sequencing and gating logic lives in private controller methods with no extra namespace.
- Quiz/review differences are encapsulated by polymorphic
QuizTaskItemandReviewTaskIteminner classes. - All legacy
task_orderingfiles are deleted. - All specs pass with no API contract regression.
Future Work
- Extend task-building logic to 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 fully submitted or not
- Expose a participant-level quiz completion percentage endpoint for frontend progress indicators
- Add new inner task classes to support additional response map types as new assignment 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 Merjeri
Mentor:
- Vihar Manojkumar Shah
Last updated: April 2026 | CSC 517, Spring 2026, NCSU