CSC/ECE 517 Spring 2026 - E2601. Reimplement student quizzes

From Expertiza_Wiki
Jump to navigation Jump to search

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 TaskOrdering module 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 StudentTasksController 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 — a ResponsesController 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.

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:

  1. Call resolve_context_for_assignment(params[:assignment_id]) to obtain participant, team membership, assignment, and duty. Return 404 if the participant cannot be found.
  2. Call build_tasks(context) to construct the ordered task list.
  3. Call ensure_response_objects!(tasks) to guarantee response maps and response records exist for every task.
  4. Render tasks.map(&:to_h) as JSON.

next_task after refactor:

  1. Resolve context and build tasks as above.
  2. Find the first task where completed? returns false.
  3. Render that task's to_h payload, or render { message: "All tasks completed" } if all tasks are done.

start_task after refactor:

  1. Find the ResponseMap by params[:response_map_id]. Return 404 if not found.
  2. Verify map.reviewer.user_id == current_user.id. Return 403 if not.
  3. Call resolve_context_for_participant(map.reviewer) to build the context.
  4. Call build_tasks(context) and locate the task for this map via find_task_for_map(tasks, map.id). Return 404 if the map is not in the participant's queue.
  5. Call prior_tasks_complete?(tasks, current_task). Return 403 with "Complete previous task first" if any earlier task is still incomplete.
  6. 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:

  1. Ownership check — the requesting user must be the reviewer assigned to the response map
  2. Queue order checkTaskQueue must 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. ResponsesController will reuse the same logic via a shared concern or equivalent private methods.
  • Polymorphism without over-engineeringQuizTaskItem and ReviewTaskItem are inner classes that share a BaseTaskItem interface. 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 the queue action 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 BaseTaskItem interface, 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_type
  • assignment_id
  • response_map_id
  • response_map_type
  • reviewee_id
  • team_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.rb
  • app/models/task_ordering/review_task.rb
  • app/models/task_ordering/quiz_task.rb
  • app/models/task_ordering/task_factory.rb
  • app/models/task_ordering/task_queue.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

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

  • StudentTasksController and ResponsesController no longer reference TaskOrdering.
  • Sequencing and gating logic lives in shared StudentTask class methods.
  • Quiz/review differences are encapsulated by QuizTaskItem and ReviewTaskItem.
  • Legacy app/models/task_ordering source 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 TaskOrdering engine 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_tasks 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 binary submitted/not-submitted
  • Expose a participant-level quiz completion percentage endpoint for frontend progress indicators
  • Add new inner task classes to StudentTasksController to 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

Team

Members:

  • Akhil Kumar
  • Dev Patel
  • Arnav Mejari

Mentor:

  • Vihar Manojkumar Shah

Last updated: April 2026 | CSC 517, Spring 2026, NCSU