CSC/ECE 517 Spring 2025 - E2514. Reimplement student quizzes controller.rb
Introduction
Expertiza is an educational web application collaboratively developed and maintained by students and faculty at NCSU. As an open-source project built on the Ruby on Rails platform, its code is accessible on GitHub. The platform enables students to provide peer reviews and refine their work based on feedback.
Project Overview
Quizzes in Expertiza are designed to ensure that reviewers comprehend the material they are evaluating. When an assignment includes quizzes, submitting teams create these quizzes based on their work. Reviewers must complete the quizzes before reviewing to demonstrate their understanding. If a reviewer performs poorly, their review can be discounted to maintain quality.
The student_quizzes_controller.rb manages quiz-related actions, including quiz creation, scoring, and recording responses for both students and reviewers. However, the existing implementation violates several key Rails principles, including the DRY (Don't Repeat Yourself) principle and the improper placement of business logic within the controller. Instead, this logic should reside in model classes, in adherence to the MVC (Model-View-Controller) architecture.
Previous Implementation
The previous implementation of the student_quizzes_controller.rb contained several design issues. While some progress was made, such as moving calculate_score and process_answers to the ResponseMap model and replacing role ID checks with Role model methods, several key issues remained. Business logic was still embedded in the controller, violating the Single Responsibility Principle (SRP). Method names were unclear, like set_student_quiz, and resource-finding logic was incorrectly placed in the controller. Additionally, quiz retake policies and question-skipping lacked adequate support. Testing coverage was also insufficient, and documentation needed improvement. Fixing these issues will significantly enhance the code's maintainability, clarity, and overall structure.
Project Goals
This project focuses on reimplementing student_quizzes_controller.rb in the reimplementation-backend repository, addressing issues from the previous implementation. Key improvements will include:
- Improving method names and readability to better align with their purposes
- Refactoring to remove redundant code and consolidate functionality into reusable functions
- Enhancing test coverage and documentation clarity
By addressing these issues, the project aims to produce a well-structured, maintainable, and scalable student_quizzes_controller.rb that follows Rails best practices, improves code organization, and enhances the overall maintainability of the Expertiza platform.
Design
Key Features
1) Controller Actions: The StudentQuizzesController is responsible for managing quiz-related functionalities such as creating quizzes, assigning quizzes to students, submitting answers, updating quizzes, and deleting quizzes. Below is an overview of the main actions and their responsibilities:
- Index
(GET /student_quizzes)
: Retrieves all available quizzes (questionnaires) from the database. - Show
(GET /student_quizzes/:id)
: Fetches a single quiz (questionnaire) by its ID. - Create
(POST /student_quizzes)
: Creates a new quiz along with its associated questions and answers. - Assign Quiz
(POST /student_quizzes/assign)
: Assigns a specific quiz to a student. - Submit Quiz
(POST /student_quizzes/submit_answers)
: Allows the student to submit answers for a quiz and calculates the total score. - Update
(PUT /student_quizzes/:id)
: Updates an existing quiz. - Destroy
(DELETE /student_quizzes/:id)
: Deletes a specific quiz.
2) Services and Helpers: Several helper methods and service objects are used to encapsulate complex logic and improve code readability.
- FindResourceService: This service is responsible for fetching resources (e.g.,
Participant
,Questionnaire
) from the database. It helps keep the controller code clean and focused on the action logic. - ResponseMap: This class is used to map students to quizzes and track their responses. The controller checks if a student is already assigned to a quiz before creating a new assignment. It also handles the processing of answers and the calculation of total scores.
3) Role-Based Access Control: The before_action
callback check_instructor_role
ensures that only users with an instructor role (role_id = 2) can perform certain actions, such as assigning quizzes and creating new ones. This ensures that only authorized users can access sensitive functionalities.
4) Error Handling: The controller uses standard error handling to ensure the system responds gracefully in case of any issues. If an error occurs (e.g., quiz already assigned, invalid data), a custom error message is rendered along with the appropriate HTTP status code.
For example, when a quiz is assigned to a student, if the student is already assigned, the system returns a unprocessable_entity error with a descriptive message.
5) Transaction Handling: The create
and submit_quiz
actions use database transactions (ActiveRecord::Base.transaction) to ensure that operations are atomic. If any part of the process fails (e.g., saving answers), the entire transaction is rolled back to maintain data integrity.
UML Diagram
Sequence of Actions in Assigning a Quiz
1) The instructor calls the POST /student_quizzes/assign
endpoint to assign a quiz.
2) The assign_quiz
method is invoked:
- FindParticipant: The
FindResourceService
is called to retrieve the Participant and Questionnaire by their respective IDs. - Check Assignment: The
quiz_assigned?
method checks whether the quiz has already been assigned to the student. - Build ResponseMap: A new
ResponseMap
is created, linking the student to the quiz. - Save ResponseMap: The
ResponseMap
is saved to the database, completing the assignment process. - The system responds with the newly created ResponseMap and a success message.
Sequence of Actions in Submitting Quiz Answers
1) The student submits their answers to the quiz via the POST /student_quizzes/submit_answers
endpoint.
2) The submit_quiz
method is invoked:
- Find ResponseMap: The system retrieves the
ResponseMap
for the current user and quiz. - Process Answers: The student's answers are processed and associated with the
ResponseMap
. - Calculate Score: The
calculate_score
method is called to compute the total score based on the answers provided. - Update ResponseMap: The
ResponseMap
is updated with the total score. - The system responds with the calculated score.
Files Modified
- app/models/response_map.rb
- app/controllers/api/v1/student_quizzes_controller.rb
- app/services/find_resource_service.rb
- app/models/response.rb
- spec/requests/api/v1/student_quizzes_controller_spec.rb
- spec/services/resource_map_spec.rb
Changes Implemented
1) Moving Find Resource Out of Controller
The method renaming and extraction of logic to a service are intended to improve the maintainability, readability, and scalability of the code. The refactor helps ensure that the controller remains focused on its core responsibility of handling HTTP requests while delegating resource lookup to the service. The FindResourceService improves error handling by ensuring that any ActiveRecord::RecordNotFound exceptions are caught and a custom message is raised, enhancing the user experience.
2) Ensured deleting a quiz is handled correctly in student_quizzes_controller.rb
'
This change ensures that the destroy action in the student_quizzes_controller.rb behaves correctly in all cases, including when attempting to delete a non-existent quiz.
3) Renamed and improved method names
a) Renaming assign_quiz_to_student to assign_quiz: The refactored name, assign_quiz, is simpler and still clear in the context of assigning a quiz. It follows the principle of keeping method names concise without losing meaning, making the code easier to maintain and read.
b) Renaming quiz_already_assigned? to quiz_assigned?: The renaming simplifies the method name while keeping its meaning intact. The method still performs the same check, but the new name improves code clarity and readability.
c) Renaming set_student_quiz to fetch_quiz: The new name, fetch_quiz, is more descriptive and aligns better with the method's purpose—retrieving a quiz from the database. The previous name, set_student_quiz, was less intuitive and didn’t clearly convey the action of retrieving a quiz. The name fetch_quiz also adheres to common naming conventions, making it easier to understand at a glance.
d) Renaming create_questions_and_answers to create_items_and_choices: There were discrepancies in the database. The question model was renamed to item and there were choices associated with it. The new name is more meaningful.
4) Implement self.assessment in Review Response Map
By implementing this class method self.assessments_for, the logic for retrieving all assessments for a reviewee is encapsulated, making it reusable and easier to call from other parts of the codebase.
5) Moved Score Calculation from ResponseMap to ResponseObject
The method to calculate quiz scores was previously placed in `ResponseMap`, which violates SRP and tightly couples quiz logic to mapping logic. This has now been moved to the `Response` model, which represents student answers and is the appropriate layer to handle score calculation. This also improves future support for multiple quiz attempts and better aligns with the system's domain model.
6) Added Skippable Question Logic to Quizzes
Skippable quiz questions are now supported with a `skippable` attribute added to quiz items. The system will now skip over these questions during answer validation and scoring, improving flexibility for quiz creators. Logic to process `skippable` answers was added during submission.
7) Refactored Parameters
This change improves compatibility with the updated data model and supports dynamic quiz item rendering, skippable questions, and proper nesting for answer choices.
Before:
def questionnaire_params
params.require(:questionnaire).permit(
:name,
:instructor_id,
:min_question_score,
:max_question_score,
:assignment_id,
:questionnaire_type,
:private,
questions_attributes: [
:id,
:txt,
:question_type,
:break_before,
:correct_answer,
:score_value,
{ answers_attributes: %i[id answer_text correct] }
]
)
end
After:
def questionnaire_params
params.require(:questionnaire).permit(
:name,
:instructor_id,
:min_question_score,
:max_question_score,
:questionnaire_type,
:private,
items_attributes: [
:txt,
:question_type,
:break_before,
:weight,
:skippable,
:seq,
{ quiz_question_choices_attributes: %i[txt iscorrect] }
]
)
end
Migration files
- Added skippable to facilitate the skippable function to item table
- Changed the column_name to item in quiz_choices as the current table name is item, not questions.
Implementation Details
1) Refactoring and Moving Business Logic
The following changes were made to improve maintainability and follow MVC principles:
- Refactored the create_score method
- Refactored the process_answers method
- Moved findResource logic to a separate file
- Implemented self.assessments_for in ReviewResponseMap
- Renamed methods in student_quizzes_controller.rb for clarity
2) Error Fixes
3) RSpec Testing
RSpec tests were written for all major controller actions, model logic (including skippable question scoring), and error handling paths. This ensures the correctness of the refactored implementation and guards against regressions.
Test Plan
We added three test cases in the file: spec/requests/api/v1/student_quizzes_controller_spec.rb
Pre-Test Setup
- Hierarchical roles are created using create_roles_hierarchy :
@roles = create_roles_hierarchy
- Users with appropriate roles are instantiated for test scenarios.
JWT is used in tests to pretend a real user is logged in so we can check if only the right people can access certain API actions. It helps test authentication and authorization properly.
let(:token) { JsonWebToken.encode({ id: instructor.id }) }
let(:auth_headers) { { "Authorization" => "Bearer #{token}", "Content-Type" => "application/json" } }
Testing
After setting up authentication and creating test users and roles, we implemented request-level and unit-level RSpec tests to validate the functionality of each action and refactored methods in `StudentQuizzesController` and associated models.
Below is an example of testing the #index
action:
Purpose: Retrieve a list of all quizzes. Test: Validates that the endpoint returns a 200 OK status and a JSON array of quizzes.
describe '#index' do
it "returns a list of quizzes" do
Questionnaire.create!(
name: "Quiz One", instructor_id: instructor.id,
min_question_score: 0, max_question_score: 10
)
get '/api/v1/student_quizzes', headers: auth_headers
expect(response).to have_http_status(:ok)
quizzes = JSON.parse(response.body)
expect(quizzes).to be_an(Array)
end
end
In addition to the above, we tested quiz creation with both skippable and non-skippable items:
describe '#create' do
it 'creates a questionnaire with skippable items and choices' do
post api_v1_student_quizzes_path,
params: skipable_valid_attrs.to_json,
headers: auth_headers
expect(response).to have_http_status(:created)
end
it 'creates a questionnaire with non-skippable items and choices' do
post api_v1_student_quizzes_path,
params: non_skipable_valid_attrs.to_json,
headers: auth_headers
expect(response).to have_http_status(:created)
end
end
We also tested quiz update functionality:
describe '#update' do
it 'updates a questionnaire' do
put "/api/v1/student_quizzes/#{quiz.id}",
params: { questionnaire: { name: 'Updated Questionnaire' } }.to_json,
headers: auth_headers
expect(response).to have_http_status(:ok)
expect(JSON.parse(response.body)['name']).to eq('Updated Questionnaire')
end
end
After adding the tests, our test overage increased by 6% from 68% to around 74%.
Conclusion: The RSpec suite thoroughly validates controller functionality, model-level scoring, and key edge cases like skippable questions and zero scoring boundaries. These tests ensure the robustness of the refactored controller and the correctness of all new features introduced.
Github Pull Request
https://github.com/expertiza/reimplementation-back-end/pull/180
Video
Mentor
- Anish Toorpu (atoorpu@ncsu.edu)
Team Members
- Kavya Lalbahadur Joshi (kjoshi4@ncsu.edu)
- Vedant Patel (vpatel32@ncsu.edu)
- Uchswas Paul (upaul@ncsu.edu)