CSC/ECE 517 Spring 2024 - E2420. Reimplement student quizzes controller
Expertiza
Expertiza is an open-source project built using Ruby on Rails. It provides a platform for instructors and students to manage, submit, and evaluate assignments and projects. Instructors can create, edit, and grade assignments, while students can collaborate in teams, submit their work, and review peers' submissions. Expertiza supports submission across various document types, including URLs and wiki pages.
Issues with previous Functionality
- DRY principles: The previous code in the StudentQuizzesController lacked adherence to the DRY (Don't Repeat Yourself) principle, with evident repetition in JSON rendering and ActiveRecord object fetching across multiple actions. This redundancy not only hampers maintainability but also increases the risk of inconsistencies and errors during future updates, highlighting the need for a more streamlined and reusable code structure.
- HTTP Status Codes: While some actions correctly return HTTP status codes (like: unprocessable_entity for validation errors), it's essential to ensure consistency across all actions. For example, the calculate_score action should explicitly return a 200 OK status upon success for clarity, even though it's the default.
- Error Handling: Some actions like destroy handle exceptions (ActiveRecord::RecordNotFound), but this consistent error handling approach is not applied across all actions where it might be relevant (e.g., show, update).
- Parameter Handling: The method questionnaire_params is well-structured for strong parameter handling, but there's inconsistency in how parameters are managed across actions. For example, response_map_params is defined but not used. Ensuring consistency and security in parameter handling is crucial.
- Conditional Logic for Rendering: There's a mix of rendering JSON directly in the actions and conditional logic for handling different outcomes (e.g., in assign_quiz_to_student). This could be streamlined for readability and maintainability.
Design Goals
This project builds on E2376, and the main goal is to get the endpoints of StudentQuizzesController running in reimplementation-backend repository. Detailed goals of the project are as follows:
- Preserve and enhance the functionality of the existing codebase, ensuring all features operate correctly and any identified issues are resolved.
- Eliminate redundant code by leveraging existing functionalities within the Expertiza system, adhering to the DRY (Don't Repeat Yourself) principle.
- Maintain the integrity of existing test cases and expand the test suite to include comprehensive tests for new and modified functionalities, ensuring thorough coverage and reliability.
- Improve error handling and response consistency across all endpoints, ensuring clear and informative feedback is provided for both successful operations and error conditions.
- Document all aspects of the implementation clearly, including inline code comments and external documentation, to facilitate ease of understanding and future development efforts.
Implementation
Student quizzes Controller contains APIs which allows the instructor to create a new quiz and perform other CRUD operations on quiz.It allows the students to submit answers to the quizzes and check their scores. Our implementation plan includes reimplementing existing methods and adding new methods.
Reimplemented Methods
Each section below covers methods of the student_quiizes_controller.rb model, how they were reimplemented, and why it was done that way.
“calculate_score” method Commit
Fig 4.1: Comparison of original and re-implemented calculate_score
method
The reimplementation of the calculate_score method in the controller maintains its original functionality, focusing on retrieving and returning a score based on a given ResponseMap ID, or providing an error message if the ResponseMap is not found. The notable changes in the new version include the introduction of render_success and render_error helper methods, which serve to encapsulate the rendering of JSON responses, thereby enhancing code readability and maintainability. These modifications do not alter the method's core logic but provide a clearer, more consistent approach to handling successful outcomes and errors, ensuring a standardized response format across the controller. This approach reflects a modern Ruby coding practice, focusing on cleaner code without compromising the method's purpose or efficiency.
“create_questionnaire” method Commit
Fig 4.2: Comparison of original and re-implemented create_questionnaire
method
Significant refinements were implemented to enhance the code's organization, readability, and error handling. Initially, the old method encapsulated the creation of a questionnaire, its questions, and associated answers within a single transaction block, directly interacting with the database models and handling exceptions in a generic manner. This approach, while functional, resulted in a more verbose method with nested iterations and direct creation calls within the transaction block.
The new create method introduces a more structured approach by segmenting the questionnaire creation and the addition of questions and answers into distinct operations. This separation is achieved through the create_questionnaire and create_questions_and_answers helper functions, which not only streamline the code but also improve clarity by breaking down the process into more manageable, focused steps. The use of these helper functions within the transaction ensures that the entire operation remains atomic, adhering to the principle that all parts of the questionnaire creation should either complete successfully or fail together, thus maintaining data integrity.
Furthermore, the new method employs a more sophisticated error handling strategy, explicitly rescuing StandardError and utilizing a render_error helper method to provide a consistent response format across different actions in the controller. This approach not only standardizes error responses but also enhances the method's robustness by clearly defining the scope of errors it can handle.
The shift to using helper methods like render_success and render_error for rendering responses aligns with modern Ruby practices, focusing on code modularity and maintainability. By encapsulating the success and error rendering logic, the new method offers a cleaner, more consistent interface for handling the various outcomes of the create operation, facilitating easier future modifications and improving the overall code quality.
“assign_quiz_to_student” method Commit
Fig 4.3: Comparison of original and re-implemented assign_quiz_to_student
method
In the reimplementation of the assign_quiz_to_student method, several changes were made.Firstly, the new version introduces helper methods find_resource_by_id for fetching the participant and questionnaire, and quiz_already_assigned? to check if the quiz has already been assigned to the student. These abstractions not only make the method more readable but also encapsulate specific functionalities, making the code more reusable and maintainable.
The condition checking for an existing assignment is now encapsulated within the quiz_already_assigned? method, simplifying the main method's logic flow and focusing it on the assignment process rather than validation checks.Another notable change is the introduction of build_response_map, a method presumably responsible for constructing a new ResponseMap instance. This further modularizes the code, separating the concerns of validation and object creation.
Error handling has been standardized using render_error and render_success helper methods, consistent with modern practices of handling HTTP responses. This not only makes the code cleaner but also ensures that the method's responses are consistent with other parts of the application.The use of return unless participant && questionnaire early in the method is a Ruby idiom that helps to avoid nested conditional statements, thereby enhancing the readability and flow of the method.
“submit_answers” method Commit
Fig 4.4: Comparison of original and re-implemented submit_answers
method
In the reimplementation of the submit_answers method, several refinements were made to enhance code modularity, readability, and adherence to the DRY (Don't Repeat Yourself) principle, which the old version somewhat lacked, particularly in handling responses and scoring logic.The new method employs helper methods like find_response_map_for_current_user to fetch the relevant ResponseMap for the current user and process_answers to handle the logic of processing submitted answers and calculating the total score. This approach not only makes the code cleaner and more readable by breaking down the process into smaller, more manageable chunks but also improves maintainability by isolating specific functionalities.
In the original version, the logic for finding the ResponseMap, processing each answer, and calculating the total score was all embedded within the main submit_answers method, making it more cumbersome and less readable. This approach also violated the DRY principle, as similar patterns of fetching and processing could potentially be used in other parts of the application but would need to be rewritten.
The restructured method enhances error handling by utilizing render_error for consistent error messaging, similar to other methods in the controller, thereby standardizing response formats across different actions. This is a shift from the old method, where JSON error responses were constructed inline, leading to potential inconsistencies in error handling across the application.
By abstracting repetitive logic into helper methods, the new version adheres more closely to the DRY principle, reducing redundancy and the likelihood of errors during future modifications. The encapsulation of specific tasks into separate methods not only clarifies the main method's purpose—submitting answers and calculating scores—but also allows for easier testing and debugging of individual components.
Overall, the changes made in the new version of the submit_answers method reflect a thoughtful refactoring effort aimed at improving code quality, maintainability, and adherence to best practices in Ruby on Rails development
New and enhanced methods
The old code had issues with redundancy, lengthy methods, and a lack of clear separation between different pieces of logic. The introduction of these new helper methods addresses these concerns.Here's an overview of the new methods and the rationale behind their implementation:
# | New method | Purpose | Rationale | Commit |
---|---|---|---|---|
1 | find_resource_by_id
|
To find a specific resource (like Participant or Questionnaire) by its ID, and handle the case where the resource is not found by rendering an error message and returning nil. | This method streamlines the process of fetching resources and centralizes error handling for 'not found' scenarios.In the old code, resource fetching and error handling were done inline within each action, which could lead to repeated code and inconsistencies in handling 'not found' errors. | Commit |
2 | quiz_already_assigned?
|
To check if a quiz (questionnaire) has already been assigned to a participant, based on the existence of a ResponseMap. | Encapsulating this check into its own method enhances readability and reusability. Previously, this check was performed directly within the assign_quiz_to_student method, mixing business logic with action logic and making the method longer and less readable. | Commit |
3 | build_response_map
|
To create a new ResponseMap instance for assigning a quiz to a student, encapsulating the logic for setting up reviewee_id, reviewer_id, and reviewed_object_id. | This method abstracts away the details of ResponseMap creation, making the assign_quiz_to_student method simpler and focusing it on higher-level logic. In the old code, the ResponseMap was constructed inline within the action, cluttering the method with initialization details. | Commit |
4 | find_or_initialize_response
|
To find an existing Response based on response_map_id and question_id or initialize a new one if it doesn't exist. | This method DRYs up the code by centralizing the logic for handling Response objects, which is useful in scenarios where responses need to be either retrieved or initialized. In the old code, this was done inline within the submit_answers method, which added complexity. | Commit |
5 | process_answers
|
To process submitted answers, calculate the total score, and handle the initialization and updating of Response objects. | Abstracting this logic into a separate method significantly reduces the complexity of the submit_answers action, making the code easier to read and maintain. The old method contained a dense block of logic for processing answers, which was more difficult to understand and test. | Commit |
Other changes
The POST /api/v1/student_quizzes endpoint is designed for quiz creation within our system. Initially, the method lacked explicit response codes for key operations such as creation, updates, and handling of unprocessable entities. To enhance clarity and provide more detailed feedback for API consumers, we have introduced the following improvements in HTTP response code handling:
Successful Creation: Upon successful quiz creation, the endpoint now returns a 201 Created status code. This response signifies that the quiz has been successfully created in the system. Additionally, the response header includes a Location field, pointing to the URL of the newly created quiz resource.
Update Acknowledgment: For update operations that are successfully processed, the endpoint responds with a 204 No Content status code. This indicates that the request was successful, and the updated resource does not need to be returned in the response body.
Handling Unprocessable Entities: In scenarios where the request cannot be processed due to semantic errors (e.g., validation failures), the endpoint now returns a 422 Unprocessable Entity status code. This improvement helps in distinguishing between bad request formats (400 Bad Request) and requests that are syntactically correct but semantically erroneous.
These enhancements aim to provide more granular feedback to API consumers, enabling better error handling and integration experiences.
Testing Plan
We employ a combination of manual testing with tools like Postman, automated testing through RSpec, and API documentation and testing using Swagger. This comprehensive approach allows us to validate our API's functionality, security, and usability across various scenarios.
Manual testing with Postman
We are using Postman to test our APIs during the development process. Below are few examples:
- POST /api/v1/students_quizzes: Endpoint to test the creation of a Quiz.
Fig 5.1.1 Postman UI displaying API testing to create student quizzes.
- POST /api/v1/students_quizzes/submit_answers: Endpoint to test the submission of answers of a Quiz by a student.
Fig 5.1.2 Postman UI displaying API testing to submission of answers of a student quiz.
- GET api/v1/student_quizzes/2: Endpoint to test the functionality of fetching a quiz using its ID.
Fig 5.1.3 Postman UI displaying API testing to fetch a quiz using its ID.
- DELETE api/v1/student_quizzes/2: Endpoint to test the functionality of deleting a quiz with quiz id.
Fig 5.1.4 Postman UI displaying API testing to delete a quiz using its ID.
- UPDATE api/v1/student_quizzes/1: Endpoint to test the functionality of updating the quiz.
Fig 5.1.5 Postman UI displaying API testing to update a quiz using its ID.
Automated Testing with Rspec
Scenario 1: Creating a New Student Quiz
Validates the creation of a new Student Quiz with appropriate parameters.
Expectation: Expects a 201 Created status and a JSON response with the created quiz details.
describe 'POST /api/v1/student_quizzes' do context 'with valid parameters' do it 'creates a new Student Quiz' do expect { post '/api/v1/student_quizzes', params: valid_attributes }.to change(Questionnaire, :count).by(1) expect(response).to have_http_status(:created) end end
Scenario 2: Handling Invalid Student Quiz Creation
Tests the creation process with invalid parameters to ensure robust error handling.
Expectation: Expects a 400 Bad Request status and an error message detailing the invalid parameters.
context 'with invalid parameters' do it 'does not create a new Student Quiz' do expect { post '/api/v1/student_quizzes', params: invalid_attributes }.to change(Questionnaire, :count).by(0) expect(response).to have_http_status(:bad_request) end end end
Scenario 3: Calculating Quiz Score
Ensures accurate calculation of a quiz score given valid input.
Expectation: Expects a 200 OK status and a JSON response with the correct score calculation.
describe 'GET /api/v1/student_quizzes/{quiz_id}/calculate_score' do it 'calculates score for a given quiz' do get "/api/v1/student_quizzes/#{questionnaire_id}/calculate_score" expect(response).to have_http_status(:ok) # Additional expectations for score calculation end end
Scenario 4: Assigning Quiz to Student
Validates the functionality for assigning a quiz to a student, ensuring no duplicates.
Expectation: Expects a 201 Created status indicating successful assignment.
describe 'POST /api/v1/student_quizzes/assign' do let(:assign_attributes) { { participant_id: student.id, questionnaire_id: questionnaire_id } } it 'assigns a quiz to a student' do post '/api/v1/student_quizzes/assign', params: assign_attributes expect(response).to have_http_status(:created) end end
Scenario 5: Submitting Quiz Answers
Tests the submission of answers for a quiz and the subsequent score calculation.
Expectation: Expects a 200 OK status and a JSON response with the total score.
describe 'POST /api/v1/student_quizzes/submit_answers' do let(:submit_attributes) { { questionnaire_id: questionnaire_id, answers: [{ question_id: create(:question, questionnaire: questionnaire).id, answer_value: 'Programming Language' }] } } it 'submits answers and calculates the total score' do post '/api/v1/student_quizzes/submit_answers', params: submit_attributes expect(response).to have_http_status(:ok) # Additional checks for score calculation end end
Scenario 6: Updating a Student Quiz
Ensures that an existing quiz can be updated with valid parameters.
Expectation: Expects a 200 OK status and the updated quiz details in the response.
describe 'PUT /api/v1/student_quizzes/{id}' do context 'when the record exists' do before { put "/api/v1/student_quizzes/#{questionnaire_id}", params: valid_attributes_update } it 'updates the record' do expect(response).to have_http_status(:ok) updated_quiz = Questionnaire.find(questionnaire_id) expect(updated_quiz.name).to match(/Updated Quiz Name/) end end end
Scenario 7: Updating a Student Quiz
Checks the behavior when attempting to update a quiz with invalid parameters.
Expectation: Expects a 422 Unprocessable Entity status and an error message detailing the issue.
context 'with invalid parameters' do before { put "/api/v1/student_quizzes/#{questionnaire_id}", params: invalid_attributes } it 'returns status code 422' do expect(response).to have_http_status(:unprocessable_entity) end end
API Documentation and Testing with Swagger
To provide clear, interactive API documentation, facilitating easy understanding and testing of API endpoints.
Setup: Integrate Swagger with Rails application using gems like rswag.
Documentation: Annotate the API code to generate Swagger documentation automatically. Ensure that each endpoint is clearly described, with information on parameters, request bodies, and response schemas. Interactive Testing: Use the Swagger UI to interactively test API endpoints directly from the browser, providing an easy way for non-developers to understand and verify API functionality.
Fig 5.3 Swagger UI displaying API documentation and interactive testing interface for student quizzes endpoints.
Relevant Links
- Github Repository: https://github.com/prathyu99/reimplementation-back-end
- Pull Request: https://github.com/expertiza/reimplementation-back-end/pull/87
- Test Video Link: https://youtu.be/dAJGMJMJ5kQ
Team
Mentor: Chetana Chunduru - cchetan2@ncsu.edu
- Prathyusha Kodali - pkodali@ncsu.edu
- Soubhagya Akkena - sakkena@ncsu.edu
- Sri Lakshmi Kotha - skotha2@ncsu.edu