CSC/ECE 517 Spring 2023 - E2345. Reimplement QuestionnairesController and QuestionsController: Difference between revisions

From Expertiza_Wiki
Jump to navigation Jump to search
No edit summary
No edit summary
 
(48 intermediate revisions by 4 users not shown)
Line 16: Line 16:
* Ensure all present test cases still work properly after making the above changes and create additional test cases.
* Ensure all present test cases still work properly after making the above changes and create additional test cases.


==Current Implementation==
==Implementation==
Plan for current implementation for the code can be found here [https://expertiza.csc.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2023_-_E2321._Reimplement_QuestionnairesController_and_QuestionsController E2321].
The current implementation for the code can be found here [https://expertiza.csc.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2023_-_E2321._Reimplement_QuestionnairesController_and_QuestionsController E2321].<br>
To get the QuestionnairesController and QuestionsController endpoints running in the reimplementation-back-end repository, we need to reimplement the respective model files and add other dependent files.<br>


After seperation of the  responsibilities of the QuestionnairesController and QuestionsController, fixing bugs, discarding unused functionality, we reimplemented the code to create basic CRUD operations. Then the methods were converted to basic CRUD operations which are described below. (Information related to separating responsibilities, bugs fixed, current testing and unused functionality can be found on the above link for plan of current implementation.)
The following UML diagram shows the association between models we will be working on -
*'''Reimplement CRUD functionality and create JSON endpoints in QuestionsController:''' We reimplemented the following methods to create the required endpoints in [https://github.com/expertiza/reimplementation-back-end/blob/0b09c15466e5d9865663ed2253d9f37a2e1ca1d9/app/controllers/api/v1/questions_controller.rb questions_controller.rb].
#index: This is a GET endpoint that responds with a list of all questions with their attributes.
#show: This is a GET endpoint that accepts a question id and responds with the attributes of that question if it exists.
#create: This is a POST endpoint that accepts the question parameters and creates a new question.
#destroy: This is a DELETE endpoint that accepts a question ID and deletes that question.
#update: This is a PUT endpoint that accepts a question ID and parameters, and, updates that question with the given parameters.
#types: This is a GET endpoint that responds with the list of question types.


*'''Reimplement CRUD functionality and create JSON endpoints in QuestionnairesController:''' We reimplemented the following methods to create the required endpoints in [https://github.com/expertiza/reimplementation-back-end/blob/0b09c15466e5d9865663ed2253d9f37a2e1ca1d9/app/controllers/api/v1/questionnaires_controller.rb questionnaires_controller.rb].
[[File:oodd_uml.png|border|800px|]]  
#index: This is a GET endpoint that responds with a list of all questionnaires with their attributes.
#show: This is a GET endpoint that accepts a questionnaire id and responds with the attributes of that questionnaire if it exists.
#create: This is a POST endpoint that accepts the questionnaire parameters and creates a new questionnaire.
#destroy: This is a DELETE endpoint that accepts a questionnaire ID and deletes that questionnaire.
#update: This is a PUT endpoint that accepts a questionnaire ID and parameters, and, updates that questionnaire with the given parameters.
#copy: This is a POST endpoint that accepts a questionnaire ID and creates a new questionnaire (copy) with the same attributes of that questionnaire.
#toggle_access: This is a GET endpoint that changes the access from public to private and vice versa.


The current implementation of the Controllers include only manual testing via Postman. In E2345, we plan to add automated RSpec tests using the Swagger API to test each endpoint.
Below is the implementation plan that we have followed for E2345:
 
==Implementation==
We did not have models for Questionnaires and Questions, along with other primary models on which the Questionnaire and Question model depend on in the reimplementation-back-end repository, so we ran it on expertiza repository. To get the questions and questionnaires module running in the reimplementation-back-end repository, we plan to follow these steps:<br>
<ol>
<ol>
<li>
<li>
Reimplement basic version of ''question.rb'' and ''questionnaire.rb'' such that it facilitates the controllers' CRUD operations.<br>
'''Implement basic version of ''question.rb'' and ''questionnaire.rb'' such that it facilitates the controllers' CRUD operations.'''<br>
:*Add required associations for ''user.rb, role.rb, question.rb, and questionnaire.rb'' only.
:*Add required associations for ''user.rb, role.rb, question.rb, and questionnaire.rb'' only.
:*The below code snippets represent current implementation of ''question.rb'':
:*The below code snippets represent current implementation of ''question.rb'':
Line 129: Line 113:
</pre>
</pre>
In the above code,  
In the above code,  
::*The as_json method is used to decorate the JSON object that is returned as a part of the response body from the controller endpoint. It is used to replace the questionnaire_id with the questionnaire details within the question object in JSON format.
::*The as_json method is used to decorate the JSON object that is returned as a part of the response body from the controller endpoint. It is used to replace the instructor_id with the instructor details within the questionnaire object in JSON format.
::*The check_for_question_associations method is used to raise an Error if a Questionnaire that has associated questions is tried to be deleted.  
::*The check_for_question_associations method is used to raise an Error if a Questionnaire that has associated questions is tried to be deleted.  
::*The copy_questionnaire_details method is used to create a new object with the same attributes of the questionnaire and save it to the database. It is called when the copy endpoint is requested.
::*The copy_questionnaire_details method is used to create a new object with the same attributes of the questionnaire and save it to the database. It is called when the copy endpoint is requested.
</li>
</li>
<li>
<li>
Reimplement ''questions_controller.rb'' and ''questionnaires_controller.rb'' to incorporate the feedback received in E2321. The feedback is as follows:<br>
'''Reimplement ''questionnaires_controller.rb'' to incorporate the feedback received in E2321.'''<br>
:*Refactor the Create endpoint in ''questionnaires_controller.rb'' such that individual fields does not need to be set and validated. The below code represents the revised implementation of the method. A Questionnaire object is created using the new method instead of manual assignment of attributes.
:*index: This is a GET endpoint that responds with a list of all questionnaires with their attributes.
<pre>
def index
    @questionnaires = Questionnaire.order(:id)
    render json: @questionnaires, status: :ok and return
end
</pre>
:*show: This is a GET endpoint that accepts a questionnaire id and responds with the attributes of that questionnaire if it exists, otherwise provides with appropriate error.
<pre>
def show
    begin
      @questionnaire = Questionnaire.find(params[:id])
      render json: @questionnaire, status: :ok and return
    rescue ActiveRecord::RecordNotFound
      render json: $ERROR_INFO.to_s, status: :not_found and return
    end
end
</pre>
:*create: This is a POST endpoint that accepts the questionnaire parameters and creates a new questionnaire. Refactor the Create endpoint in ''questionnaires_controller.rb'' such that individual fields does not need to be set and validated. The below code represents the revised implementation of the method. A Questionnaire object is created using the new method instead of manual assignment of attributes.
<pre>
<pre>
def create
def create
Line 146: Line 148:
       render json: $ERROR_INFO.to_s, status: :unprocessable_entity
       render json: $ERROR_INFO.to_s, status: :unprocessable_entity
     end
     end
  end
end
</pre>
:*destroy: This is a DELETE endpoint that accepts a questionnaire ID and deletes that questionnaire if it exists, otherwise provides with appropriate error. It should delete the questionnaire only if there are no questions associated to it.
<pre>
def destroy
    begin
      @questionnaire = Questionnaire.find(params[:id])
      @questionnaire.delete
    rescue ActiveRecord::RecordNotFound
      render json: $ERROR_INFO.to_s, status: :not_found and return
    rescue ActiveRecord::RecordInvalid
      render json: $ERROR_INFO.to_s, status: :unprocessable_entity
    end
end
</pre>
:*update: This is a PUT endpoint that accepts a questionnaire ID and parameters, and, updates that questionnaire with the given parameters if it exists, otherwise provides with appropriate error.
<pre>
def update
    # Save questionnaire information
    begin
      @questionnaire = Questionnaire.find(params[:id])
      @questionnaire.update(questionnaire_params)
      @questionnaire.save!
      render json: @questionnaire, status: :ok and return
    rescue ActiveRecord::RecordNotFound
      render json: $ERROR_INFO.to_s, status: :not_found and return
    rescue ActiveRecord::RecordInvalid
      render json: $ERROR_INFO.to_s, status: :unprocessable_entity
    end
end
</pre>
:*copy: This is a POST endpoint that accepts a questionnaire ID and creates a new questionnaire (copy) with the same attributes of that questionnaire if it exists, otherwise provides with appropriate error.
<pre>
def copy
    begin
      @questionnaire = Questionnaire.copy_questionnaire_details(params)
      render json: "Copy of the questionnaire has been created successfully.", status: :ok and return
    rescue ActiveRecord::RecordNotFound
      render json: $ERROR_INFO.to_s, status: :not_found and return
    rescue ActiveRecord::RecordInvalid
      render json: $ERROR_INFO.to_s, status: :unprocessable_entity
    end
end
</pre>
:*toggle_access: This is a GET endpoint that changes the access from public to private and vice versa if it exists, otherwise provides with appropriate error.
<pre>
def toggle_access
    begin
      @questionnaire = Questionnaire.find(params[:id])
      @questionnaire.private = !@questionnaire.private
      @questionnaire.save!
      @access = @questionnaire.private == true ? 'private' : 'public'
      render json: "The questionnaire \"#{@questionnaire.name}\" has been successfully made #{@access}. ", status: :ok and return
    rescue ActiveRecord::RecordNotFound
      render json: $ERROR_INFO.to_s, status: :not_found and return
    rescue ActiveRecord::RecordInvalid
      render json: $ERROR_INFO.to_s, status: :unprocessable_entity
    end
end
</pre>
</li>
<li>
'''Reimplement ''questions_controller.rb'' to incorporate the feedback received in E2321.'''<br>
:*index: This is a GET endpoint that responds with a list of all questions with their attributes.
<pre>
def index
    @questions = Question.order(:id)
    render json: @questions, status: :ok
end
</pre>
:*show: This is a GET endpoint that accepts a question id and responds with the attributes of that question if it exists, otherwise provides with appropriate error.
<pre>
def show
    begin
      @question = Question.find(params[:id])
      render json: @question, status: :ok
    rescue ActiveRecord::RecordNotFound
      render json: $ERROR_INFO.to_s, status: :not_found
    end
end
</pre>
</pre>
:*Refactor the Create endpoint in ''questions_controller.rb'' such that individual fields does not need to be set and validated. The below code represents the revised implementation of the method. A Question object is created using the new method instead of manual assignment of attributes.
:*create: This is a POST endpoint that accepts the question parameters and creates a new question. Refactor the Create endpoint in questions_controller.rb such that individual fields does not need to be set and validated. The below code represents the revised implementation of the method. A Question object is created using the new method instead of manual assignment of attributes.
<pre>
<pre>
def create
def create
Line 160: Line 241:
         question_type: params[:question_type],
         question_type: params[:question_type],
         break_before: true)
         break_before: true)
      # question = Object.const_get(params[:question][:type])
.create(txt: '', questionnaire_id: questionnaire_id, seq: num_of_existed_questions + 1, question_type:params[:question][:type], break_before: true)
       case question.question_type
       case question.question_type
         when 'Scale'
         when 'Scale'
Line 185: Line 264:
     rescue ActiveRecord::RecordNotFound
     rescue ActiveRecord::RecordNotFound
       render json: $ERROR_INFO.to_s, status: :not_found and return   
       render json: $ERROR_INFO.to_s, status: :not_found and return   
    rescue ActiveRecord::RecordInvalid
      render json: $ERROR_INFO.to_s, status: :unprocessable_entity
    end
end
</pre>
:*destroy: This is a DELETE endpoint that accepts a question ID and deletes that question if it exists, otherwise provides with appropriate error.
<pre>
def destroy
    begin
      @question = Question.find(params[:id])
      @question.destroy
    rescue ActiveRecord::RecordNotFound
      render json: $ERROR_INFO.to_s, status: :not_found and return
    rescue ActiveRecord::RecordInvalid
      render json: $ERROR_INFO.to_s, status: :unprocessable_entity
    end
end
</pre>
:*delete_all: This is a DELETE endpoint that takes questionnaire_id as the path parameter and deletes all questions associated to that questionnaire. It returns status 404 if the questionnaire does not exist, status 422 if no questions are associated to that questionnaire and status 200 if all the questions are successfully deleted.
<pre>
def delete_all
    begin
      @questionnaire = Questionnaire.find(params[:id])
      if @questionnaire.questions.size > 0
        @questionnaire.questions.destroy_all
        msg = "All questions for Questionnaire ID:" + params[:id].to_s + " has been successfully deleted!"
        render json: msg, status: :ok
      else
        render json: "No questions associated to Questionnaire ID:" + params[:id].to_s, status: :unprocessable_entity and return
      end
    rescue ActiveRecord::RecordNotFound
      render json: $ERROR_INFO.to_s, status: :not_found and return
    rescue ActiveRecord::RecordInvalid
      render json: $ERROR_INFO.to_s, status: :unprocessable_entity
    end
end
</pre>
:*shiw_all: This is a GET endpoint that takes questionnaire_id as the path parameter and shows all questions associated to that questionnaire. It returns status 404 if the questionnaire does not exist, status 422 if no questions are associated to that questionnaire and status 200 if all the questions are successfully displayed.
<pre>
  def show_all
    begin
      @questionnaire = Questionnaire.find(params[:id])
      @questions = @questionnaire.questions
      render json: @questions, status: :ok
    rescue ActiveRecord::RecordNotFound
      render json: $ERROR_INFO.to_s, status: :not_found
     rescue ActiveRecord::RecordInvalid
     rescue ActiveRecord::RecordInvalid
       render json: $ERROR_INFO.to_s, status: :unprocessable_entity
       render json: $ERROR_INFO.to_s, status: :unprocessable_entity
     end
     end
   end
   end
</pre>
:*update: This is a PUT endpoint that accepts a question ID and parameters, and, updates that question with the given parameters if it exists, otherwise provides with appropriate error.
<pre>
def update
    begin
      @question = Question.find(params[:id])
      @question.update(question_params)
      @question.save!
      render json: @question, status: :ok and return
    rescue ActiveRecord::RecordNotFound
      render json: $ERROR_INFO.to_s, status: :not_found and return
    rescue ActiveRecord::RecordInvalid
      render json: $ERROR_INFO.to_s, status: :unprocessable_entity
    end
end
</pre>
:*types: This is a GET endpoint that responds with the list of question types.
<pre>
def types
    types = Question.distinct.pluck(:question_type)
    render json: types.to_a, status: :ok
end
</pre>
</pre>
</li>
<li>
'''Write unit tests for Questions model and Questionnaires model using Rspec:''' Kindly refer to the Testing Methodology section for detailed description.
:*Unit tests for Question model: [https://github.com/Vineet2311/reimplementation-back-end/blob/main/spec/models/question_spec.rb question_spec.rb]
:*Unit tests for Questionnaire model: [https://github.com/Vineet2311/reimplementation-back-end/blob/main/spec/models/questionnaire_spec.rb questionnaire_spec.rb]
</li>
<li>
'''Write Rspec tests using Swagger API for each endpoint of QuestionsController and QuestionnairesController.''' Kindly refer to the Testing Methodology section for detailed description.
:*Swagger tests for QuestionsController: [https://github.com/Vineet2311/reimplementation-back-end/blob/main/spec/requests/api/v1/questions_spec.rb questions_spec.rb]
:*Swagger tests for QuestionnairesController: [https://github.com/Vineet2311/reimplementation-back-end/blob/main/spec/requests/api/v1/questionnaires_spec.rb questionnaires_spec.rb]
</li>
</li>
</ol>
</ol>
Line 195: Line 353:
==Testing Methodology==
==Testing Methodology==


* Rspec tests for QuestionnairesController and QuestionsController had been added in the previous project [https://expertiza.csc.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2023_-_E2321._Reimplement_QuestionnairesController_and_QuestionsController E2321] but had not been executed due to missing dependencies. This project focuses on implementing them and getting automated test cases running as expected.
Rspec test skeleton for QuestionnairesController and QuestionsController had been added in the previous project [https://expertiza.csc.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2023_-_E2321._Reimplement_QuestionnairesController_and_QuestionsController E2321] but had not been executed due to missing dependencies. This project focuses on implementing them and getting automated test cases running as expected.
In our previous project we implemented the CRUD endpoints but since all the models were not present in reimplementation backend repository, we could not test our controller tests. In this project, we focus to create the models required to implement CRUD operations in the reimplementation backend repository and then run and test the API tests for the CRUD operations.
In our previous project we implemented the CRUD endpoints but since all the models were not present in reimplementation backend repository, we could only do manual testing of our controller endpoints using Postman. In this project, we focus to create the models required to implement CRUD operations in the reimplementation-back-end repository and test the endpoints using Swagger API.


====Controller Tests====
====Controller Tests====
Line 202: Line 360:
To run the controller tests:
To run the controller tests:


1. git clone  
#git clone  
 
#cd reimplementation-back-end/
2. cd reimplementation-back-end/
#bundle install
 
#rspec spec/requests/api/v1/questionnaires_controller_spec.rb
3. bundle install
#rspec spec/requests/api/v1/questions_controller_spec.rb
#RAILS_ENV=test rails rswag


4. bundle exec rspec spec/requests/api/v1/questionnaires_controller_spec.rb
'''Tests for QuestionnairesController:''' [https://github.com/Vineet2311/reimplementation-back-end/blob/main/spec/requests/api/v1/questionnaires_spec.rb questionnaires_spec.rb]


{| class="wikitable" style="margin-left:30px"
{| class="wikitable" style="margin-left:30px"
Line 218: Line 377:
| 2 || Test to create a new Questionnaire with invalid parameters.
| 2 || Test to create a new Questionnaire with invalid parameters.
|-
|-
| 3 || Test to create a copy of Questionnaire.
| 3 || Test to create a copy of Questionnaire with existing id in database.
|-
| 4 || Test to create a copy of Questionnaire with non-existing id in database.
|-
| 5 || Test to list the Questionnaires.
|-
|-
| 4 || Test to list the Questionnaires.
| 6 || Test to update a Questionnaire (using put request) with correct parameters.
|-
|-
| 5 || Test to update the Questionnaire with correct parameters.
| 7 || Test to update a Questionnaire (using put request) with incorrect parameters.
|-
|-
| 6 || Test to update the Questionnaire with incorrect parameters.
| 8 || Test to update a Questionnaire (using put request) with non-existing id in database.
|-
|-
| 7 || Test to delete Questionnaire with existing id.
| 9 || Test to delete Questionnaire with existing id in database.
|-
|-
| 8 || Test to delete Questionnaire with the non-existing id.
| 10 || Test to delete Questionnaire with the non-existing id in database.
|-
|-
| 9 || Test to show a Questionnaire with a valid id.
| 11 || Test to show a Questionnaire with a existing id in database.
|-
|-
| 10 || Test to show a Questionnaire with an invalid id.
| 12 || Test to show a Questionnaire with non-existing id in database.
|-
|-
| 11 || Test to toggle private boolean flag of a Questionnaire with a valid id.
| 13 || Test to toggle private boolean flag of a Questionnaire with a existing id in database.
|-
|-
| 12 || Test to toggle private boolean flag of a Questionnaire with a invalid id.
| 14 || Test to toggle private boolean flag of a Questionnaire with non-existing id in database.
|-
|15 || Test to update the Questionnaire (using patch request) with correct parameters.
|-
|16 || Test to update the Questionnaire (using patch request) with incorrect parameters.
|-
|17 || Test to update the Questionnaire (using patch request) with non-existing id in the database.
|}
|}


bundle exec rspec spec/requests/api/v1/questions_controller_spec.rb
[[File:Questionnaire_spec_new.png|border|800px|]]  <br><br>
[[File:Questionnaire_swagger_new.png|border|800px|]]
 
'''Tests for QuestionsController:''' [https://github.com/Vineet2311/reimplementation-back-end/blob/main/spec/requests/api/v1/questions_spec.rb questions_spec.rb]


{| class="wikitable" style="margin-left:30px"
{| class="wikitable" style="margin-left:30px"
Line 249: Line 421:
| 2 || Test to create a new Question with invalid parameters.
| 2 || Test to create a new Question with invalid parameters.
|-
|-
| 3 || Test to list the Questions.
| 3 || Test to create a new Question with non-existing Questionnaire id in the database
|-
| 4 || Test to list the Questions in the database.
|-
| 5 || Test to update the Question (using put request)with correct parameters.
|-
| 6 || Test to update the Question (using put request) with incorrect parameters.
|-
| 7 || Test to update the Question (using put request)with non-existing id in the database.
|-
| 8 || Test to delete Question with existing id in the database.
|-
|-
| 4 || Test to update the Question with correct parameters.
| 9 || Test to delete Question with the non-existing id in the database.
|-
|-
| 5 || Test to update the Question with incorrect parameters.
| 10 || Test to show a Question with existing id in the database.
|-
|-
| 6 || Test to delete Question with existing id.
| 11 || Test to show a Question with non-existing id in the database.
|-
|-
| 7 || Test to delete Question with the non-existing id.
| 12|| Test to get a types of Questions present in the database.
|-
|-
| 8 || Test to show a Question with a valid id.
| 13 || Test to delete all Questions of a Questionnaire with a valid Questionnaire id.
|-
|-
| 9 || Test to show a Question with an invalid id.
| 14 || Test to delete all Questions of a Questionnaire with an invalid Questionnaire id.
|-
|-
|10|| Test to get a type of a Question with valid id.
|15 || Test to update the Question (using patch request) with correct parameters.
|-
|-
|11|| Test to get a type of a Question with invalid id.
|16 || Test to update the Question (using patch request) with incorrect parameters.
|-
|-
|12|| Test to delete all Questions of a Questionnaire with a valid id.
|17 || Test to update the Question (using patch request) with non-existing id in the database.
|-
|-
|13|| Test to delete all Questions of a Questionnaire with an invalid id.
|18 || Test to show all Questions of a Questionnaire with a valid Questionnaire id.
|-
|19 || Test to show all Questions of a Questionnaire with an invalid Questionnaire id.
|}
|}


* Rspec unit and functional tests for Questionnaire and Question model will be created. (Since, we are implementing required models for reimplementation backend repository in this project, we plan to write unit and functional tests to test the methods written in the model.)
 
[[File:Question_spec_new.png|border|800px|]] <br><br>
[[File:Question_swagger_new.png|border|800px|]]


====Model Tests====
====Model Tests====
Line 279: Line 465:
To run the model tests:
To run the model tests:


1. git clone  
#git clone  
#cd reimplementation-back-end/
#bundle install
#rspec spec/models/questionnaire_spec.rb
#rspec spec/models/question_spec.rb


2. cd reimplementation-back-end/
'''Tests for Questionnaire model:''' [https://github.com/Vineet2311/reimplementation-back-end/blob/main/spec/models/questionnaire_spec.rb questionnaire_spec.rb]
 
3. bundle install
 
4. bundle exec rspec spec/models/question_spec.rb
{| class="wikitable" style="margin-left:30px"
{| class="wikitable" style="margin-left:30px"
|-
|-
! Sr No !! Test Description  
! Sr No !! Test Description  
|-
|-
| 1 || Test that Question has valid attributes defined.
| 1 || Test that Questionnaire requires a name.
|-
| 2 || Test validates the name of the questionnaire.
|-
| 3 || Test validates the instructor id in the questionnaire .
|-
| 4 || Test validates the maximum score in the questionnaire.
|-
| 5 || Test ensures maximum score of the questionnaire is an integer.
|-
| 6 || Test ensures maximum score is positive.
|-
| 7 || Test ensures maximum score is greater than the minimum score.
|-
| 8 || Test validates minimum score of a questionnaire.
|-
|-
| 2 || Test ensures that a question is not valid without seq field.
| 9 || Test ensures minimum score is smaller than maximum score.
|-
|-
| 3 || Test that Question sequence number must be numerical.
| 10 || Test ensures minimum score is an integer.
|-
|-
| 4 || Test ensures that a question is not valid without txt field.
| 11 || Test validates the association that a questionnaire comprises of several questions.
|-
|-
| 5 || Test ensures that a question is not valid without question_type field.
| 12 || Test ensures that a questionnaire is not deleted when it has questions associated.
|-
|-
|6|| Test ensures that a question is not valid without break_before field.
| 13 || Test ensures calls from the method copy_questionnaire_details.
|-
|-
|7|| Test ensures that a question does not exist without a questionnaire.
| 14 || Test ensures creation of a copy of given questionnaire.
|-
|-
|8|| Test ensures that a question object is deleted properly taking all its association into consideration.
| 15 || Test ensures creation of copy of all the present questionnaire in the database.
|}
|}


5. bundle exec rspec spec/models/questionnaire_spec.rb
[[File:Questionnaire_model_test.jpeg|border|800px|]]


'''Tests for Question model:''' [https://github.com/Vineet2311/reimplementation-back-end/blob/main/spec/models/question_spec.rb question_spec.rb]
{| class="wikitable" style="margin-left:30px"
{| class="wikitable" style="margin-left:30px"
|-
|-
! Sr No !! Test Description  
! Sr No !! Test Description  
|-
|-
| 1 || Test that Questionnaire requires a name.
| 1 || Test that Question has valid attributes defined.
|-
|-
| 2 || Test validates the name of the questionnaire.
| 2 || Test ensures that a question is not valid without seq field.
|-
|-
| 3 || Test validates the instructor id in the questionnaire .
| 3 || Test that Question sequence number must be numerical.
|-
|-
| 4 || Test validates the maximum score in the questionnaire.
| 4 || Test ensures that a question is not valid without txt field.
|-
|-
| 5 || Test ensures maximum score of the questionnaire is an integer.
| 5 || Test ensures that a question is not valid without question_type field.
|-
|-
| 6 || Test ensures maximum score is positive.
|6|| Test ensures that a question is not valid without break_before field.
|-
|-
| 7 || Test ensures maximum score is greater than the minimum score.
|7|| Test ensures that a question does not exist without a questionnaire.
|-
|-
| 8 || Test validates minimum score of a questionnaire.
|8|| Test ensures that a question object is deleted properly taking all its association into consideration.
|}
|}


* Demonstrating working of API endpoints on Postman. Given below are the endpoints which we tested successfully on Postman.
[[File:Question_model_test.jpeg|border|800px|]]
'''Questionnaires Controller'''
#index: GET method - http://152.7.178.189:8080/api/v1/questionnaires
#show: GET method - http://152.7.178.189:8080/api/v1/questionnaires/1
#create: POST method - http://152.7.178.189:8080/api/v1/questionnaires/
#destroy: DELETE method - http://152.7.178.189:8080/api/v1/questionnaires/1
#update: .PUT method - http://152.7.178.189:8080/api/v1/questionnaires/1
#copy: POST method - http://152.7.178.189:8080/api/v1/questionnaires/copy/1
#toggle_access: GET method - http://152.7.178.189:8080/api/v1/questionnaires/toggle_access/1


'''Questions Controller'''
== Future Work ==
#index: GET method - http://152.7.178.189:8080/api/v1/questions/
*The questions and questionnaires model in the reimplementation-back-end repo has been updated to have question_type and questionnaire_type attribute instead of type attribute. Ruby has implicit inheritance when the type attribute is used. However, our repo does not have the dependent models for each type of Question and Questionnaire. For future implementation, those model classes could be added and this attribute can be refactored in the model and controller files.
#show: GET method - http://152.7.178.189:8080/api/v1/questions/1
*Implement other associations (for example: assignments) within each models.
#create: POST method - http://152.7.178.189:8080/api/v1/questions/
#destroy: DELETE method - http://152.7.178.189:8080/api/v1/questions/2
#update: PUT method - http://152.7.178.189:8080/api/v1/questions/2
#delete all: DELETE method - http://152.7.178.189:8080/api/v1/questions/delete_all/2
#types: GET method - http://152.7.178.189:8080/api/v1/questions/types


== Relevant Links ==
== Relevant Links ==
Line 353: Line 541:
* '''Pull Request:''' https://github.com/expertiza/reimplementation-back-end/pull/20
* '''Pull Request:''' https://github.com/expertiza/reimplementation-back-end/pull/20
* '''VCL Server:''' http://152.7.178.189:8080
* '''VCL Server:''' http://152.7.178.189:8080
* '''Test Video Link:'''
* '''Test Video Link:''' https://youtu.be/zkId16tklXI


==Contributors==
==Contributors==
This feature was created as part of Dr. Edward Gehringer's "CSC/ECE 517: Object-Oriented Design and Development" class, Spring 2023. The contributors were: Vineet Vimal Chheda, Rohan Jigarbhai Shah, and Aditya Srivastava. Our project mentor was Ankur Mundra (amundra@ncsu.edu)
This feature was created as part of Dr. Edward Gehringer's "CSC/ECE 517: Object-Oriented Design and Development" class, Spring 2023. The contributors were: Vineet Vimal Chheda, Rohan Jigarbhai Shah, and Aditya Srivastava. Our project mentor was Ankur Mundra (amundra@ncsu.edu)

Latest revision as of 02:13, 16 November 2023

Introduction

This page gives a description of the changes made for the questions_controller.rb, questionnaire_controller.rb, question.rb & questionnaire.rb of Expertiza based OSS project.

Problem Statement

What is needed: This project builds on E2321, and the main goal is to get the endpoints of QuestionsController and QuestionnairesController running in reimplementation-backend repository. Detailed goals of the project are as follows:

  1. Improving the clarity and conciseness of code: The code should be written in a clean and concise manner. Methods with identical names that perform different functionalities should be renamed for clarity. Functions or functionality that are not clear should be commented on or removed. Any loops or methods that can be refactored for better performance should be addressed.
  2. Discarding unused functionality: Any unused functionality should be removed from the controllers. This will help to reduce complexity and make the code easier to maintain.
  3. Reimplement Question and Questionnaire models: Reimplement the two models to facilitate the endpoints of the respective controllers.
  4. Writing tests for the two models: Tests should be written for both the Questionnaires and the Questions models.
  5. Writing tests for the two controllers: Tests should be written for both the QuestionnairesController and the QuestionsController. The tests should written in the RSwag test format.

General Design Goals

  • Ensure all prior/expected functionality is working correctly, and if not, make any repairs or changes necessary.
  • Ensure that the each of the models and controllers have loose coupling and tight cohesion.
  • DRY out prior implementation's controller and model methods which have functionality that already exists within the expertiza system.
  • Ensure all present test cases still work properly after making the above changes and create additional test cases.

Implementation

The current implementation for the code can be found here E2321.
To get the QuestionnairesController and QuestionsController endpoints running in the reimplementation-back-end repository, we need to reimplement the respective model files and add other dependent files.

The following UML diagram shows the association between models we will be working on -

Below is the implementation plan that we have followed for E2345:

  1. Implement basic version of question.rb and questionnaire.rb such that it facilitates the controllers' CRUD operations.
    • Add required associations for user.rb, role.rb, question.rb, and questionnaire.rb only.
    • The below code snippets represent current implementation of question.rb:
    class Question < ApplicationRecord
        belongs_to :questionnaire # each question belongs to a specific questionnaire
        
        validates :seq, presence: true, numericality: true # sequence must be numeric
        validates :txt, length: { minimum: 0, allow_nil: false, message: "can't be nil" } # user must define text content for a question
        validates :question_type, presence: true # user must define type for a question
        validates :break_before, presence: true
      
        
        def as_json(options = {})
            super(options.merge({
                                  only: %i[questionnaire_id txt weight seq question_type size alternatives break_before min_label max_label created_at updated_at],
                                  include: {
                                    questionnaire: 
                                          { only: %i[name private min_question_score max_question_score instructor_id created_at updated_at questionnaire_type] }
                                  }
                                })).tap do |hash|
              hash['questionnaire'] ||= { id: nil, name: nil }
            end
        end
      
      end
    

    In the above code, the as_json method is used to decorate the JSON object that is returned as a part of the response body from the controller endpoint. It is used to replace the questionnaire_id with the questionnaire details within the question object in JSON format.

    • The below code snippets represent current implementation of questionnaire.rb:
    class Questionnaire < ApplicationRecord
        has_many :questions, dependent: :restrict_with_error
        belongs_to :instructor
      
        before_destroy :check_for_question_associations
    
        validate :validate_questionnaire
        validates :name, presence: true
        validates :max_question_score, :min_question_score, numericality: true
      
        # clones the contents of a questionnaire, including the questions and associated advice
        def self.copy_questionnaire_details(params)
          orig_questionnaire = Questionnaire.find(params[:id])
          questions = Question.where(questionnaire_id: params[:id])
          questionnaire = orig_questionnaire.dup
          questionnaire.name = 'Copy of ' + orig_questionnaire.name
          questionnaire.created_at = Time.zone.now
          questionnaire.updated_at = Time.zone.now
          questionnaire.save!
          questions.each do |question|
            new_question = question.dup
            new_question.questionnaire_id = questionnaire.id
            new_question.save!
          end
          questionnaire
        end
      
        # validate the entries for this questionnaire
        def validate_questionnaire
          errors.add(:max_question_score, 'The maximum question score must be a positive integer.') if max_question_score < 1
          errors.add(:min_question_score, 'The minimum question score must be a positive integer.') if min_question_score < 0
          errors.add(:min_question_score, 'The minimum question score must be less than the maximum.') if min_question_score >= max_question_score
          results = Questionnaire.where('id <> ? and name = ? and instructor_id = ?', id, name, instructor_id)
          errors.add(:name, 'Questionnaire names must be unique.') if results.present?
        end
    
        # Check_for_question_associations checks if questionnaire has associated questions or not
        def check_for_question_associations
          if questions.any?
            raise ActiveRecord::DeleteRestrictionError.new(:base, "Cannot delete record because dependent questions exist")
          end
        end
    
        def as_json(options = {})
            super(options.merge({
                                  only: %i[id name private min_question_score max_question_score created_at updated_at questionnaire_type],
                                  include: {
                                    instructor: { only: %i[name email fullname password role] }
                                  }
                                })).tap do |hash|
              hash['instructor'] ||= { id: nil, name: nil }
            end
        end
      end
    

    In the above code,

    • The as_json method is used to decorate the JSON object that is returned as a part of the response body from the controller endpoint. It is used to replace the instructor_id with the instructor details within the questionnaire object in JSON format.
    • The check_for_question_associations method is used to raise an Error if a Questionnaire that has associated questions is tried to be deleted.
    • The copy_questionnaire_details method is used to create a new object with the same attributes of the questionnaire and save it to the database. It is called when the copy endpoint is requested.
  2. Reimplement questionnaires_controller.rb to incorporate the feedback received in E2321.
    • index: This is a GET endpoint that responds with a list of all questionnaires with their attributes.
    def index
        @questionnaires = Questionnaire.order(:id)
        render json: @questionnaires, status: :ok and return
    end
    
    • show: This is a GET endpoint that accepts a questionnaire id and responds with the attributes of that questionnaire if it exists, otherwise provides with appropriate error.
    def show
        begin
          @questionnaire = Questionnaire.find(params[:id])
          render json: @questionnaire, status: :ok and return
        rescue ActiveRecord::RecordNotFound
          render json: $ERROR_INFO.to_s, status: :not_found and return
        end
    end
    
    • create: This is a POST endpoint that accepts the questionnaire parameters and creates a new questionnaire. Refactor the Create endpoint in questionnaires_controller.rb such that individual fields does not need to be set and validated. The below code represents the revised implementation of the method. A Questionnaire object is created using the new method instead of manual assignment of attributes.
    def create
        begin
          @questionnaire = Questionnaire.new(questionnaire_params)
          @questionnaire.display_type = sanitize_display_type(@questionnaire.questionnaire_type)
          @questionnaire.save!
          render json: @questionnaire, status: :created and return
        rescue ActiveRecord::RecordInvalid
          render json: $ERROR_INFO.to_s, status: :unprocessable_entity
        end
    end
    
    • destroy: This is a DELETE endpoint that accepts a questionnaire ID and deletes that questionnaire if it exists, otherwise provides with appropriate error. It should delete the questionnaire only if there are no questions associated to it.
    def destroy
        begin
          @questionnaire = Questionnaire.find(params[:id])
          @questionnaire.delete
        rescue ActiveRecord::RecordNotFound
          render json: $ERROR_INFO.to_s, status: :not_found and return
        rescue ActiveRecord::RecordInvalid
          render json: $ERROR_INFO.to_s, status: :unprocessable_entity
        end
    end
    
    • update: This is a PUT endpoint that accepts a questionnaire ID and parameters, and, updates that questionnaire with the given parameters if it exists, otherwise provides with appropriate error.
    def update
        # Save questionnaire information
        begin
          @questionnaire = Questionnaire.find(params[:id])
          @questionnaire.update(questionnaire_params)
          @questionnaire.save!
          render json: @questionnaire, status: :ok and return
        rescue ActiveRecord::RecordNotFound
          render json: $ERROR_INFO.to_s, status: :not_found and return
        rescue ActiveRecord::RecordInvalid
          render json: $ERROR_INFO.to_s, status: :unprocessable_entity
        end
    end
    
    • copy: This is a POST endpoint that accepts a questionnaire ID and creates a new questionnaire (copy) with the same attributes of that questionnaire if it exists, otherwise provides with appropriate error.
    def copy
        begin
          @questionnaire = Questionnaire.copy_questionnaire_details(params)
          render json: "Copy of the questionnaire has been created successfully.", status: :ok and return
        rescue ActiveRecord::RecordNotFound
          render json: $ERROR_INFO.to_s, status: :not_found and return
        rescue ActiveRecord::RecordInvalid
          render json: $ERROR_INFO.to_s, status: :unprocessable_entity
        end
    end
    
    • toggle_access: This is a GET endpoint that changes the access from public to private and vice versa if it exists, otherwise provides with appropriate error.
    def toggle_access
        begin
          @questionnaire = Questionnaire.find(params[:id])
          @questionnaire.private = !@questionnaire.private
          @questionnaire.save!
          @access = @questionnaire.private == true ? 'private' : 'public'
          render json: "The questionnaire \"#{@questionnaire.name}\" has been successfully made #{@access}. ", status: :ok and return
        rescue ActiveRecord::RecordNotFound
          render json: $ERROR_INFO.to_s, status: :not_found and return
        rescue ActiveRecord::RecordInvalid
          render json: $ERROR_INFO.to_s, status: :unprocessable_entity
        end
    end
    
  3. Reimplement questions_controller.rb to incorporate the feedback received in E2321.
    • index: This is a GET endpoint that responds with a list of all questions with their attributes.
    def index
        @questions = Question.order(:id)
        render json: @questions, status: :ok
    end
    
    • show: This is a GET endpoint that accepts a question id and responds with the attributes of that question if it exists, otherwise provides with appropriate error.
    def show
        begin
          @question = Question.find(params[:id])
          render json: @question, status: :ok
        rescue ActiveRecord::RecordNotFound
          render json: $ERROR_INFO.to_s, status: :not_found
        end
    end
    
    • create: This is a POST endpoint that accepts the question parameters and creates a new question. Refactor the Create endpoint in questions_controller.rb such that individual fields does not need to be set and validated. The below code represents the revised implementation of the method. A Question object is created using the new method instead of manual assignment of attributes.
    def create
        begin
          questionnaire_id = params[:questionnaire_id] unless params[:questionnaire_id].nil?
          num_of_existed_questions = Questionnaire.find(questionnaire_id).questions.size
          question = Question.create(
            txt: params[:txt],
            questionnaire_id: questionnaire_id,
            seq: num_of_existed_questions + 1,
            question_type: params[:question_type],
            break_before: true)
          case question.question_type
            when 'Scale'
              question.weight = params[:weight]
              question.max_label = 'Strongly agree'
              question.min_label = 'Strongly disagree'
            when 'Cake', 'Criterion'
              question.weight = params[:weight]
              question.max_label = 'Strongly agree'
              question.min_label = 'Strongly disagree'
              question.size = '50, 3'
            when 'Dropdown'
              question.alternatives = '0|1|2|3|4|5'
              question.size = nil
            when 'TextArea'
              question.size = '60, 5'
            when 'TextField'
              question.size = '30'
          end
        
          question.save!
          render json: question, status: :created
        rescue ActiveRecord::RecordNotFound
          render json: $ERROR_INFO.to_s, status: :not_found and return  
        rescue ActiveRecord::RecordInvalid
          render json: $ERROR_INFO.to_s, status: :unprocessable_entity
        end
    end
    
    • destroy: This is a DELETE endpoint that accepts a question ID and deletes that question if it exists, otherwise provides with appropriate error.
    def destroy
        begin
          @question = Question.find(params[:id])
          @question.destroy
        rescue ActiveRecord::RecordNotFound
          render json: $ERROR_INFO.to_s, status: :not_found and return
        rescue ActiveRecord::RecordInvalid
          render json: $ERROR_INFO.to_s, status: :unprocessable_entity
        end
    end
    
    • delete_all: This is a DELETE endpoint that takes questionnaire_id as the path parameter and deletes all questions associated to that questionnaire. It returns status 404 if the questionnaire does not exist, status 422 if no questions are associated to that questionnaire and status 200 if all the questions are successfully deleted.
    def delete_all
        begin
          @questionnaire = Questionnaire.find(params[:id])
          if @questionnaire.questions.size > 0 
            @questionnaire.questions.destroy_all
            msg = "All questions for Questionnaire ID:" + params[:id].to_s + " has been successfully deleted!"
            render json: msg, status: :ok
          else
            render json: "No questions associated to Questionnaire ID:" + params[:id].to_s, status: :unprocessable_entity and return
          end
        rescue ActiveRecord::RecordNotFound
          render json: $ERROR_INFO.to_s, status: :not_found and return
        rescue ActiveRecord::RecordInvalid
          render json: $ERROR_INFO.to_s, status: :unprocessable_entity
        end
    end
    
    • shiw_all: This is a GET endpoint that takes questionnaire_id as the path parameter and shows all questions associated to that questionnaire. It returns status 404 if the questionnaire does not exist, status 422 if no questions are associated to that questionnaire and status 200 if all the questions are successfully displayed.
      def show_all
        begin
          @questionnaire = Questionnaire.find(params[:id])
          @questions = @questionnaire.questions
          render json: @questions, status: :ok
        rescue ActiveRecord::RecordNotFound
          render json: $ERROR_INFO.to_s, status: :not_found
        rescue ActiveRecord::RecordInvalid
          render json: $ERROR_INFO.to_s, status: :unprocessable_entity
        end
      end
    
    
    • update: This is a PUT endpoint that accepts a question ID and parameters, and, updates that question with the given parameters if it exists, otherwise provides with appropriate error.
    def update
        begin
          @question = Question.find(params[:id])
          @question.update(question_params)
          @question.save!
          render json: @question, status: :ok and return
        rescue ActiveRecord::RecordNotFound
          render json: $ERROR_INFO.to_s, status: :not_found and return
        rescue ActiveRecord::RecordInvalid
          render json: $ERROR_INFO.to_s, status: :unprocessable_entity
        end
    end
    
    • types: This is a GET endpoint that responds with the list of question types.
    def types
        types = Question.distinct.pluck(:question_type)
        render json: types.to_a, status: :ok
    end
    
  4. Write unit tests for Questions model and Questionnaires model using Rspec: Kindly refer to the Testing Methodology section for detailed description.
  5. Write Rspec tests using Swagger API for each endpoint of QuestionsController and QuestionnairesController. Kindly refer to the Testing Methodology section for detailed description.

Testing Methodology

Rspec test skeleton for QuestionnairesController and QuestionsController had been added in the previous project E2321 but had not been executed due to missing dependencies. This project focuses on implementing them and getting automated test cases running as expected. In our previous project we implemented the CRUD endpoints but since all the models were not present in reimplementation backend repository, we could only do manual testing of our controller endpoints using Postman. In this project, we focus to create the models required to implement CRUD operations in the reimplementation-back-end repository and test the endpoints using Swagger API.

Controller Tests

To run the controller tests:

  1. git clone
  2. cd reimplementation-back-end/
  3. bundle install
  4. rspec spec/requests/api/v1/questionnaires_controller_spec.rb
  5. rspec spec/requests/api/v1/questions_controller_spec.rb
  6. RAILS_ENV=test rails rswag

Tests for QuestionnairesController: questionnaires_spec.rb

Sr No Test Description
1 Test to create a new Questionnaire with valid parameters.
2 Test to create a new Questionnaire with invalid parameters.
3 Test to create a copy of Questionnaire with existing id in database.
4 Test to create a copy of Questionnaire with non-existing id in database.
5 Test to list the Questionnaires.
6 Test to update a Questionnaire (using put request) with correct parameters.
7 Test to update a Questionnaire (using put request) with incorrect parameters.
8 Test to update a Questionnaire (using put request) with non-existing id in database.
9 Test to delete Questionnaire with existing id in database.
10 Test to delete Questionnaire with the non-existing id in database.
11 Test to show a Questionnaire with a existing id in database.
12 Test to show a Questionnaire with non-existing id in database.
13 Test to toggle private boolean flag of a Questionnaire with a existing id in database.
14 Test to toggle private boolean flag of a Questionnaire with non-existing id in database.
15 Test to update the Questionnaire (using patch request) with correct parameters.
16 Test to update the Questionnaire (using patch request) with incorrect parameters.
17 Test to update the Questionnaire (using patch request) with non-existing id in the database.



Tests for QuestionsController: questions_spec.rb

Sr No Test Description
1 Test to create a new Question with valid parameters.
2 Test to create a new Question with invalid parameters.
3 Test to create a new Question with non-existing Questionnaire id in the database
4 Test to list the Questions in the database.
5 Test to update the Question (using put request)with correct parameters.
6 Test to update the Question (using put request) with incorrect parameters.
7 Test to update the Question (using put request)with non-existing id in the database.
8 Test to delete Question with existing id in the database.
9 Test to delete Question with the non-existing id in the database.
10 Test to show a Question with existing id in the database.
11 Test to show a Question with non-existing id in the database.
12 Test to get a types of Questions present in the database.
13 Test to delete all Questions of a Questionnaire with a valid Questionnaire id.
14 Test to delete all Questions of a Questionnaire with an invalid Questionnaire id.
15 Test to update the Question (using patch request) with correct parameters.
16 Test to update the Question (using patch request) with incorrect parameters.
17 Test to update the Question (using patch request) with non-existing id in the database.
18 Test to show all Questions of a Questionnaire with a valid Questionnaire id.
19 Test to show all Questions of a Questionnaire with an invalid Questionnaire id.




Model Tests

To run the model tests:

  1. git clone
  2. cd reimplementation-back-end/
  3. bundle install
  4. rspec spec/models/questionnaire_spec.rb
  5. rspec spec/models/question_spec.rb

Tests for Questionnaire model: questionnaire_spec.rb

Sr No Test Description
1 Test that Questionnaire requires a name.
2 Test validates the name of the questionnaire.
3 Test validates the instructor id in the questionnaire .
4 Test validates the maximum score in the questionnaire.
5 Test ensures maximum score of the questionnaire is an integer.
6 Test ensures maximum score is positive.
7 Test ensures maximum score is greater than the minimum score.
8 Test validates minimum score of a questionnaire.
9 Test ensures minimum score is smaller than maximum score.
10 Test ensures minimum score is an integer.
11 Test validates the association that a questionnaire comprises of several questions.
12 Test ensures that a questionnaire is not deleted when it has questions associated.
13 Test ensures calls from the method copy_questionnaire_details.
14 Test ensures creation of a copy of given questionnaire.
15 Test ensures creation of copy of all the present questionnaire in the database.

Tests for Question model: question_spec.rb

Sr No Test Description
1 Test that Question has valid attributes defined.
2 Test ensures that a question is not valid without seq field.
3 Test that Question sequence number must be numerical.
4 Test ensures that a question is not valid without txt field.
5 Test ensures that a question is not valid without question_type field.
6 Test ensures that a question is not valid without break_before field.
7 Test ensures that a question does not exist without a questionnaire.
8 Test ensures that a question object is deleted properly taking all its association into consideration.

Future Work

  • The questions and questionnaires model in the reimplementation-back-end repo has been updated to have question_type and questionnaire_type attribute instead of type attribute. Ruby has implicit inheritance when the type attribute is used. However, our repo does not have the dependent models for each type of Question and Questionnaire. For future implementation, those model classes could be added and this attribute can be refactored in the model and controller files.
  • Implement other associations (for example: assignments) within each models.

Relevant Links

Contributors

This feature was created as part of Dr. Edward Gehringer's "CSC/ECE 517: Object-Oriented Design and Development" class, Spring 2023. The contributors were: Vineet Vimal Chheda, Rohan Jigarbhai Shah, and Aditya Srivastava. Our project mentor was Ankur Mundra (amundra@ncsu.edu)