CSC/ECE 517 Fall 2022 - E2255. Refactor student quizzes controller.rb
About Expertiza
Ruby on Rails was used to create Expertiza, a multifunctional online application for instructors and students. Instructors using Expertiza have the ability to design their own teams, classes, assignments, quizzes, and much more. However, students are also permitted to work in groups, take tests, and do tasks. Additionally, Expertiza enables students to share peer reviews, allowing them to collaborate to enhance others' learning opportunities. It is an open-source program, and Expertiza is in Github repository Expertiza.
E2255. Refactoring student_quizzes_controller.rb in Expertiza
This page is a description of Expertiza OSS project E2255, which is refactoring the student_quizzes_controller.rb file.
Controller Description
The student_quizzes_controller is a collection of tools for designing, grading, and documenting the results of quizzes taken by students or reviewers from other teams working on the same project. Some of the problems with this controller go against fundamental Rails design tenets like the DRY principle. This controller has a few methods that belong in model classes. Code duplication is caused by some procedures that share the same code. Rewriting certain method comments is necessary.
Files Modified
1. app/controllers/student_quizzes_controller.rb
2. app/views/student_quizzes/finished_quiz.html
3. app/helpers/student_quizzes_helper.rb
4. app/views/student_quizzes/review_questions.html.erb
Modifications made to the existing code
1. Modification - Added comments for custom methods in this controller explaining the purpose.
#Checks authorization for any action based on user type: Student or Teaching Assistant.
#returns quizzes to be reviewed for a participant.
#checks if logged in user is not a participant. def index
#For Student, check if student has authorizations of reviewer & submitter. #returns true for any action other than 'index'. #check if user is Teaching Assistant def action_allowed?
#Checks authorization for any action based on user type: Student or Teaching Assistant.
#returns quizzes to be reviewed for a participant.
#checks if logged in user is not a participant. def index
# Populating Quiz Response Data.
# Populating participant response. #Populating Quiz Response. #Populating Quiz Questions. #Populating ResponseMap. #Populating participant who gave the quiz. #Populating quiz score of the Participant def finished_quiz
# check if there is any response for this map_id. This is to prevent student take same quiz twice.
# If there is no instance of the response of the student, create new and save.<br> #Quiz is already taken.<br> def record_response
#Get all teams of participants who created quizzes. #Get all quizzes of the team. #Populate all the questionnaires of quiz. def review_questions.
# Get reviewer # Get reviewed_team_response_maps based on reviewer_id # Get team of the reviewer # Get the latest quiz questions of the team of reviewer
2. Modification - Removed unused function from:
student_quizzes_controller.rb:
def graded?(response, question) Answer.where(question_id: question.id, response_id: response.id).first end
3. Modification - Name of function has been renamed from record_response to save_quiz_response and also renamed other variables to better understand the purpose of the function.
student_quizzes_controller.rb:
Old:
def record_response map = ResponseMap.find(params[:map_id]) if map.response.empty? # If there is no instance of the response of the student, create new and save.
New:
def save_quiz_response participant_response = ResponseMap.find(params[:map_id]) if participant_response.response.empty? # If there is no instance of the response of the student, create new and save.
4. Modification - Changed variable names to better understand what information they are storing.
student_quizzes_controller.rb:
Old:
calculate_score map, response redirect_to controller: 'student_quizzes', action: 'finished_quiz', map_id: map.id
New:
calculate_score participant_response, response redirect_to controller: 'student_quizzes', action: 'finished_quiz', map_id: participant_response.id
config/routes.rb:
Old:
post :record_response
New:
post :save_quiz_response
5. Modification - Changed variable names like @response to @participant_response to better understand what information they are storing.
student_quizzes_controller.rb:
Old:
@response = Response.where(map_id: params[:map_id]).first # Populating participant response @response_map = QuizResponseMap.find(params[:map_id]) #Populating Quiz Response @questions = Question.where(questionnaire_id: @response_map.reviewed_object_id) #Populating Quiz Questions @map = ResponseMap.find(params[:map_id]) #Populating ResponseMap @participant = AssignmentTeam.find(@map.reviewee_id).participants.first #Populating participant who gave the quiz @quiz_score = @response_map.quiz_score #Populating quiz score of the Participant
New:
@participant_response = Response.where(map_id: params[:map_id]).first # Populating participant response @quiz_response_map = QuizResponseMap.find(params[:map_id]) #Populating Quiz Response @quiz_questions = Question.where(questionnaire_id: @quiz_response_map.reviewed_object_id) #Populating Quiz Questions response_map = ResponseMap.find(params[:map_id]) #Populating ResponseMap @participant = AssignmentTeam.find(response_map.reviewee_id).participants.first #Populating participant who gave the quiz @participant_quiz_score = @quiz_response_map.quiz_score #Populating quiz score of the Participant
finished_quiz.html.erb:
Old:
<%=t ".quiz_score"%><%= @quiz_score %> <% @questions.each do |question| %> <%user_answer=Answer.where(response_id: @response.id, question_id: question.id)%> <%= link_to t(".back"), student_quizzes_path(:id => @response_map.reviewer_id) %>
New:
<%=t ".quiz_score"%><%= @participant_quiz_score %> <% @quiz_questions.each do |question| %> <%user_answer=Answer.where(response_id: @participant_response.id, question_id: question.id)%> <%= link_to t(".back"), student_quizzes_path(:id => @quiz_response_map.reviewer_id) %>
6. Modification - Moved the Calculate_score function to the helper file from the controller, and broke it into multiple functions, which makes it easy to understand.
student_quizzes_helper.rb:
Old:
def calculate_score(map, response) questionnaire = Questionnaire.find(map.reviewed_object_id) scores = [] valid = true questions = Question.where(questionnaire_id: questionnaire.id) questions.each do |question| score = 0 correct_answers = QuizQuestionChoice.where(question_id: question.id, iscorrect: true) ques_type = question.type if ques_type.eql? 'MultipleChoiceCheckbox' if params[question.id.to_s].nil? valid = false else params[question.id.to_s].each do |choice| # loop the quiz taker's choices and see if 1)all the correct choice are checked and 2) # of quiz taker's choice matches the # of the correct choices correct_answers.each do |correct| score += 1 if choice.eql? correct.txt end end score = score == correct_answers.count && score == params[question.id.to_s].count ? 1 : 0 # for MultipleChoiceCheckbox, score =1 means the quiz taker have done this question correctly, not just make select this choice correctly. params[question.id.to_s].each do |choice| new_score = Answer.new comments: choice, question_id: question.id, response_id: response.id, answer: score valid = false unless new_score.valid? scores.push(new_score) end end else # TrueFalse and MultipleChoiceRadio correct_answer = correct_answers.first score = correct_answer.txt == params[question.id.to_s] ? 1 : 0 new_score = Answer.new comments: params[question.id.to_s], question_id: question.id, response_id: response.id, answer: score valid = false if new_score.nil? || new_score.comments.nil? || new_score.comments.empty? scores.push(new_score) end end if valid scores.each(&:save) redirect_to controller: 'student_quizzes', action: 'finished_quiz', map_id: map.id else response.destroy flash[:error] = 'Please answer every question.' redirect_to action: :take_quiz, assignment_id: params[:assignment_id], questionnaire_id: questionnaire.id, map_id: map.id end end
New
module StudentQuizzesHelper # the way 'answers' table store the results of quiz def calculate_score(participant_response, quiz_response) scores = [] valid = true get_all_questions(participant_response).each do |question| correct_answers = QuizQuestionChoice.where(question_id: question.id, iscorrect: true) ques_type = question.type if ques_type.eql? 'MultipleChoiceCheckbox' valid = is_valid_checkbox(params, question, valid) valid = score_multiple_choice_checkbox(correct_answers, question, params, valid, scores, quiz_response) else # TrueFalse and MultipleChoiceRadio valid = score_multiple_choice_radio(correct_answers, params, question, scores, quiz_response, valid) end end save_scores_if_valid(valid, scores, quiz_response,participant_response) end def save_scores_if_valid(valid, scores, quiz_response,participant_response) if valid scores.each(&:save) redirect_to controller: 'student_quizzes', action: 'finished_quiz', map_id: participant_response.id else quiz_response.destroy flash[:error] = 'Please answer every question.' questionnaire = Questionnaire.find(participant_response.reviewed_object_id) redirect_to action: :take_quiz, assignment_id: params[:assignment_id], questionnaire_id: questionnaire.id, map_id: participant_response.id end end def get_all_questions(participant_response) questionnaire = Questionnaire.find(participant_response.reviewed_object_id) Question.where(questionnaire_id: questionnaire.id) end def is_valid_checkbox(params, question, valid) if params[question.id.to_s].nil? valid = false; end valid end def score_multiple_choice_checkbox(correct_answers, question, params, valid, scores, quiz_response) score = 0 params[question.id.to_s].each do |choice| # loop the quiz taker's choices and see if 1)all the correct choice are checked and 2) # of quiz taker's choice matches the # of the correct choices correct_answers.each do |correct| score += 1 if choice.eql? correct.txt end end score = score == correct_answers.count && score == params[question.id.to_s].count ? 1 : 0 # for MultipleChoiceCheckbox, score =1 means the quiz taker have done this question correctly, not just make select this choice correctly. score_checkbox(scores, params, question, valid, quiz_response, score) end def score_checkbox(scores, params, question, valid, quiz_response, score) params[question.id.to_s].each do |choice| new_score = Answer.new comments: choice, question_id: question.id, response_id: quiz_response.id, answer: score valid = false unless new_score.valid? scores.push(new_score) end valid end def score_multiple_choice_radio(correct_answers, params, question, scores, quiz_response, valid) correct_answer = correct_answers.first score = correct_answer.txt == params[question.id.to_s] ? 1 : 0 new_score = Answer.new comments: params[question.id.to_s], question_id: question.id, response_id: quiz_response.id, answer: score valid = false if new_score.nil? || new_score.comments.nil? || new_score.comments.empty? scores.push(new_score) valid end end
7. Modification - Renamed @assignment_id to quiz_creator_user_id to clarify the values they are holding.
New
@assignment_id = params[:id]
Old
@quiz_creator_user_id = params[:id]
8. Modification - Renamed variable names for better understanding.
student_quizzes_controller.rb:
Old:
def review_questions
New:
def get_questions_created_by_id
9. Modification - Updated config variable names.
config/routes.rb:
Old:
get :review_questions
New:
get :get_questions_created_by_id
10. Modification - Modified variable name from quizzes to quiz_questions.
app/controllers/student_quizzes_controller.rb
Old:
# Create an array of candidate quizzes for current reviewer def self.take_quiz(assignment_id, reviewer_id) quizzes = [] quizzes << quiz_questionnaire unless quiz_questionnaire.taken_by? reviewer
New:
# Create an array of candidate quiz_questions for current reviewer def self.get_quiz_questionnaire(assignment_id, reviewer_id) quiz_questions = [] quiz_questions << quiz_questionnaire unless quiz_questionnaire.taken_by? reviewer
Test Plan
Manual Testing
Follow these instructions to manually test the functionality of student_quizzes_controller.
1. Log in as instructor using the credentials, username: instructor6, password: password.
2. Select the Assignments subheading. Here, create a new assignment by entering the necessary details. Enable the Has Teams? checkbox and create. Also enable the Has quiz? checkbox.
3. Set the proper due dates for submission review and evaluation by going to the Due Dates page. For Round 1: Review, enable Has Quiz using the dropdown Menu. Once you save, A Number of Quiz questions field will be visible on the Assignments page. Set this to 2 and save.
4. There will be a message inviting you to add participants at the top of the page. Several students should be added to the task using the given link.
5. After logging out as the teacher, log back in as the pupils. Before the deadline for submissions, turn in your assignment.
Note: To submit, you must first establish a team and give it a name.
6. There is a link to "Create a Quiz" at the bottom of the submission page. Select that link.
7. You are then sent to the quiz creation page. Here, you may set True/False questions, Checkbox questions, and Radio button questions along with the appropriate responses.
Then click "Create Quiz." Thus, the quiz is made.
8. Log out and re-enter the assignment as a different student who has registered. submit a document in this student's place. Review the first student's work when the submission deadline has passed, then go back to the main menu.
9. The option to "Take quizzes" will be visible. Select "Request" and then choose the quiz you made. You may now take the test that the other student made here.
Manual Testing Walkthorugh
The procedure for manually evaluating the controller's functioning is demonstrated in the video below.
Manual testing video
Testing Result Screenshot
Below is the screenshot of the testing cases which shows that all test cases were successfully run.
Testing Video
According to the documentation, there were no test cases for the student_quizzes_controller at first, so we created some to see if the functionality had been broken by the modifications we made. Our test cases have been completed without error. Here is the corresponding video.
Test Video link
Pull Request
The link for our pull request - https://github.com/expertiza/expertiza/pull/2468
Team members
Priyam Swatil Shah (pshah9@ncsu.edu)
Sanay Yogesh Shah (sshah34@ncsu.edu)
Subodh Gujar (sgujar@ncsu.edu)
Team Mentor
Neha Kotcherlakota (nkotche@ncsu.edu)