From a42201a21d1f60ba946601aed20757fc077d0b6e Mon Sep 17 00:00:00 2001 From: neerua08 Date: Mon, 2 Dec 2024 22:37:32 -0500 Subject: [PATCH 01/19] Added previous implementation --- .../api/v1/assignments_controller.rb | 13 +- .../api/v1/participants_controller.rb | 35 +++ .../api/v1/questionnaires_controller.rb | 8 +- .../api/v1/questions_controller.rb | 3 +- .../api/v1/student_quizzes_controller.rb | 198 +++++++++++++ app/models/answer.rb | 3 +- app/models/question.rb | 43 +-- app/models/questionnaire.rb | 49 ++-- app/models/response.rb | 57 +--- app/models/response_map.rb | 48 +--- config/routes.rb | 11 + ...515_add_assignment_id_to_questionnaires.rb | 5 + ...2225112_add_correct_answer_to_questions.rb | 7 + ...0322231234_add_score_value_to_questions.rb | 7 + ...240322231719_add_answer_text_to_answers.rb | 7 + .../20240322232203_add_correct_to_answers.rb | 5 + db/migrate/20240323155800_modify_response.rb | 7 + ...0240323155906_add_score_to_response_map.rb | 5 + db/schema.rb | 15 + spec/models/assignment_spec.rb | 64 ++++- .../api/v1/participants_controller_spec.rb | 81 ++++++ .../api/v1/student_quizzes_controller_spec.rb | 150 ++++++++++ swagger/v1/swagger.yaml | 260 ++++++++++++++++++ 23 files changed, 927 insertions(+), 154 deletions(-) create mode 100644 app/controllers/api/v1/participants_controller.rb create mode 100644 app/controllers/api/v1/student_quizzes_controller.rb create mode 100644 db/migrate/20240322223515_add_assignment_id_to_questionnaires.rb create mode 100644 db/migrate/20240322225112_add_correct_answer_to_questions.rb create mode 100644 db/migrate/20240322231234_add_score_value_to_questions.rb create mode 100644 db/migrate/20240322231719_add_answer_text_to_answers.rb create mode 100644 db/migrate/20240322232203_add_correct_to_answers.rb create mode 100644 db/migrate/20240323155800_modify_response.rb create mode 100644 db/migrate/20240323155906_add_score_to_response_map.rb create mode 100644 spec/requests/api/v1/participants_controller_spec.rb create mode 100644 spec/requests/api/v1/student_quizzes_controller_spec.rb diff --git a/app/controllers/api/v1/assignments_controller.rb b/app/controllers/api/v1/assignments_controller.rb index e28ad573f..98c8eea3a 100644 --- a/app/controllers/api/v1/assignments_controller.rb +++ b/app/controllers/api/v1/assignments_controller.rb @@ -45,7 +45,7 @@ def destroy render json: { error: "Assignment not found" }, status: :not_found end end - + #add participant to assignment def add_participant assignment = Assignment.find_by(id: params[:assignment_id]) @@ -92,7 +92,7 @@ def remove_assignment_from_course render json: assignment.errors, status: :unprocessable_entity end end - + end #update course id of an assignment/ assign the assign to some together course @@ -146,7 +146,7 @@ def show_assignment_details end # check if assignment has topics - # has_topics is set to true if there is SignUpTopic corresponding to the input assignment id + # has_topics is set to true if there is SignUpTopic corresponding to the input assignment id def has_topics assignment = Assignment.find_by(id: params[:assignment_id]) if assignment.nil? @@ -156,7 +156,7 @@ def has_topics end end - # check if assignment is a team assignment + # check if assignment is a team assignment # true if assignment's max team size is greater than 1 def team_assignment assignment = Assignment.find_by(id: params[:assignment_id]) @@ -204,11 +204,12 @@ def varying_rubrics_by_round? end end end - + private # Only allow a list of trusted parameters through. def assignment_params - params.require(:assignment).permit(:title, :description) + params.require(:assignment).permit(:name, :directory_path, :course_id, :instructor_id, + :require_quiz, :num_quiz_questions, :description) end # Helper method to determine staggered_and_no_topic for the assignment diff --git a/app/controllers/api/v1/participants_controller.rb b/app/controllers/api/v1/participants_controller.rb new file mode 100644 index 000000000..8d84ed189 --- /dev/null +++ b/app/controllers/api/v1/participants_controller.rb @@ -0,0 +1,35 @@ +class Api::V1::ParticipantsController < ApplicationController + before_action :find_user, only: :create + before_action :find_assignment, only: :create + # POST /participants + def create + participant = Participant.new(participant_params) + + if participant.save + render json: participant, status: :created + else + render json: participant.errors, status: :unprocessable_entity + end + end + + private + + # to fetch user + def find_user + unless User.exists?(params[:participant][:user_id]) + render json: { error: 'User does not exist' }, status: :not_found and return + end + end + + #to find assignment in the db + def find_assignment + unless Assignment.exists?(params[:participant][:assignment_id]) + render json: { error: 'Assignment does not exist' }, status: :not_found and return + end + end + + #to check params of a participant + def participant_params + params.require(:participant).permit(:user_id, :assignment_id) + end +end diff --git a/app/controllers/api/v1/questionnaires_controller.rb b/app/controllers/api/v1/questionnaires_controller.rb index 9bc2cd876..cebd1f258 100644 --- a/app/controllers/api/v1/questionnaires_controller.rb +++ b/app/controllers/api/v1/questionnaires_controller.rb @@ -76,7 +76,7 @@ def toggle_access @questionnaire.toggle!(:private) @access = @questionnaire.private ? 'private' : 'public' render json: "The questionnaire \"#{@questionnaire.name}\" has been successfully made #{@access}. ", - status: :ok + status: :ok rescue ActiveRecord::RecordNotFound render json: $ERROR_INFO.to_s, status: :not_found rescue ActiveRecord::RecordInvalid @@ -87,10 +87,13 @@ def toggle_access private def questionnaire_params - params.require(:questionnaire).permit(:name, :questionnaire_type, :private, :min_question_score, :max_question_score, :instructor_id) + params.require(:questionnaire).permit(:name, :questionnaire_type, :private, :min_question_score, + :max_question_score, :instructor_id, :assignment_id) end def sanitize_display_type(type) + return nil if type.nil? # Return immediately if type is nil + display_type = type.split('Questionnaire')[0] if %w[AuthorFeedback CourseSurvey TeammateReview GlobalSurvey AssignmentSurvey BookmarkRating].include?(display_type) display_type = (display_type.split(/(?=[A-Z])/)).join('%') @@ -98,4 +101,5 @@ def sanitize_display_type(type) display_type end + end \ No newline at end of file diff --git a/app/controllers/api/v1/questions_controller.rb b/app/controllers/api/v1/questions_controller.rb index 10c26875d..1e8d4bb1d 100644 --- a/app/controllers/api/v1/questions_controller.rb +++ b/app/controllers/api/v1/questions_controller.rb @@ -120,10 +120,9 @@ def types end private - # Only allow a list of trusted parameters through. def question_params params.permit(:txt, :weight, :seq, :questionnaire_id, :question_type, :size, - :alternatives, :break_before, :max_label, :min_label) + :alternatives, :break_before, :max_label, :min_label, :assignment_id, :correct_answer, :score_value) end end diff --git a/app/controllers/api/v1/student_quizzes_controller.rb b/app/controllers/api/v1/student_quizzes_controller.rb new file mode 100644 index 000000000..dc5519cb9 --- /dev/null +++ b/app/controllers/api/v1/student_quizzes_controller.rb @@ -0,0 +1,198 @@ +class Api::V1::StudentQuizzesController < ApplicationController + before_action :check_instructor_role, except: [:submit_answers] + before_action :set_student_quiz, only: [:show, :update, :destroy] + + rescue_from ActiveRecord::RecordInvalid do |exception| + render_error(exception.message) + end + + #GET /student_quizzes + def index + quizzes = Questionnaire.all + render_success(quizzes) + end + + #GET /student_quizzes/:id + def show + render_success(@student_quiz) + end + + #GET /student_quizzes/:id/calculate_score + def calculate_score + response_map = ResponseMap.find_by(id: params[:id]) + if response_map + render_success({ score: response_map.score }) + else + render_error('Attempt not found or you do not have permission to view this score.', :not_found) + end + end + + #POST /student_quizzes + def create + questionnaire = ActiveRecord::Base.transaction do + questionnaire = create_questionnaire(questionnaire_params.except(:questions_attributes)) + create_questions_and_answers(questionnaire, questionnaire_params[:questions_attributes]) + questionnaire + end + render_success(questionnaire, :created) + rescue StandardError => e + render_error(e.message, :unprocessable_entity) + end + + #POST /student_quizzes/assign + def assign_quiz_to_student + participant = find_resource_by_id(Participant, params[:participant_id]) + questionnaire = find_resource_by_id(Questionnaire, params[:questionnaire_id]) + return unless participant && questionnaire + + if quiz_already_assigned?(participant, questionnaire) + render_error("This student is already assigned to the quiz.", :unprocessable_entity) + return + end + + response_map = build_response_map(participant.user_id, questionnaire) + if response_map.save + render_success(response_map, :created) + else + render_error(response_map.errors.full_messages.to_sentence, :unprocessable_entity) + end + end + + #POST /student_quizzes/submit_answers + def submit_answers + ActiveRecord::Base.transaction do + response_map = find_response_map_for_current_user + unless response_map + render_error("You are not assigned to take this quiz.", :forbidden) + return + end + + total_score = process_answers(params[:answers], response_map) + response_map.update!(score: total_score) + render_success({ total_score: total_score }) + end + rescue ActiveRecord::RecordInvalid => e + render_error(e.message, :unprocessable_entity) + end + + #PUT /student_quizzes/:id + def update + if @student_quiz.update(questionnaire_params) + render_success(@student_quiz) + else + render_error(@student_quiz.errors.full_messages.to_sentence, :unprocessable_entity) + end + end + + #DELETE /student_quizzes/:id + def destroy + @student_quiz.destroy + head :no_content + rescue ActiveRecord::RecordNotFound + render_error('Record does not exist', :not_found) + end + + private + + #To get quiz from db + def set_student_quiz + @student_quiz = find_resource_by_id(Questionnaire, params[:id]) + end + + # Find the response map for the current user's attempt to submit quiz answers + def find_response_map_for_current_user + ResponseMap.find_by( + reviewee_id: current_user.id, + reviewed_object_id: params[:questionnaire_id] + ) + end + + # Process and calculate the total score for submitted answers + def process_answers(answers, response_map) + answers.sum do |answer| + question = Question.find(answer[:question_id]) + submitted_answer = answer[:answer_value] + + response = find_or_initialize_response(response_map.id, question.id) + response.submitted_answer = submitted_answer + response.save! + + question.correct_answer == submitted_answer ? question.score_value : 0 + end + end + + # Find or initialize a response for a specific question within an attempt + def find_or_initialize_response(response_map_id, question_id) + Response.find_or_initialize_by( + response_map_id: response_map_id, + question_id: question_id + ) + end + + # Find a specific resource by ID, handling the case where it's not found + def find_resource_by_id(model, id) + model.find(id) + rescue ActiveRecord::RecordNotFound + render_error("#{model.name} not found", :not_found) + nil + end + + # Check if a quiz has already been assigned to a participant + def quiz_already_assigned?(participant, questionnaire) + ResponseMap.exists?( + reviewee_id: participant.user_id, + reviewed_object_id: questionnaire.id + ) + end + + # Build a new ResponseMap instance for assigning a quiz to a student + def build_response_map(student_id, questionnaire) + instructor_id = questionnaire.assignment.instructor_id + ResponseMap.new( + reviewee_id: student_id, + reviewer_id: instructor_id, + reviewed_object_id: questionnaire.id + ) + end + + # Create a new questionnaire along with its questions and answers + def create_questionnaire(params) + Questionnaire.create!(params) + end + + # Create questions and their respective answers for a questionnaire + def create_questions_and_answers(questionnaire, questions_attributes) + questions_attributes.each do |question_attr| + question = questionnaire.questions.create!(question_attr.except(:answers_attributes)) + question_attr[:answers_attributes]&.each do |answer_attr| + question.answers.create!(answer_attr) + end + end + end + + # Permit and require the necessary parameters for creating/updating a questionnaire + def questionnaire_params + params.require(:questionnaire).permit( + :name, :instructor_id, :min_question_score, :max_question_score, :assignment_id, + questions_attributes: [:id, :txt, :question_type, :break_before, :correct_answer, :score_value, + { answers_attributes: %i[id answer_text correct] }] + ) + end + + # Render a success response with optional custom status code + def render_success(data, status = :ok) + render json: data, status: status + end + + # Render an error response with message and status code + def render_error(message, status = :unprocessable_entity) + render json: { error: message }, status: status + end + + # Ensure only instructors can perform certain actions + def check_instructor_role + unless current_user.role_id == 2 + render_error('Only instructors are allowed to perform this action', :forbidden) + end + end +end \ No newline at end of file diff --git a/app/models/answer.rb b/app/models/answer.rb index 6b063ed8c..c2a182530 100644 --- a/app/models/answer.rb +++ b/app/models/answer.rb @@ -1,4 +1,5 @@ class Answer < ApplicationRecord - belongs_to :response belongs_to :question + validates :answer_text, presence: true + validates :correct, inclusion: {in: [true, false]} end diff --git a/app/models/question.rb b/app/models/question.rb index 4dc76ae10..3ba30ef2d 100644 --- a/app/models/question.rb +++ b/app/models/question.rb @@ -1,28 +1,39 @@ class Question < ApplicationRecord - before_create :set_seq + before_validation :set_seq, on: :create belongs_to :questionnaire # each question belongs to a specific questionnaire has_many :answers, dependent: :destroy - - validates :seq, presence: true, numericality: true # sequence must be numeric + accepts_nested_attributes_for :answers # Allows nested attributes for answers + + 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 + validates :break_before, inclusion: { in: [true, false] } + validates :correct_answer, presence: true + validates :score_value, presence: true + def scorable? false end - - def set_seq - self.seq = questionnaire.questions.size + 1 - end def as_json(options = {}) - super(options.merge({ - only: %i[txt weight seq question_type size alternatives break_before min_label max_label created_at updated_at], - include: { - questionnaire: { only: %i[name id] } - } - })).tap do |hash| - end + super(options.merge({ + only: %i[txt weight seq question_type size alternatives break_before min_label max_label created_at updated_at], + include: { + questionnaire: { only: %i[name id] } + } + })).tap do |hash| + end end -end + + + private + + def set_seq + if questionnaire.present? + max_seq = questionnaire.questions.maximum(:seq) + self.seq = max_seq.to_i + 1 + end + end + +end \ No newline at end of file diff --git a/app/models/questionnaire.rb b/app/models/questionnaire.rb index d576bc421..134891465 100644 --- a/app/models/questionnaire.rb +++ b/app/models/questionnaire.rb @@ -2,11 +2,12 @@ class Questionnaire < ApplicationRecord belongs_to :assignment, foreign_key: 'assignment_id', inverse_of: false belongs_to :instructor has_many :questions, dependent: :destroy # the collection of questions associated with this Questionnaire + accepts_nested_attributes_for :questions # Allows nested attributes for questions before_destroy :check_for_question_associations validate :validate_questionnaire validates :name, presence: true - validates :max_question_score, :min_question_score, numericality: 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) @@ -25,31 +26,39 @@ def self.copy_questionnaire_details(params) 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? + if max_question_score.nil? || max_question_score < 1 + errors.add(:max_question_score, 'The maximum question score must be a positive integer.') + end + + if min_question_score.nil? || min_question_score.negative? + errors.add(:min_question_score, 'The minimum question score must be a positive integer.') + end + + return unless min_question_score && max_question_score && min_question_score >= max_question_score + + errors.add(:min_question_score, 'The minimum question score must be less than the maximum.') + 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 + return unless questions.any? + + raise ActiveRecord::DeleteRestrictionError, :base + 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 instructor_id], - include: { - instructor: { only: %i[name email fullname password role] - } - } - })).tap do |hash| - hash['instructor'] ||= { id: nil, name: nil } - end + super(options.merge({ + only: %i[id name private min_question_score max_question_score created_at updated_at + questionnaire_type instructor_id], + include: { + instructor: { only: %i[name email fullname password role] } + } + })).tap do |hash| + hash['instructor'] ||= { id: nil, name: nil } + end end -end +end \ No newline at end of file diff --git a/app/models/response.rb b/app/models/response.rb index c63bdb5fd..eb9d703c1 100644 --- a/app/models/response.rb +++ b/app/models/response.rb @@ -1,59 +1,6 @@ # frozen_string_literal: true class Response < ApplicationRecord - include ScorableHelper - include MetricHelper - - belongs_to :response_map, class_name: 'ResponseMap', foreign_key: 'map_id', inverse_of: false - has_many :scores, class_name: 'Answer', foreign_key: 'response_id', dependent: :destroy, inverse_of: false - - alias map response_map - delegate :questionnaire, :reviewee, :reviewer, to: :map - - def reportable_difference? - map_class = map.class - # gets all responses made by a reviewee - existing_responses = map_class.assessments_for(map.reviewee) - - count = 0 - total = 0 - # gets the sum total percentage scores of all responses that are not this response - existing_responses.each do |response| - unless id == response.id # the current_response is also in existing_responses array - count += 1 - total += response.aggregate_questionnaire_score.to_f / response.maximum_score - end - end - - # if this response is the only response by the reviewee, there's no grade conflict - return false if count.zero? - - # calculates the average score of all other responses - average_score = total / count - - # This score has already skipped the unfilled scorable question(s) - score = aggregate_questionnaire_score.to_f / maximum_score - questionnaire = questionnaire_by_answer(scores.first) - assignment = map.assignment - assignment_questionnaire = AssignmentQuestionnaire.find_by(assignment_id: assignment.id, questionnaire_id: questionnaire.id) - - # notification_limit can be specified on 'Rubrics' tab on assignment edit page. - allowed_difference_percentage = assignment_questionnaire.notification_limit.to_f - - # the range of average_score_on_same_artifact_from_others and score is [0,1] - # the range of allowed_difference_percentage is [0, 100] - (average_score - score).abs * 100 > allowed_difference_percentage - end - - def aggregate_questionnaire_score - # only count the scorable questions, only when the answer is not nil - # we accept nil as answer for scorable questions, and they will not be counted towards the total score - sum = 0 - scores.each do |s| - question = Question.find(s.question_id) - # For quiz responses, the weights will be 1 or 0, depending on if correct - sum += s.answer * question.weight unless s.answer.nil? || !question.scorable? - end - sum - end + belongs_to :response_map + belongs_to :question end diff --git a/app/models/response_map.rb b/app/models/response_map.rb index d27124d76..d0a2743d7 100644 --- a/app/models/response_map.rb +++ b/app/models/response_map.rb @@ -1,41 +1,9 @@ class ResponseMap < ApplicationRecord - has_many :response, foreign_key: 'map_id', dependent: :destroy, inverse_of: false - belongs_to :reviewer, class_name: 'Participant', foreign_key: 'reviewer_id', inverse_of: false - belongs_to :reviewee, class_name: 'Participant', foreign_key: 'reviewee_id', inverse_of: false - belongs_to :assignment, class_name: 'Assignment', foreign_key: 'reviewed_object_id', inverse_of: false - - alias map_id id - - # returns the assignment related to the response map - def response_assignment - return Participant.find(self.reviewer_id).assignment - end - - def self.assessments_for(team) - responses = [] - # stime = Time.now - if team - array_sort = [] - sort_to = [] - maps = where(reviewee_id: team.id) - maps.each do |map| - next if map.response.empty? - - all_resp = Response.where(map_id: map.map_id).last - if map.type.eql?('ReviewResponseMap') - # If its ReviewResponseMap then only consider those response which are submitted. - array_sort << all_resp if all_resp.is_submitted - else - array_sort << all_resp - end - # sort all versions in descending order and get the latest one. - sort_to = array_sort.sort # { |m1, m2| (m1.updated_at and m2.updated_at) ? m2.updated_at <=> m1.updated_at : (m1.version_num ? -1 : 1) } - responses << sort_to[0] unless sort_to[0].nil? - array_sort.clear - sort_to.clear - end - responses = responses.sort { |a, b| a.map.reviewer.fullname <=> b.map.reviewer.fullname } - end - responses - end -end + # 'reviewer_id' points to the User who is the instructor. + belongs_to :reviewer, class_name: 'User', foreign_key: 'reviewer_id', optional: true + belongs_to :reviewee, class_name: 'User', foreign_key: 'reviewee_id', optional: true + belongs_to :questionnaire, foreign_key: 'reviewed_object_id', optional: true + has_many :responses + validates :reviewee_id, uniqueness: { scope: :reviewed_object_id, + message: "is already assigned to this questionnaire" } +end \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index fc8a710a2..67b1e0aae 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -109,6 +109,17 @@ get :processed, action: :processed_requests end end + resources :student_quizzes do + member do + get :calculate_score + end + collection do + post 'assign', to: 'student_quizzes#assign_quiz_to_student' + post :create + end + end + post 'student_quizzes/submit_answers', to: 'student_quizzes#submit_answers' + resources :participants, only: [:create] end end end \ No newline at end of file diff --git a/db/migrate/20240322223515_add_assignment_id_to_questionnaires.rb b/db/migrate/20240322223515_add_assignment_id_to_questionnaires.rb new file mode 100644 index 000000000..aacf5ee82 --- /dev/null +++ b/db/migrate/20240322223515_add_assignment_id_to_questionnaires.rb @@ -0,0 +1,5 @@ +class AddAssignmentIdToQuestionnaires < ActiveRecord::Migration[7.0] + def change + add_reference :questionnaires, :assignment, null: false, foreign_key: true + end +end diff --git a/db/migrate/20240322225112_add_correct_answer_to_questions.rb b/db/migrate/20240322225112_add_correct_answer_to_questions.rb new file mode 100644 index 000000000..4dc5f5e21 --- /dev/null +++ b/db/migrate/20240322225112_add_correct_answer_to_questions.rb @@ -0,0 +1,7 @@ +class AddCorrectAnswerToQuestions < ActiveRecord::Migration[7.0] + def change + unless column_exists? :questions, :correct_answer + add_column :questions, :correct_answer, :string + end + end +end diff --git a/db/migrate/20240322231234_add_score_value_to_questions.rb b/db/migrate/20240322231234_add_score_value_to_questions.rb new file mode 100644 index 000000000..52bfff0c7 --- /dev/null +++ b/db/migrate/20240322231234_add_score_value_to_questions.rb @@ -0,0 +1,7 @@ +class AddScoreValueToQuestions < ActiveRecord::Migration[7.0] + def change + unless column_exists? :questions, :score_value + add_column :questions, :score_value, :integer + end + end +end diff --git a/db/migrate/20240322231719_add_answer_text_to_answers.rb b/db/migrate/20240322231719_add_answer_text_to_answers.rb new file mode 100644 index 000000000..d22f79e7b --- /dev/null +++ b/db/migrate/20240322231719_add_answer_text_to_answers.rb @@ -0,0 +1,7 @@ +class AddAnswerTextToAnswers < ActiveRecord::Migration[7.0] + def change + unless column_exists? :answers, :answer_text + add_column :answers, :answer_text, :text + end + end +end diff --git a/db/migrate/20240322232203_add_correct_to_answers.rb b/db/migrate/20240322232203_add_correct_to_answers.rb new file mode 100644 index 000000000..126b00e4c --- /dev/null +++ b/db/migrate/20240322232203_add_correct_to_answers.rb @@ -0,0 +1,5 @@ +class AddCorrectToAnswers < ActiveRecord::Migration[7.0] + def change + add_column :answers, :correct, :boolean, default: false + end +end diff --git a/db/migrate/20240323155800_modify_response.rb b/db/migrate/20240323155800_modify_response.rb new file mode 100644 index 000000000..211e87349 --- /dev/null +++ b/db/migrate/20240323155800_modify_response.rb @@ -0,0 +1,7 @@ +class ModifyResponse < ActiveRecord::Migration[7.0] + def change + add_reference :responses, :response_map, foreign_key: true + add_reference :responses, :question, foreign_key: true + add_column :responses, :submitted_answer, :string + end +end diff --git a/db/migrate/20240323155906_add_score_to_response_map.rb b/db/migrate/20240323155906_add_score_to_response_map.rb new file mode 100644 index 000000000..b3d514e8a --- /dev/null +++ b/db/migrate/20240323155906_add_score_to_response_map.rb @@ -0,0 +1,5 @@ +class AddScoreToResponseMap < ActiveRecord::Migration[7.0] + def change + add_column :response_maps, :score, :integer, default: 0 + end +end diff --git a/db/schema.rb b/db/schema.rb index 1770d8997..554874495 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -32,6 +32,8 @@ t.text "comments" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.text "answer_text" + t.boolean "correct", default: false t.index ["question_id"], name: "fk_score_questions" t.index ["response_id"], name: "fk_score_response" end @@ -205,6 +207,8 @@ t.text "instruction_loc" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.bigint "assignment_id", null: false + t.index ["assignment_id"], name: "index_questionnaires_on_assignment_id" end create_table "questions", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| @@ -220,6 +224,8 @@ t.datetime "created_at", null: false t.datetime "updated_at", null: false t.bigint "questionnaire_id", null: false + t.string "correct_answer" + t.integer "score_value" t.index ["questionnaire_id"], name: "fk_question_questionnaires" t.index ["questionnaire_id"], name: "index_questions_on_questionnaire_id" end @@ -230,6 +236,7 @@ t.integer "reviewee_id", default: 0, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.integer "score", default: 0 t.index ["reviewer_id"], name: "fk_response_map_reviewer" end @@ -239,7 +246,12 @@ t.boolean "is_submitted", default: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.bigint "response_map_id" + t.bigint "question_id" + t.string "submitted_answer" t.index ["map_id"], name: "fk_response_response_map" + t.index ["question_id"], name: "index_responses_on_question_id" + t.index ["response_map_id"], name: "index_responses_on_response_map_id" end create_table "roles", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| @@ -341,7 +353,10 @@ add_foreign_key "participants", "join_team_requests" add_foreign_key "participants", "teams" add_foreign_key "participants", "users" + add_foreign_key "questionnaires", "assignments" add_foreign_key "questions", "questionnaires" + add_foreign_key "responses", "questions" + add_foreign_key "responses", "response_maps" add_foreign_key "roles", "roles", column: "parent_id", on_delete: :cascade add_foreign_key "sign_up_topics", "assignments" add_foreign_key "signed_up_teams", "sign_up_topics" diff --git a/spec/models/assignment_spec.rb b/spec/models/assignment_spec.rb index d36f0d95a..a02bafacc 100644 --- a/spec/models/assignment_spec.rb +++ b/spec/models/assignment_spec.rb @@ -2,34 +2,74 @@ require 'rails_helper' RSpec.describe Assignment, type: :model do + let(:instructor) { create(:user) } + let(:course) { create(:course) } + let(:assignment) { + Assignment.create( + name: 'Test Assignment', + directory_path: '/path/to/assignment', + course_id: course.id, + instructor_id: instructor.id, + require_quiz: true, + num_quiz_questions: 5, + description: 'Assignment Description' + ) + } - let(:team) {Team.new} - let(:assignment) { Assignment.new(id: 1, name: 'Test Assignment') } - let(:review_response_map) { ReviewResponseMap.new(assignment: assignment, reviewee: team) } - let(:answer) { Answer.new(answer: 1, comments: 'Answer text', question_id: 1) } - let(:answer2) { Answer.new(answer: 1, comments: 'Answer text', question_id: 1) } + describe 'Model associations and validations' do + it 'belongs to course' do + expect(assignment).to belong_to(:course) + end + + it 'belongs to instructor' do + expect(assignment).to belong_to(:instructor) + end + + it 'validates presence of name' do + expect(assignment).to validate_presence_of(:name) + end + + it 'validates presence of directory_path' do + expect(assignment).to validate_presence_of(:directory_path) + end + + it 'validates presence of require_quiz' do + expect(assignment.require_quiz).to be true + end + + it 'validates numericality of num_quiz_questions' do + expect(assignment).to validate_numericality_of(:num_quiz_questions).is_greater_than_or_equal_to(0) + end + + it 'has a valid factory' do + expect(assignment).to be_valid + end + end describe '.get_all_review_comments' do + let(:team) { Team.new } + let(:review_response_map) { ReviewResponseMap.new(assignment: assignment, reviewee: team) } + let(:answer) { Answer.new(answer: 1, comments: 'Answer text', question_id: 1) } + let(:answer2) { Answer.new(answer: 1, comments: 'Answer text', question_id: 1) } + it 'returns concatenated review comments and # of reviews in each round' do - allow(Assignment).to receive(:find).with(1).and_return(assignment) - allow(assignment).to receive(:num_review_rounds).and_return(2) - allow(ReviewResponseMap).to receive_message_chain(:where, :find_each).with(reviewed_object_id: 1, reviewer_id: 1) + allow(ReviewResponseMap).to receive_message_chain(:where, :find_each).with(reviewed_object_id: assignment.id) .with(no_args).and_yield(review_response_map) response1 = double('Response', round: 1, additional_comment: '') response2 = double('Response', round: 2, additional_comment: 'LGTM') allow(review_response_map).to receive(:response).and_return([response1, response2]) allow(response1).to receive(:scores).and_return([answer]) allow(response2).to receive(:scores).and_return([answer2]) - expect(assignment.get_all_review_comments(1)).to eq([[nil, 'Answer text', 'Answer textLGTM', ''], [nil, 1, 1, 0]]) + + result = assignment.get_all_review_comments(1) + expect(result).to eq([[nil, 'Answer text', 'Answer textLGTM', ''], [nil, 1, 1, 0]]) end end - # Get a collection of all comments across all rounds of a review as well as a count of the total number of comments. Returns the above - # information both for totals and in a list per-round. describe '.volume_of_review_comments' do it 'returns volumes of review comments in each round' do allow(assignment).to receive(:get_all_review_comments).with(1) - .and_return([[nil, 'Answer text', 'Answer textLGTM', ''], [nil, 1, 1, 0]]) + .and_return([[nil, 'Answer text', 'Answer textLGTM', ''], [nil, 1, 1, 0]]) expect(assignment.volume_of_review_comments(1)).to eq([1, 2, 2, 0]) end end diff --git a/spec/requests/api/v1/participants_controller_spec.rb b/spec/requests/api/v1/participants_controller_spec.rb new file mode 100644 index 000000000..596141e6c --- /dev/null +++ b/spec/requests/api/v1/participants_controller_spec.rb @@ -0,0 +1,81 @@ +require 'rails_helper' + +RSpec.describe Api::V1::ParticipantsController, type: :request do + before(:each) do + @user = User.create!(username: 'testuser', email: 'test@example.com', password: 'password123', role_id: 2) + @course = Course.create!( + name: 'Intro to Testing', + subject: 'Software Engineering' + ) + @assignment = Assignment.create!(name: 'Test Assignment', + course_id: @course.id, + instructor_id: @user.id, + description: 'This is a test assignment.') + end + + describe 'POST /api/v1/participants' do + context 'when the user and assignment exist' do + let(:valid_attributes) do + { + participant: { + user_id: @user.id, + assignment_id: @assignment.id + } + } + end + + it 'creates a new Participant' do + expect { + post '/api/v1/participants', params: valid_attributes + }.to change(Participant, :count).by(1) + + expect(response).to have_http_status(:created) + end + end + + context 'when the user does not exist' do + let(:invalid_attributes) do + { + participant: { + user_id: nil, + assignment_id: @assignment.id + } + } + end + + it 'does not create a participant and returns a not found status' do + expect { + post '/api/v1/participants', params: invalid_attributes + }.not_to change(Participant, :count) + + expect(response).to have_http_status(:not_found) + expect(response.body).to include('User does not exist') + end + end + + context 'when the assignment does not exist' do + let(:invalid_attributes) do + { + participant: { + user_id: @user.id, + assignment_id: nil + } + } + end + + it 'does not create a participant and returns a not found status' do + expect { + post '/api/v1/participants', params: invalid_attributes + }.not_to change(Participant, :count) + + expect(response).to have_http_status(:not_found) + expect(response.body).to include('Assignment does not exist') + end + end + end + + after(:each) do + User.delete_all + Assignment.delete_all + end +end diff --git a/spec/requests/api/v1/student_quizzes_controller_spec.rb b/spec/requests/api/v1/student_quizzes_controller_spec.rb new file mode 100644 index 000000000..49252b6ca --- /dev/null +++ b/spec/requests/api/v1/student_quizzes_controller_spec.rb @@ -0,0 +1,150 @@ +require 'swagger_helper' + +RSpec.describe 'StudentQuizzes API', type: :request do + let!(:role) { create(:role, name: "Instructor") } # Creating a role with the name "Instructor" + let!(:instructor) { create(:user, role: role) } # Creating a user with the role created above + let!(:student) { create(:user, role:'Student') } + let!(:questionnaire) { create(:questionnaire, instructor_id: instructor.id) } + let(:questionnaire_id) { questionnaire.id } + let(:valid_attributes) do + { + questionnaire: { + name: 'Quiz 1', + instructor_id: instructor.id, + min_question_score: 1, + max_question_score: 5, + assignment_id: create(:assignment).id, + questions_attributes: [ + { + txt: 'What is Ruby?', + question_type: 'text', + break_before: true, + correct_answer: 'Programming Language', + score_value: 1, + answers_attributes: [ + { answer_text: 'Programming Language', correct: true }, + { answer_text: 'A Gem', correct: false } + ] + } + ] + } + } + end + let(:invalid_attributes) do + { questionnaire: { name: '' } } + end + + 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 + + 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 + + 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) + end + end + + describe 'POST /api/v1/student_quizzes/assign' do + let(:assign_attributes) do + { participant_id: student.id, questionnaire_id: questionnaire_id } + end + + 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 + + describe 'POST /api/v1/student_quizzes/submit_answers' do + let(:submit_attributes) do + { + questionnaire_id: questionnaire_id, + answers: [ + { question_id: create(:question, questionnaire: questionnaire).id, answer_value: 'Programming Language' } + ] + } + end + + 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) + end + end + + + describe 'PUT /api/v1/student_quizzes/{id}' do + let(:valid_attributes_update) do + { + questionnaire: { + name: 'Updated Quiz Name', + } + } + end + + 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 + + context 'when the record does not exist' do + before { put "/api/v1/student_quizzes/#{questionnaire_id + 100}", params: valid_attributes_update } # Assuming an ID that does not exist + + it 'returns status code 404' do + expect(response).to have_http_status(:not_found) + end + end + + 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 + end + + describe 'DELETE /api/v1/student_quizzes/{id}' do + let!(:quiz_to_delete) { create(:questionnaire, instructor_id: instructor.id) } + + context 'when the record exists' do + it 'deletes the record' do + expect { + delete "/api/v1/student_quizzes/#{quiz_to_delete.id}" + }.to change(Questionnaire, :count).by(-1) + expect(response).to have_http_status(:no_content) + end + end + + context 'when the record does not exist' do + before { delete "/api/v1/student_quizzes/#{quiz_to_delete.id + 100}" } # Assuming an ID that does not exist + + it 'returns status code 404' do + expect(response).to have_http_status(:not_found) + end + end + end +end diff --git a/swagger/v1/swagger.yaml b/swagger/v1/swagger.yaml index f23cb7b20..ca20a34ee 100644 --- a/swagger/v1/swagger.yaml +++ b/swagger/v1/swagger.yaml @@ -1279,6 +1279,266 @@ paths: description: signed up team deleted '422': description: invalid request + # Create - Create a new questionnaire with questions and answers + "/api/v1/student_quizzes": + post: + summary: Create a new questionnaire with questions and answers + tags: + - Student Quizzes + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + questionnaire: + type: object + properties: + name: + type: string + instructor_id: + type: integer + min_question_score: + type: integer + max_question_score: + type: integer + assignment_id: + type: integer + questions_attributes: + type: array + items: + type: object + properties: + txt: + type: string + question_type: + type: string + break_before: + type: boolean + correct_answer: + type: string + score_value: + type: integer + answers_attributes: + type: array + items: + type: object + properties: + answer_text: + type: string + correct: + type: boolean + required: + - txt + - question_type + - break_before + - correct_answer + - score_value + - answers_attributes + required: + - name + - instructor_id + - min_question_score + - max_question_score + - assignment_id + - questions_attributes + responses: + '201': + description: Questionnaire created successfully + '400': + description: Bad request + get: + summary: Lists all questionnaires (quizzes) + tags: + - Student Quizzes + responses: + '200': + description: A list of all quizzes + "/api/v1/student_quizzes/{quiz_id}/calculate_score": + get: + summary: Calculate and retrieve the score for a specific student quiz + tags: + - Student Quizzes + parameters: + - name: quiz_id + in: path + description: ID of the student quiz to calculate the score for + required: true + schema: + type: integer + responses: + '200': + description: The calculated score for the specified student quiz + '404': + description: Student quiz not found or permission denied to view the score + "/api/v1/student_quizzes/assign": + post: + summary: Assign a quiz to a student + tags: + - Student Quizzes + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + participant_id: + type: string + description: The ID of the participant to whom the quiz is assigned + questionnaire_id: + type: string + description: The ID of the questionnaire (quiz) to be assigned + required: + - participant_id + - questionnaire_id + responses: + '201': + description: Quiz successfully assigned to the student + '422': + description: Unable to assign the quiz (e.g., quiz already assigned or validation errors) + "/api/v1/student_quizzes/submit_answers": + post: + summary: Submit answers for a quiz and calculate the total score + tags: + - Student Quizzes + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + questionnaire_id: + type: integer + description: The ID of the questionnaire for which answers are being submitted + answers: + type: array + items: + type: object + properties: + question_id: + type: integer + description: The ID of the question to which the answer corresponds + answer_value: + type: string + description: The value of the submitted answer + required: + - question_id + - answer_value + required: + - questionnaire_id + - answers + responses: + '200': + description: Answers submitted successfully and total score calculated + '403': + description: Forbidden action, such as trying to submit answers for a quiz not assigned to the user + '422': + description: Unprocessable entity, such as validation errors in the submitted answers + "/api/v1/student_quizzes/{id}": + put: + summary: Update an existing student quiz + tags: + - Student Quizzes + parameters: + - name: id + in: path + description: The ID of the student quiz to update + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + questionnaire: + type: object + properties: + name: + type: string + instructor_id: + type: integer + min_question_score: + type: integer + max_question_score: + type: integer + assignment_id: + type: integer + questions_attributes: + type: array + items: + type: object + properties: + id: + type: integer + txt: + type: string + question_type: + type: string + break_before: + type: boolean + correct_answer: + type: string + score_value: + type: integer + answers_attributes: + type: array + items: + type: object + properties: + id: + type: integer + answer_text: + type: string + correct: + type: boolean + required: + - name + - instructor_id + - min_question_score + - max_question_score + - assignment_id + - questions_attributes + responses: + '200': + description: Student quiz successfully updated + '422': + description: Validation error with provided data + delete: + summary: Delete a specific student quiz + tags: + - Student Quizzes + parameters: + - name: id + in: path + description: The ID of the student quiz to delete + required: true + schema: + type: integer + responses: + '204': + description: Student quiz successfully deleted, no content returned + '404': + description: Student quiz not found + get: + summary: Retrieve a specific student quiz + tags: + - Student Quizzes + parameters: + - name: id + in: path + description: ID of the student quiz to retrieve + required: true + schema: + type: integer + responses: + '200': + description: Details of the specified student quiz "/login": post: summary: Logs in a user From 36a79e698474217823080adab3a83b4deceb2a3b Mon Sep 17 00:00:00 2001 From: neerua08 Date: Tue, 3 Dec 2024 16:38:19 -0500 Subject: [PATCH 02/19] Deployed app --- config/database.yml | 47 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/config/database.yml b/config/database.yml index b9f5aa055..f42127b82 100644 --- a/config/database.yml +++ b/config/database.yml @@ -1,18 +1,55 @@ +# MySQL. Versions 5.5.8 and up are supported. +# +# Install the MySQL driver +# gem install mysql2 +# +# Ensure the MySQL gem is defined in your Gemfile +# gem "mysql2" +# +# And be sure to use new-style password hashing: +# https://dev.mysql.com/doc/refman/5.7/en/password-hashing.html +# default: &default adapter: mysql2 encoding: utf8mb4 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> - port: 3306 - socket: /var/run/mysqld/mysqld.sock + username: dev + password: root + development: <<: *default - url: <%= ENV['DATABASE_URL'].gsub('?', '_development?') %> + database: reimplementation_development +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. test: <<: *default - url: <%= ENV['DATABASE_URL'].gsub('?', '_test?') %> + database: reimplementation_test +# As with config/credentials.yml, you never want to store sensitive information, +# like your database password, in your source code. If your source code is +# ever seen by anyone, they now have access to your database. +# +# Instead, provide the password or a full connection URL as an environment +# variable when you boot the app. For example: +# +# DATABASE_URL="mysql2://myuser:mypass@localhost/somedatabase" +# +# If the connection URL is provided in the special DATABASE_URL environment +# variable, Rails will automatically merge its configuration values on top of +# the values provided in this file. Alternatively, you can specify a connection +# URL environment variable explicitly: +# +# production: +# url: <%= ENV["MY_APP_DATABASE_URL"] %> +# +# Read https://guides.rubyonrails.org/configuring.html#configuring-a-database +# for a full overview on how database connection configuration can be specified. +# production: <<: *default - url: <%= ENV['DATABASE_URL'].gsub('?', '_production?') %> \ No newline at end of file + database: reimplementation_production + username: reimplementation + password: <%= ENV["REIMPLEMENTATION_DATABASE_PASSWORD"] %> \ No newline at end of file From c1dd8c817d842df0817bde8fa408d760fb0a6fdd Mon Sep 17 00:00:00 2001 From: neerua08 Date: Tue, 3 Dec 2024 16:44:12 -0500 Subject: [PATCH 03/19] Moved calculate score to Response map --- app/controllers/api/v1/student_quizzes_controller.rb | 2 +- app/models/response_map.rb | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/controllers/api/v1/student_quizzes_controller.rb b/app/controllers/api/v1/student_quizzes_controller.rb index dc5519cb9..2c419a50b 100644 --- a/app/controllers/api/v1/student_quizzes_controller.rb +++ b/app/controllers/api/v1/student_quizzes_controller.rb @@ -21,7 +21,7 @@ def show def calculate_score response_map = ResponseMap.find_by(id: params[:id]) if response_map - render_success({ score: response_map.score }) + response_map.calculate_score else render_error('Attempt not found or you do not have permission to view this score.', :not_found) end diff --git a/app/models/response_map.rb b/app/models/response_map.rb index d0a2743d7..0eb4270dd 100644 --- a/app/models/response_map.rb +++ b/app/models/response_map.rb @@ -6,4 +6,8 @@ class ResponseMap < ApplicationRecord has_many :responses validates :reviewee_id, uniqueness: { scope: :reviewed_object_id, message: "is already assigned to this questionnaire" } + + def calculate_score + render_success({ score: self.score }) + end end \ No newline at end of file From dacecb623bbfa8cfff89a3c6deaa8affd8eaf0f5 Mon Sep 17 00:00:00 2001 From: neerua08 Date: Tue, 3 Dec 2024 16:53:54 -0500 Subject: [PATCH 04/19] Moved process_answer to response_map --- .../api/v1/student_quizzes_controller.rb | 11 +---------- app/models/response_map.rb | 13 +++++++++++++ 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/app/controllers/api/v1/student_quizzes_controller.rb b/app/controllers/api/v1/student_quizzes_controller.rb index 2c419a50b..42d6efbfd 100644 --- a/app/controllers/api/v1/student_quizzes_controller.rb +++ b/app/controllers/api/v1/student_quizzes_controller.rb @@ -109,16 +109,7 @@ def find_response_map_for_current_user # Process and calculate the total score for submitted answers def process_answers(answers, response_map) - answers.sum do |answer| - question = Question.find(answer[:question_id]) - submitted_answer = answer[:answer_value] - - response = find_or_initialize_response(response_map.id, question.id) - response.submitted_answer = submitted_answer - response.save! - - question.correct_answer == submitted_answer ? question.score_value : 0 - end + response_map.process_answers(answers) end # Find or initialize a response for a specific question within an attempt diff --git a/app/models/response_map.rb b/app/models/response_map.rb index 0eb4270dd..a48b32ba7 100644 --- a/app/models/response_map.rb +++ b/app/models/response_map.rb @@ -10,4 +10,17 @@ class ResponseMap < ApplicationRecord def calculate_score render_success({ score: self.score }) end + + def process_answers(answers) + answers.sum do |answer| + question = Question.find(answer[:question_id]) + submitted_answer = answer[:answer_value] + + response = find_or_initialize_response(self.id, question.id) + response.submitted_answer = submitted_answer + response.save! + + question.correct_answer == submitted_answer ? question.score_value : 0 + end + end end \ No newline at end of file From 55d860747ff08506aea134e182d98119eadc58e9 Mon Sep 17 00:00:00 2001 From: neerua08 Date: Tue, 3 Dec 2024 17:13:20 -0500 Subject: [PATCH 05/19] Call the method in role for srp --- app/controllers/api/v1/student_quizzes_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/v1/student_quizzes_controller.rb b/app/controllers/api/v1/student_quizzes_controller.rb index 42d6efbfd..eeb7f4b72 100644 --- a/app/controllers/api/v1/student_quizzes_controller.rb +++ b/app/controllers/api/v1/student_quizzes_controller.rb @@ -182,7 +182,7 @@ def render_error(message, status = :unprocessable_entity) # Ensure only instructors can perform certain actions def check_instructor_role - unless current_user.role_id == 2 + unless current_user.role.instructor? render_error('Only instructors are allowed to perform this action', :forbidden) end end From 0dc53dd139caa7d18d380c98d51578d98911ef3b Mon Sep 17 00:00:00 2001 From: neerua08 Date: Tue, 3 Dec 2024 17:22:41 -0500 Subject: [PATCH 06/19] Fixed DRY for finding resource --- .../api/v1/student_quizzes_controller.rb | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/app/controllers/api/v1/student_quizzes_controller.rb b/app/controllers/api/v1/student_quizzes_controller.rb index eeb7f4b72..7bc5f97ac 100644 --- a/app/controllers/api/v1/student_quizzes_controller.rb +++ b/app/controllers/api/v1/student_quizzes_controller.rb @@ -2,6 +2,8 @@ class Api::V1::StudentQuizzesController < ApplicationController before_action :check_instructor_role, except: [:submit_answers] before_action :set_student_quiz, only: [:show, :update, :destroy] + include ResourceFinder + rescue_from ActiveRecord::RecordInvalid do |exception| render_error(exception.message) end @@ -120,14 +122,6 @@ def find_or_initialize_response(response_map_id, question_id) ) end - # Find a specific resource by ID, handling the case where it's not found - def find_resource_by_id(model, id) - model.find(id) - rescue ActiveRecord::RecordNotFound - render_error("#{model.name} not found", :not_found) - nil - end - # Check if a quiz has already been assigned to a participant def quiz_already_assigned?(participant, questionnaire) ResponseMap.exists?( @@ -186,4 +180,14 @@ def check_instructor_role render_error('Only instructors are allowed to perform this action', :forbidden) end end + + # Find a specific resource by ID, handling the case where it's not found + module ResourceFinder + def find_resource_by_id(model, id) + model.find(id) + rescue ActiveRecord::RecordNotFound + render_error("#{model.name} not found", :not_found) + nil + end + end end \ No newline at end of file From b20ff7d20c6897219a59624a2c16d9c3d5ff6fd9 Mon Sep 17 00:00:00 2001 From: neerua08 Date: Tue, 3 Dec 2024 17:37:59 -0500 Subject: [PATCH 07/19] Refactored student_quiz names --- app/controllers/api/v1/student_quizzes_controller.rb | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/controllers/api/v1/student_quizzes_controller.rb b/app/controllers/api/v1/student_quizzes_controller.rb index 7bc5f97ac..db570f075 100644 --- a/app/controllers/api/v1/student_quizzes_controller.rb +++ b/app/controllers/api/v1/student_quizzes_controller.rb @@ -1,5 +1,5 @@ class Api::V1::StudentQuizzesController < ApplicationController - before_action :check_instructor_role, except: [:submit_answers] + before_action :check_instructor_role, except: [:grade_submitted_answers] before_action :set_student_quiz, only: [:show, :update, :destroy] include ResourceFinder @@ -42,6 +42,7 @@ def create end #POST /student_quizzes/assign + # Assigns a specific quiz to a student def assign_quiz_to_student participant = find_resource_by_id(Participant, params[:participant_id]) questionnaire = find_resource_by_id(Questionnaire, params[:questionnaire_id]) @@ -61,7 +62,7 @@ def assign_quiz_to_student end #POST /student_quizzes/submit_answers - def submit_answers + def grade_submitted_answers ActiveRecord::Base.transaction do response_map = find_response_map_for_current_user unless response_map @@ -183,10 +184,10 @@ def check_instructor_role # Find a specific resource by ID, handling the case where it's not found module ResourceFinder - def find_resource_by_id(model, id) - model.find(id) + def find_resource_by_id(resource, id) + resource.find(id) rescue ActiveRecord::RecordNotFound - render_error("#{model.name} not found", :not_found) + render_error("#{resource.name} not found", :not_found) nil end end From 865857ea2dcc0eb73d87cff87e1610bf8ba6d142 Mon Sep 17 00:00:00 2001 From: Krishna Pallavalli Date: Tue, 3 Dec 2024 19:56:05 -0500 Subject: [PATCH 08/19] Refactored assign_quiz_to_student --- 2 | 0 app/controllers/api/v1/student_quizzes_controller.rb | 12 ++++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 2 diff --git a/2 b/2 new file mode 100644 index 000000000..e69de29bb diff --git a/app/controllers/api/v1/student_quizzes_controller.rb b/app/controllers/api/v1/student_quizzes_controller.rb index dc5519cb9..09c73af33 100644 --- a/app/controllers/api/v1/student_quizzes_controller.rb +++ b/app/controllers/api/v1/student_quizzes_controller.rb @@ -41,22 +41,30 @@ def create #POST /student_quizzes/assign def assign_quiz_to_student + # Retrieve the participant and questionnaire using their respective IDs participant = find_resource_by_id(Participant, params[:participant_id]) questionnaire = find_resource_by_id(Questionnaire, params[:questionnaire_id]) + + # Stop execution if either the participant or questionnaire does not exist return unless participant && questionnaire - + + # Check if the quiz has already been assigned to this student if quiz_already_assigned?(participant, questionnaire) render_error("This student is already assigned to the quiz.", :unprocessable_entity) return end - + + # Create a new response map to link the student and the quiz response_map = build_response_map(participant.user_id, questionnaire) + + # Attempt to save the response map and render appropriate success or error messages if response_map.save render_success(response_map, :created) else render_error(response_map.errors.full_messages.to_sentence, :unprocessable_entity) end end + #POST /student_quizzes/submit_answers def submit_answers From e0e33dcb816d6036722727cf4561559a06e950b7 Mon Sep 17 00:00:00 2001 From: Aarya Rajoju Date: Tue, 3 Dec 2024 20:15:49 -0500 Subject: [PATCH 09/19] updated the db seed file to create more users --- db/seeds.rb | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/db/seeds.rb b/db/seeds.rb index 134ac82e5..08ac4d0d8 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -13,6 +13,26 @@ institution_id: 1, role_id: 1 ) + + User.create!( + name: 'instructor', + email: 'instructor@example.com', + password: 'password123', + full_name: 'instructor instructor', + institution_id: 1, + role_id: 3 + ) + + User.create!( + name: 'student', + email: 'student@example.com', + password: 'password123', + full_name: 'student student', + institution_id: 5, + role_id: 5 + ) + + rescue ActiveRecord::RecordInvalid => e puts 'The db has already been seeded' -end \ No newline at end of file +end From e17e80af86877b4194ce819e5e2966a4257bde4e Mon Sep 17 00:00:00 2001 From: Krishna Pallavalli <32885657+KrishnaPallavalli@users.noreply.github.com> Date: Tue, 3 Dec 2024 23:39:55 -0500 Subject: [PATCH 10/19] Delete 2 --- 2 | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 2 diff --git a/2 b/2 deleted file mode 100644 index e69de29bb..000000000 From 9b3c0342c6a407766e38ea2cb860434cbf10efcd Mon Sep 17 00:00:00 2001 From: neerua08 Date: Wed, 4 Dec 2024 00:13:24 -0500 Subject: [PATCH 11/19] Moved methods and removed unnecessary methods --- .../api/v1/student_quizzes_controller.rb | 32 ++-------- app/models/response_map.rb | 62 +++++++++++-------- 2 files changed, 42 insertions(+), 52 deletions(-) diff --git a/app/controllers/api/v1/student_quizzes_controller.rb b/app/controllers/api/v1/student_quizzes_controller.rb index 7ca13f268..ccc31285f 100644 --- a/app/controllers/api/v1/student_quizzes_controller.rb +++ b/app/controllers/api/v1/student_quizzes_controller.rb @@ -2,8 +2,6 @@ class Api::V1::StudentQuizzesController < ApplicationController before_action :check_instructor_role, except: [:grade_submitted_answers] before_action :set_student_quiz, only: [:show, :update, :destroy] - include ResourceFinder - rescue_from ActiveRecord::RecordInvalid do |exception| render_error(exception.message) end @@ -32,7 +30,7 @@ def calculate_score #POST /student_quizzes def create questionnaire = ActiveRecord::Base.transaction do - questionnaire = create_questionnaire(questionnaire_params.except(:questions_attributes)) + questionnaire = Questionnaire.create!(questionnaire_params.except(:questions_attributes)) create_questions_and_answers(questionnaire, questionnaire_params[:questions_attributes]) questionnaire end @@ -45,8 +43,8 @@ def create # Assigns a specific quiz to a student def assign_quiz_to_student # Retrieve the participant and questionnaire using their respective IDs - participant = find_resource_by_id(Participant, params[:participant_id]) - questionnaire = find_resource_by_id(Questionnaire, params[:questionnaire_id]) + participant = ResourceFinder.find_resource_by_id(Participant, params[:participant_id]) + questionnaire = ResourceFinder.find_resource_by_id(Questionnaire, params[:questionnaire_id]) # Stop execution if either the participant or questionnaire does not exist return unless participant && questionnaire @@ -58,7 +56,7 @@ def assign_quiz_to_student end # Create a new response map to link the student and the quiz - response_map = build_response_map(participant.user_id, questionnaire) + response_map = ResponseMap.build_response_map(participant.user_id, questionnaire) # Attempt to save the response map and render appropriate success or error messages if response_map.save @@ -107,7 +105,7 @@ def destroy #To get quiz from db def set_student_quiz - @student_quiz = find_resource_by_id(Questionnaire, params[:id]) + @student_quiz = ResourceFinder.find_resource_by_id(Questionnaire, params[:id]) end # Find the response map for the current user's attempt to submit quiz answers @@ -118,11 +116,6 @@ def find_response_map_for_current_user ) end - # Process and calculate the total score for submitted answers - def process_answers(answers, response_map) - response_map.process_answers(answers) - end - # Find or initialize a response for a specific question within an attempt def find_or_initialize_response(response_map_id, question_id) Response.find_or_initialize_by( @@ -139,21 +132,6 @@ def quiz_already_assigned?(participant, questionnaire) ) end - # Build a new ResponseMap instance for assigning a quiz to a student - def build_response_map(student_id, questionnaire) - instructor_id = questionnaire.assignment.instructor_id - ResponseMap.new( - reviewee_id: student_id, - reviewer_id: instructor_id, - reviewed_object_id: questionnaire.id - ) - end - - # Create a new questionnaire along with its questions and answers - def create_questionnaire(params) - Questionnaire.create!(params) - end - # Create questions and their respective answers for a questionnaire def create_questions_and_answers(questionnaire, questions_attributes) questions_attributes.each do |question_attr| diff --git a/app/models/response_map.rb b/app/models/response_map.rb index a48b32ba7..1eeacf96b 100644 --- a/app/models/response_map.rb +++ b/app/models/response_map.rb @@ -1,26 +1,38 @@ -class ResponseMap < ApplicationRecord - # 'reviewer_id' points to the User who is the instructor. - belongs_to :reviewer, class_name: 'User', foreign_key: 'reviewer_id', optional: true - belongs_to :reviewee, class_name: 'User', foreign_key: 'reviewee_id', optional: true - belongs_to :questionnaire, foreign_key: 'reviewed_object_id', optional: true - has_many :responses - validates :reviewee_id, uniqueness: { scope: :reviewed_object_id, - message: "is already assigned to this questionnaire" } - - def calculate_score - render_success({ score: self.score }) - end - - def process_answers(answers) - answers.sum do |answer| - question = Question.find(answer[:question_id]) - submitted_answer = answer[:answer_value] - - response = find_or_initialize_response(self.id, question.id) - response.submitted_answer = submitted_answer - response.save! - - question.correct_answer == submitted_answer ? question.score_value : 0 - end - end +class ResponseMap < ApplicationRecord + # 'reviewer_id' points to the User who is the instructor. + belongs_to :reviewer, class_name: 'User', foreign_key: 'reviewer_id', optional: true + belongs_to :reviewee, class_name: 'User', foreign_key: 'reviewee_id', optional: true + belongs_to :questionnaire, foreign_key: 'reviewed_object_id', optional: true + has_many :responses + validates :reviewee_id, uniqueness: { scope: :reviewed_object_id, + message: "is already assigned to this questionnaire" } + + # Gets the score from this response map + def calculate_score + render_success({ score: self.score }) + end + + # Save the submitted answers and check if that answer is correct + def process_answers(answers) + answers.sum do |answer| + question = Question.find(answer[:question_id]) + submitted_answer = answer[:answer_value] + + response = find_or_initialize_response(self.id, question.id) + response.submitted_answer = submitted_answer + response.save! + + question.correct_answer == submitted_answer ? question.score_value : 0 + end + end + + # Build a new ResponseMap instance for assigning a quiz to a student + def build_response_map(student_id, questionnaire) + instructor_id = questionnaire.assignment.instructor_id + ResponseMap.new( + reviewee_id: student_id, + reviewer_id: instructor_id, + reviewed_object_id: questionnaire.id + ) + end end \ No newline at end of file From c0198db6dc98e3cdc8011a476520f0005ac30f6c Mon Sep 17 00:00:00 2001 From: neerua08 Date: Wed, 4 Dec 2024 00:29:19 -0500 Subject: [PATCH 12/19] Added concern for find_resource_by_id --- .../api/v1/student_quizzes_controller.rb | 17 ++++------------- app/controllers/concerns/resource_finder.rb | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 13 deletions(-) create mode 100644 app/controllers/concerns/resource_finder.rb diff --git a/app/controllers/api/v1/student_quizzes_controller.rb b/app/controllers/api/v1/student_quizzes_controller.rb index ccc31285f..52e4d48c1 100644 --- a/app/controllers/api/v1/student_quizzes_controller.rb +++ b/app/controllers/api/v1/student_quizzes_controller.rb @@ -1,4 +1,5 @@ class Api::V1::StudentQuizzesController < ApplicationController + include ResourceFinder before_action :check_instructor_role, except: [:grade_submitted_answers] before_action :set_student_quiz, only: [:show, :update, :destroy] @@ -43,8 +44,8 @@ def create # Assigns a specific quiz to a student def assign_quiz_to_student # Retrieve the participant and questionnaire using their respective IDs - participant = ResourceFinder.find_resource_by_id(Participant, params[:participant_id]) - questionnaire = ResourceFinder.find_resource_by_id(Questionnaire, params[:questionnaire_id]) + participant = find_resource_by_id(Participant, params[:participant_id]) + questionnaire = find_resource_by_id(Questionnaire, params[:questionnaire_id]) # Stop execution if either the participant or questionnaire does not exist return unless participant && questionnaire @@ -105,7 +106,7 @@ def destroy #To get quiz from db def set_student_quiz - @student_quiz = ResourceFinder.find_resource_by_id(Questionnaire, params[:id]) + @student_quiz = find_resource_by_id(Questionnaire, params[:id]) end # Find the response map for the current user's attempt to submit quiz answers @@ -167,14 +168,4 @@ def check_instructor_role render_error('Only instructors are allowed to perform this action', :forbidden) end end - - # Find a specific resource by ID, handling the case where it's not found - module ResourceFinder - def find_resource_by_id(resource, id) - resource.find(id) - rescue ActiveRecord::RecordNotFound - render_error("#{resource.name} not found", :not_found) - nil - end - end end \ No newline at end of file diff --git a/app/controllers/concerns/resource_finder.rb b/app/controllers/concerns/resource_finder.rb new file mode 100644 index 000000000..6e5da11fe --- /dev/null +++ b/app/controllers/concerns/resource_finder.rb @@ -0,0 +1,14 @@ +module ResourceFinder + extend ActiveSupport::Concern + + included do + + # Find a specific resource by ID, handling the case where it's not found + def find_resource_by_id(resource, id) + resource.find(id) + rescue ActiveRecord::RecordNotFound + render_error("#{resource.name} not found", :not_found) + nil + end + end +end \ No newline at end of file From fcf7d5f81093dd068611c5953da86a9a46d3709a Mon Sep 17 00:00:00 2001 From: neerua08 Date: Sun, 8 Dec 2024 18:09:00 -0500 Subject: [PATCH 13/19] Updated seed.rb --- db/seeds.rb | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/db/seeds.rb b/db/seeds.rb index 08ac4d0d8..4e21655fb 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,5 +1,5 @@ begin - #Create an instritution + #Create an institution Institution.create!( name: 'North Carolina State University', ) @@ -32,6 +32,33 @@ role_id: 5 ) + Questionnaire.create!( + name: 'Questionnaire', + instructor_id: 2, + min_question_score: 0, + max_question_score: 10, + private: false, + questionnaire_type: 'AuthorFeedbackReview' + ) + + Question.create!( + seq: 1, + txt: "test question 1", + question_type: "multiple_choice", + break_before: true, + weight: 5, + questionnaire_id: 1 + ) + + Question.create!( + seq: 2, + txt: "test question 2", + question_type: "multiple_choice", + break_before: false, + weight: 5, + questionnaire_id: 1 + ) + rescue ActiveRecord::RecordInvalid => e puts 'The db has already been seeded' From d8ad7b3f7054eba00e7d49102ba134a40b4cace8 Mon Sep 17 00:00:00 2001 From: neerua08 Date: Sun, 8 Dec 2024 18:49:39 -0500 Subject: [PATCH 14/19] Updated seed.rb with more questions --- db/seeds.rb | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/db/seeds.rb b/db/seeds.rb index 4e21655fb..163764115 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -59,6 +59,33 @@ questionnaire_id: 1 ) + Questionnaire.create!( + name: 'Questionnaire 2', + instructor_id: 2, + min_question_score: 0, + max_question_score: 10, + private: false, + questionnaire_type: 'AuthorFeedbackReview' + ) + + Question.create!( + seq: 1, + txt: "test question 1 for 2", + question_type: "multiple_choice", + break_before: true, + weight: 5, + questionnaire_id: 2 + ) + + Question.create!( + seq: 2, + txt: "test question 2 for 2", + question_type: "multiple_choice", + break_before: false, + weight: 5, + questionnaire_id: 2 + ) + rescue ActiveRecord::RecordInvalid => e puts 'The db has already been seeded' From 04c1de4eff5ce6f9c98d5a744abf164c08736bbe Mon Sep 17 00:00:00 2001 From: Aarya Rajoju Date: Mon, 9 Dec 2024 08:29:26 -0500 Subject: [PATCH 15/19] made some edits to controller - postman runs fine; added things for skippable questions - yet to finish --- .../api/v1/student_quizzes_controller.rb | 97 ++++++++++++------- app/controllers/application_controller.rb | 17 +++- app/models/question.rb | 2 +- app/models/response_map.rb | 15 ++- app/models/role.rb | 2 +- config/routes.rb | 1 + ...20241204052902_add_skipped_to_responses.rb | 5 + ...241204052911_add_skippable_to_questions.rb | 5 + 8 files changed, 101 insertions(+), 43 deletions(-) create mode 100644 db/migrate/20241204052902_add_skipped_to_responses.rb create mode 100644 db/migrate/20241204052911_add_skippable_to_questions.rb diff --git a/app/controllers/api/v1/student_quizzes_controller.rb b/app/controllers/api/v1/student_quizzes_controller.rb index 52e4d48c1..bc7f6221f 100644 --- a/app/controllers/api/v1/student_quizzes_controller.rb +++ b/app/controllers/api/v1/student_quizzes_controller.rb @@ -1,37 +1,41 @@ class Api::V1::StudentQuizzesController < ApplicationController - include ResourceFinder - before_action :check_instructor_role, except: [:grade_submitted_answers] + before_action :check_instructor_role, except: [:submit_quiz] before_action :set_student_quiz, only: [:show, :update, :destroy] + # Rescue from ActiveRecord::RecordInvalid exceptions and render an error response rescue_from ActiveRecord::RecordInvalid do |exception| render_error(exception.message) end - #GET /student_quizzes + # GET /student_quizzes + # Fetch and render all quizzes def index quizzes = Questionnaire.all render_success(quizzes) end - #GET /student_quizzes/:id + # GET /student_quizzes/:id + # Fetch and render a specific quiz by ID def show render_success(@student_quiz) end - #GET /student_quizzes/:id/calculate_score + # GET /student_quizzes/:id/calculate_score + # Calculate and render the score for a specific quiz attempt def calculate_score response_map = ResponseMap.find_by(id: params[:id]) if response_map - response_map.calculate_score + render_success({ score: response_map.calculate_score }) else render_error('Attempt not found or you do not have permission to view this score.', :not_found) end end - #POST /student_quizzes + # POST /student_quizzes + # Create a new quiz with associated questions and answers def create questionnaire = ActiveRecord::Base.transaction do - questionnaire = Questionnaire.create!(questionnaire_params.except(:questions_attributes)) + questionnaire = create_questionnaire(questionnaire_params.except(:questions_attributes)) create_questions_and_answers(questionnaire, questionnaire_params[:questions_attributes]) questionnaire end @@ -40,22 +44,28 @@ def create render_error(e.message, :unprocessable_entity) end - #POST /student_quizzes/assign - # Assigns a specific quiz to a student - def assign_quiz_to_student - # Retrieve the participant and questionnaire using their respective IDs + # Helper method to create a questionnaire + # @param params [Hash] the parameters for creating a questionnaire + # @return [Questionnaire] the created questionnaire + def create_questionnaire(params) + Questionnaire.create!(params) + end + + # POST /student_quizzes/assign + # Assign a quiz to a student + def assign_quiz participant = find_resource_by_id(Participant, params[:participant_id]) questionnaire = find_resource_by_id(Questionnaire, params[:questionnaire_id]) # Stop execution if either the participant or questionnaire does not exist return unless participant && questionnaire - + # Check if the quiz has already been assigned to this student if quiz_already_assigned?(participant, questionnaire) render_error("This student is already assigned to the quiz.", :unprocessable_entity) return end - + # Create a new response map to link the student and the quiz response_map = ResponseMap.build_response_map(participant.user_id, questionnaire) @@ -66,10 +76,10 @@ def assign_quiz_to_student render_error(response_map.errors.full_messages.to_sentence, :unprocessable_entity) end end - - #POST /student_quizzes/submit_answers - def grade_submitted_answers + # POST /student_quizzes/submit_answers + # Submit answers for a quiz and calculate the total score + def submit_quiz ActiveRecord::Base.transaction do response_map = find_response_map_for_current_user unless response_map @@ -77,7 +87,7 @@ def grade_submitted_answers return end - total_score = process_answers(params[:answers], response_map) + total_score = response_map.process_answers(params[:answers]) response_map.update!(score: total_score) render_success({ total_score: total_score }) end @@ -85,7 +95,8 @@ def grade_submitted_answers render_error(e.message, :unprocessable_entity) end - #PUT /student_quizzes/:id + # PUT /student_quizzes/:id + # Update a specific quiz by ID def update if @student_quiz.update(questionnaire_params) render_success(@student_quiz) @@ -94,7 +105,8 @@ def update end end - #DELETE /student_quizzes/:id + # DELETE /student_quizzes/:id + # Delete a specific quiz by ID def destroy @student_quiz.destroy head :no_content @@ -104,36 +116,41 @@ def destroy private - #To get quiz from db + # Fetch and set the quiz from the database + # @return [void] def set_student_quiz @student_quiz = find_resource_by_id(Questionnaire, params[:id]) end # Find the response map for the current user's attempt to submit quiz answers + # @return [ResponseMap, nil] the response map if found, otherwise nil def find_response_map_for_current_user - ResponseMap.find_by( - reviewee_id: current_user.id, - reviewed_object_id: params[:questionnaire_id] - ) + ResponseMap.find_by(reviewee_id: current_user.id, reviewed_object_id: params[:questionnaire_id]) end - # Find or initialize a response for a specific question within an attempt - def find_or_initialize_response(response_map_id, question_id) - Response.find_or_initialize_by( - response_map_id: response_map_id, - question_id: question_id - ) + # Find a resource by its ID and handle the case where it is not found + # @param model [Class] the model class to search + # @param id [Integer] the ID of the resource + # @return [Object, nil] the found resource or nil if not found + def find_resource_by_id(model, id) + model.find(id) + rescue ActiveRecord::RecordNotFound + render_error("#{model.name} not found", :not_found) + nil end - # Check if a quiz has already been assigned to a participant + # Check if the quiz has already been assigned to the student + # @param participant [Participant] the participant + # @param questionnaire [Questionnaire] the questionnaire + # @return [Boolean] true if the quiz is already assigned, false otherwise def quiz_already_assigned?(participant, questionnaire) - ResponseMap.exists?( - reviewee_id: participant.user_id, - reviewed_object_id: questionnaire.id - ) + ResponseMap.exists?(reviewee_id: participant.user_id, reviewed_object_id: questionnaire.id) end # Create questions and their respective answers for a questionnaire + # @param questionnaire [Questionnaire] the questionnaire + # @param questions_attributes [Array] the attributes for the questions + # @return [void] def create_questions_and_answers(questionnaire, questions_attributes) questions_attributes.each do |question_attr| question = questionnaire.questions.create!(question_attr.except(:answers_attributes)) @@ -144,6 +161,7 @@ def create_questions_and_answers(questionnaire, questions_attributes) end # Permit and require the necessary parameters for creating/updating a questionnaire + # @return [ActionController::Parameters] the permitted parameters def questionnaire_params params.require(:questionnaire).permit( :name, :instructor_id, :min_question_score, :max_question_score, :assignment_id, @@ -153,16 +171,23 @@ def questionnaire_params end # Render a success response with optional custom status code + # @param data [Object] the data to render + # @param status [Symbol] the HTTP status code + # @return [void] def render_success(data, status = :ok) render json: data, status: status end # Render an error response with message and status code + # @param message [String] the error message + # @param status [Symbol] the HTTP status code + # @return [void] def render_error(message, status = :unprocessable_entity) render json: { error: message }, status: status end # Ensure only instructors can perform certain actions + # @return [void] def check_instructor_role unless current_user.role.instructor? render_error('Only instructors are allowed to perform this action', :forbidden) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 4c8c36ece..eb9c0d64b 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,3 +1,18 @@ class ApplicationController < ActionController::API include JwtToken -end + + def find_resource_by_id(model, id) + model.find(id) + rescue ActiveRecord::RecordNotFound + render_error("#{model.name} not found", :not_found) + nil + end + + def render_success(data, status = :ok) + render json: data, status: status + end + + def render_error(message, status = :unprocessable_entity) + render json: { error: message }, status: status + end +end \ No newline at end of file diff --git a/app/models/question.rb b/app/models/question.rb index 3ba30ef2d..c307ed9ef 100644 --- a/app/models/question.rb +++ b/app/models/question.rb @@ -4,12 +4,12 @@ class Question < ApplicationRecord has_many :answers, dependent: :destroy accepts_nested_attributes_for :answers # Allows nested attributes for answers - 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, inclusion: { in: [true, false] } validates :correct_answer, presence: true validates :score_value, presence: true + validates :skippable, inclusion: { in: [true, false] def scorable? diff --git a/app/models/response_map.rb b/app/models/response_map.rb index 1eeacf96b..cf44f881e 100644 --- a/app/models/response_map.rb +++ b/app/models/response_map.rb @@ -4,12 +4,11 @@ class ResponseMap < ApplicationRecord belongs_to :reviewee, class_name: 'User', foreign_key: 'reviewee_id', optional: true belongs_to :questionnaire, foreign_key: 'reviewed_object_id', optional: true has_many :responses - validates :reviewee_id, uniqueness: { scope: :reviewed_object_id, - message: "is already assigned to this questionnaire" } + validates :reviewee_id, uniqueness: { scope: :reviewed_object_id, message: "is already assigned to this questionnaire" } # Gets the score from this response map def calculate_score - render_success({ score: self.score }) + responses.sum(&:score_value) end # Save the submitted answers and check if that answer is correct @@ -17,12 +16,14 @@ def process_answers(answers) answers.sum do |answer| question = Question.find(answer[:question_id]) submitted_answer = answer[:answer_value] + skipped = answer[:skipped] || false response = find_or_initialize_response(self.id, question.id) response.submitted_answer = submitted_answer + response.skipped = skipped response.save! - question.correct_answer == submitted_answer ? question.score_value : 0 + skipped ? 0 : (question.correct_answer == submitted_answer ? question.score_value : 0) end end @@ -35,4 +36,10 @@ def build_response_map(student_id, questionnaire) reviewed_object_id: questionnaire.id ) end + + private + + def find_or_initialize_response(response_map_id, question_id) + Response.find_or_initialize_by(response_map_id: response_map_id, question_id: question_id) + end end \ No newline at end of file diff --git a/app/models/role.rb b/app/models/role.rb index b6d223ad3..61dbc549d 100644 --- a/app/models/role.rb +++ b/app/models/role.rb @@ -65,4 +65,4 @@ def other_roles def as_json(options = nil) super(options.merge({ only: %i[id name parent_id] })) end -end +end \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 67b1e0aae..f1a2c42f7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -112,6 +112,7 @@ resources :student_quizzes do member do get :calculate_score + get :index end collection do post 'assign', to: 'student_quizzes#assign_quiz_to_student' diff --git a/db/migrate/20241204052902_add_skipped_to_responses.rb b/db/migrate/20241204052902_add_skipped_to_responses.rb new file mode 100644 index 000000000..3ee5ed4c7 --- /dev/null +++ b/db/migrate/20241204052902_add_skipped_to_responses.rb @@ -0,0 +1,5 @@ +class AddSkippedToResponses < ActiveRecord::Migration[7.0] + def change + add_column :responses, :skipped, :boolean, default: false + end +end diff --git a/db/migrate/20241204052911_add_skippable_to_questions.rb b/db/migrate/20241204052911_add_skippable_to_questions.rb new file mode 100644 index 000000000..a49277a09 --- /dev/null +++ b/db/migrate/20241204052911_add_skippable_to_questions.rb @@ -0,0 +1,5 @@ +class AddSkippableToQuestions < ActiveRecord::Migration[7.0] + def change + add_column :questions, :skippable, :boolean, default: true + end +end From f6827410aabc677a98ba46faabf8e7fa22cc73e4 Mon Sep 17 00:00:00 2001 From: neerua08 Date: Tue, 10 Dec 2024 16:14:08 -0500 Subject: [PATCH 16/19] Reverted DRY changes in previous commit --- .../api/v1/student_quizzes_controller.rb | 13 ++----------- app/controllers/concerns/resource_finder.rb | 5 ++++- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/app/controllers/api/v1/student_quizzes_controller.rb b/app/controllers/api/v1/student_quizzes_controller.rb index bc7f6221f..690855d3e 100644 --- a/app/controllers/api/v1/student_quizzes_controller.rb +++ b/app/controllers/api/v1/student_quizzes_controller.rb @@ -2,6 +2,8 @@ class Api::V1::StudentQuizzesController < ApplicationController before_action :check_instructor_role, except: [:submit_quiz] before_action :set_student_quiz, only: [:show, :update, :destroy] + include ResourceFinder + # Rescue from ActiveRecord::RecordInvalid exceptions and render an error response rescue_from ActiveRecord::RecordInvalid do |exception| render_error(exception.message) @@ -128,17 +130,6 @@ def find_response_map_for_current_user ResponseMap.find_by(reviewee_id: current_user.id, reviewed_object_id: params[:questionnaire_id]) end - # Find a resource by its ID and handle the case where it is not found - # @param model [Class] the model class to search - # @param id [Integer] the ID of the resource - # @return [Object, nil] the found resource or nil if not found - def find_resource_by_id(model, id) - model.find(id) - rescue ActiveRecord::RecordNotFound - render_error("#{model.name} not found", :not_found) - nil - end - # Check if the quiz has already been assigned to the student # @param participant [Participant] the participant # @param questionnaire [Questionnaire] the questionnaire diff --git a/app/controllers/concerns/resource_finder.rb b/app/controllers/concerns/resource_finder.rb index 6e5da11fe..073ce50b8 100644 --- a/app/controllers/concerns/resource_finder.rb +++ b/app/controllers/concerns/resource_finder.rb @@ -3,7 +3,10 @@ module ResourceFinder included do - # Find a specific resource by ID, handling the case where it's not found + # Find a resource by its ID and handle the case where it is not found + # @param resource [Class] the resource class to search + # @param id [Integer] the ID of the resource + # @return [Object, nil] the found resource or nil if not found def find_resource_by_id(resource, id) resource.find(id) rescue ActiveRecord::RecordNotFound From 2865f504a0dcc4b84b9e2f2318b3c29ec68925e7 Mon Sep 17 00:00:00 2001 From: Aarya Rajoju Date: Tue, 10 Dec 2024 23:46:02 -0500 Subject: [PATCH 17/19] did stuff to make it work --- .../api/v1/questions_controller.rb | 3 +- .../api/v1/student_quizzes_controller.rb | 9 ++-- app/models/question.rb | 2 +- app/models/response_map.rb | 18 ++++++- config/routes.rb | 6 +-- db/schema.rb | 4 +- db/seeds.rb | 47 +++++++++++++++++-- 7 files changed, 72 insertions(+), 17 deletions(-) diff --git a/app/controllers/api/v1/questions_controller.rb b/app/controllers/api/v1/questions_controller.rb index 1e8d4bb1d..bdc3a99e9 100644 --- a/app/controllers/api/v1/questions_controller.rb +++ b/app/controllers/api/v1/questions_controller.rb @@ -26,7 +26,8 @@ def create question = questionnaire.questions.build( txt: params[:txt], question_type: params[:question_type], - break_before: true + break_before: true, + skippable: params[:skippable] ) case question.question_type diff --git a/app/controllers/api/v1/student_quizzes_controller.rb b/app/controllers/api/v1/student_quizzes_controller.rb index 690855d3e..5b98c6549 100644 --- a/app/controllers/api/v1/student_quizzes_controller.rb +++ b/app/controllers/api/v1/student_quizzes_controller.rb @@ -22,12 +22,12 @@ def show render_success(@student_quiz) end - # GET /student_quizzes/:id/calculate_score + # GET /student_quizzes/:id/get_score # Calculate and render the score for a specific quiz attempt - def calculate_score + def get_score response_map = ResponseMap.find_by(id: params[:id]) if response_map - render_success({ score: response_map.calculate_score }) + render_success({ score: response_map.get_score }) else render_error('Attempt not found or you do not have permission to view this score.', :not_found) end @@ -91,6 +91,7 @@ def submit_quiz total_score = response_map.process_answers(params[:answers]) response_map.update!(score: total_score) + response_map.update!(score: total_score) render_success({ total_score: total_score }) end rescue ActiveRecord::RecordInvalid => e @@ -111,7 +112,7 @@ def update # Delete a specific quiz by ID def destroy @student_quiz.destroy - head :no_content + render json: { message: "Quiz with id #{params[:id]}, deleted" }, status: :ok rescue ActiveRecord::RecordNotFound render_error('Record does not exist', :not_found) end diff --git a/app/models/question.rb b/app/models/question.rb index c307ed9ef..4a2557f88 100644 --- a/app/models/question.rb +++ b/app/models/question.rb @@ -9,7 +9,7 @@ class Question < ApplicationRecord validates :break_before, inclusion: { in: [true, false] } validates :correct_answer, presence: true validates :score_value, presence: true - validates :skippable, inclusion: { in: [true, false] + validates :skippable, inclusion: { in: [true, false] } def scorable? diff --git a/app/models/response_map.rb b/app/models/response_map.rb index cf44f881e..3ae66d8ea 100644 --- a/app/models/response_map.rb +++ b/app/models/response_map.rb @@ -8,7 +8,14 @@ class ResponseMap < ApplicationRecord # Gets the score from this response map def calculate_score - responses.sum(&:score_value) + responses.sum do |response| + question = response.question + response.skipped ? 0 : (question.correct_answer == response.submitted_answer ? question.score_value : 0) + end + end + + def get_score + self.score end # Save the submitted answers and check if that answer is correct @@ -18,8 +25,15 @@ def process_answers(answers) submitted_answer = answer[:answer_value] skipped = answer[:skipped] || false + puts "#{skipped}" + puts "#{question.skippable}" + if skipped && !question.skippable + raise ActiveRecord::RecordInvalid.new("Question #{question.id} cannot be skipped.") + end + response = find_or_initialize_response(self.id, question.id) response.submitted_answer = submitted_answer + response.is_submitted = true response.skipped = skipped response.save! @@ -28,7 +42,7 @@ def process_answers(answers) end # Build a new ResponseMap instance for assigning a quiz to a student - def build_response_map(student_id, questionnaire) + def self.build_response_map(student_id, questionnaire) instructor_id = questionnaire.assignment.instructor_id ResponseMap.new( reviewee_id: student_id, diff --git a/config/routes.rb b/config/routes.rb index f1a2c42f7..acfae35c0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -111,15 +111,15 @@ end resources :student_quizzes do member do - get :calculate_score + get :get_score get :index end collection do - post 'assign', to: 'student_quizzes#assign_quiz_to_student' + post 'assign', to: 'student_quizzes#assign_quiz' post :create end end - post 'student_quizzes/submit_answers', to: 'student_quizzes#submit_answers' + post 'student_quizzes/submit_quiz', to: 'student_quizzes#submit_quiz' resources :participants, only: [:create] end end diff --git a/db/schema.rb b/db/schema.rb index 554874495..12dba3b7c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2024_04_15_192048) do +ActiveRecord::Schema[7.0].define(version: 2024_12_04_052911) do create_table "account_requests", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.string "username" t.string "full_name" @@ -226,6 +226,7 @@ t.bigint "questionnaire_id", null: false t.string "correct_answer" t.integer "score_value" + t.boolean "skippable", default: true t.index ["questionnaire_id"], name: "fk_question_questionnaires" t.index ["questionnaire_id"], name: "index_questions_on_questionnaire_id" end @@ -249,6 +250,7 @@ t.bigint "response_map_id" t.bigint "question_id" t.string "submitted_answer" + t.boolean "skipped", default: false t.index ["map_id"], name: "fk_response_response_map" t.index ["question_id"], name: "index_responses_on_question_id" t.index ["response_map_id"], name: "index_responses_on_response_map_id" diff --git a/db/seeds.rb b/db/seeds.rb index 163764115..4224245d5 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -28,13 +28,37 @@ email: 'student@example.com', password: 'password123', full_name: 'student student', - institution_id: 5, + institution_id: 1, role_id: 5 ) + Course.create!( + name: 'Software Engineering', + directory_path: 'software_engineering', + instructor_id: 2, + institution_id: 1 + ) + + Assignment.create!( + name: 'Assignment 1', + course_id: 1, + instructor_id: 2, + staggered_deadline: false, + enable_pair_programming: false, + has_badge: false, + is_calibrated: false + ) + + AssignmentParticipant.create!( + assignment_id: 1, + user_id: 3, + handle: 'student_handle' + ) + Questionnaire.create!( name: 'Questionnaire', instructor_id: 2, + assignment_id: 1, min_question_score: 0, max_question_score: 10, private: false, @@ -47,7 +71,10 @@ question_type: "multiple_choice", break_before: true, weight: 5, - questionnaire_id: 1 + questionnaire_id: 1, + correct_answer: "A", + score_value: 5, + skippable: true ) Question.create!( @@ -56,11 +83,15 @@ question_type: "multiple_choice", break_before: false, weight: 5, - questionnaire_id: 1 + questionnaire_id: 1, + correct_answer: "A", + score_value: 5, + skippable: true ) Questionnaire.create!( name: 'Questionnaire 2', + assignment_id: 1, instructor_id: 2, min_question_score: 0, max_question_score: 10, @@ -74,7 +105,10 @@ question_type: "multiple_choice", break_before: true, weight: 5, - questionnaire_id: 2 + questionnaire_id: 2, + correct_answer: "A", + score_value: 5, + skippable: true ) Question.create!( @@ -83,7 +117,10 @@ question_type: "multiple_choice", break_before: false, weight: 5, - questionnaire_id: 2 + questionnaire_id: 2, + correct_answer: "A", + score_value: 5, + skippable: true ) From 0b468076094adf89a9207fe7fad0c6c1e524ba28 Mon Sep 17 00:00:00 2001 From: neerua08 Date: Wed, 11 Dec 2024 00:14:02 -0500 Subject: [PATCH 18/19] Fixed name change in test --- old.rb | 198 ++++++++++++ .../api/v1/student_quizzes_controller_spec.rb | 300 +++++++++--------- 2 files changed, 348 insertions(+), 150 deletions(-) create mode 100644 old.rb diff --git a/old.rb b/old.rb new file mode 100644 index 000000000..b9b61af31 --- /dev/null +++ b/old.rb @@ -0,0 +1,198 @@ +class Api::V1::StudentQuizzesController < ApplicationController + before_action :check_instructor_role, except: [:submit_answers] + before_action :set_student_quiz, only: [:show, :update, :destroy] + + rescue_from ActiveRecord::RecordInvalid do |exception| + render_error(exception.message) + end + + #GET /student_quizzes + def index + quizzes = Questionnaire.all + render_success(quizzes) + end + + #GET /student_quizzes/:id + def show + render_success(@student_quiz) + end + + #GET /student_quizzes/:id/calculate_score + def calculate_score + response_map = ResponseMap.find_by(id: params[:id]) + if response_map + render_success({ score: response_map.score }) + else + render_error('Attempt not found or you do not have permission to view this score.', :not_found) + end + end + + #POST /student_quizzes + def create + questionnaire = ActiveRecord::Base.transaction do + questionnaire = create_questionnaire(questionnaire_params.except(:questions_attributes)) + create_questions_and_answers(questionnaire, questionnaire_params[:questions_attributes]) + questionnaire + end + render_success(questionnaire, :created) + rescue StandardError => e + render_error(e.message, :unprocessable_entity) + end + + #POST /student_quizzes/assign + def assign_quiz_to_student + participant = find_resource_by_id(Participant, params[:participant_id]) + questionnaire = find_resource_by_id(Questionnaire, params[:questionnaire_id]) + return unless participant && questionnaire + + if quiz_already_assigned?(participant, questionnaire) + render_error("This student is already assigned to the quiz.", :unprocessable_entity) + return + end + + response_map = build_response_map(participant.user_id, questionnaire) + if response_map.save + render_success(response_map, :created) + else + render_error(response_map.errors.full_messages.to_sentence, :unprocessable_entity) + end + end + + #POST /student_quizzes/submit_answers + def submit_answers + ActiveRecord::Base.transaction do + response_map = find_response_map_for_current_user + unless response_map + render_error("You are not assigned to take this quiz.", :forbidden) + return + end + + total_score = process_answers(params[:answers], response_map) + response_map.update!(score: total_score) + render_success({ total_score: total_score }) + end + rescue ActiveRecord::RecordInvalid => e + render_error(e.message, :unprocessable_entity) + end + + #PUT /student_quizzes/:id + def update + if @student_quiz.update(questionnaire_params) + render_success(@student_quiz) + else + render_error(@student_quiz.errors.full_messages.to_sentence, :unprocessable_entity) + end + end + + #DELETE /student_quizzes/:id + def destroy + @student_quiz.destroy + head :no_content + rescue ActiveRecord::RecordNotFound + render_error('Record does not exist', :not_found) + end + + private + + #To get quiz from db + def set_student_quiz + @student_quiz = find_resource_by_id(Questionnaire, params[:id]) + end + + # Find the response map for the current user's attempt to submit quiz answers + def find_response_map_for_current_user + ResponseMap.find_by( + reviewee_id: current_user.id, + reviewed_object_id: params[:questionnaire_id] + ) + end + + # Process and calculate the total score for submitted answers + def process_answers(answers, response_map) + answers.sum do |answer| + question = Question.find(answer[:question_id]) + submitted_answer = answer[:answer_value] + + response = find_or_initialize_response(response_map.id, question.id) + response.submitted_answer = submitted_answer + response.save! + + question.correct_answer == submitted_answer ? question.score_value : 0 + end + end + + # Find or initialize a response for a specific question within an attempt + def find_or_initialize_response(response_map_id, question_id) + Response.find_or_initialize_by( + response_map_id: response_map_id, + question_id: question_id + ) + end + + # Find a specific resource by ID, handling the case where it's not found + def find_resource_by_id(model, id) + model.find(id) + rescue ActiveRecord::RecordNotFound + render_error("#{model.name} not found", :not_found) + nil + end + + # Check if a quiz has already been assigned to a participant + def quiz_already_assigned?(participant, questionnaire) + ResponseMap.exists?( + reviewee_id: participant.user_id, + reviewed_object_id: questionnaire.id + ) + end + + # Build a new ResponseMap instance for assigning a quiz to a student + def build_response_map(student_id, questionnaire) + instructor_id = questionnaire.assignment.instructor_id + ResponseMap.new( + reviewee_id: student_id, + reviewer_id: instructor_id, + reviewed_object_id: questionnaire.id + ) + end + + # Create a new questionnaire along with its questions and answers + def create_questionnaire(params) + Questionnaire.create!(params) + end + + # Create questions and their respective answers for a questionnaire + def create_questions_and_answers(questionnaire, questions_attributes) + questions_attributes.each do |question_attr| + question = questionnaire.questions.create!(question_attr.except(:answers_attributes)) + question_attr[:answers_attributes]&.each do |answer_attr| + question.answers.create!(answer_attr) + end + end + end + + # Permit and require the necessary parameters for creating/updating a questionnaire + def questionnaire_params + params.require(:questionnaire).permit( + :name, :instructor_id, :min_question_score, :max_question_score, :assignment_id, + questions_attributes: [:id, :txt, :question_type, :break_before, :correct_answer, :score_value, + { answers_attributes: %i[id answer_text correct] }] + ) + end + + # Render a success response with optional custom status code + def render_success(data, status = :ok) + render json: data, status: status + end + + # Render an error response with message and status code + def render_error(message, status = :unprocessable_entity) + render json: { error: message }, status: status + end + + # Ensure only instructors can perform certain actions + def check_instructor_role + unless current_user.role_id == 2 + render_error('Only instructors are allowed to perform this action', :forbidden) + end + end +end \ No newline at end of file diff --git a/spec/requests/api/v1/student_quizzes_controller_spec.rb b/spec/requests/api/v1/student_quizzes_controller_spec.rb index 49252b6ca..563cb5316 100644 --- a/spec/requests/api/v1/student_quizzes_controller_spec.rb +++ b/spec/requests/api/v1/student_quizzes_controller_spec.rb @@ -1,150 +1,150 @@ -require 'swagger_helper' - -RSpec.describe 'StudentQuizzes API', type: :request do - let!(:role) { create(:role, name: "Instructor") } # Creating a role with the name "Instructor" - let!(:instructor) { create(:user, role: role) } # Creating a user with the role created above - let!(:student) { create(:user, role:'Student') } - let!(:questionnaire) { create(:questionnaire, instructor_id: instructor.id) } - let(:questionnaire_id) { questionnaire.id } - let(:valid_attributes) do - { - questionnaire: { - name: 'Quiz 1', - instructor_id: instructor.id, - min_question_score: 1, - max_question_score: 5, - assignment_id: create(:assignment).id, - questions_attributes: [ - { - txt: 'What is Ruby?', - question_type: 'text', - break_before: true, - correct_answer: 'Programming Language', - score_value: 1, - answers_attributes: [ - { answer_text: 'Programming Language', correct: true }, - { answer_text: 'A Gem', correct: false } - ] - } - ] - } - } - end - let(:invalid_attributes) do - { questionnaire: { name: '' } } - end - - 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 - - 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 - - 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) - end - end - - describe 'POST /api/v1/student_quizzes/assign' do - let(:assign_attributes) do - { participant_id: student.id, questionnaire_id: questionnaire_id } - end - - 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 - - describe 'POST /api/v1/student_quizzes/submit_answers' do - let(:submit_attributes) do - { - questionnaire_id: questionnaire_id, - answers: [ - { question_id: create(:question, questionnaire: questionnaire).id, answer_value: 'Programming Language' } - ] - } - end - - 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) - end - end - - - describe 'PUT /api/v1/student_quizzes/{id}' do - let(:valid_attributes_update) do - { - questionnaire: { - name: 'Updated Quiz Name', - } - } - end - - 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 - - context 'when the record does not exist' do - before { put "/api/v1/student_quizzes/#{questionnaire_id + 100}", params: valid_attributes_update } # Assuming an ID that does not exist - - it 'returns status code 404' do - expect(response).to have_http_status(:not_found) - end - end - - 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 - end - - describe 'DELETE /api/v1/student_quizzes/{id}' do - let!(:quiz_to_delete) { create(:questionnaire, instructor_id: instructor.id) } - - context 'when the record exists' do - it 'deletes the record' do - expect { - delete "/api/v1/student_quizzes/#{quiz_to_delete.id}" - }.to change(Questionnaire, :count).by(-1) - expect(response).to have_http_status(:no_content) - end - end - - context 'when the record does not exist' do - before { delete "/api/v1/student_quizzes/#{quiz_to_delete.id + 100}" } # Assuming an ID that does not exist - - it 'returns status code 404' do - expect(response).to have_http_status(:not_found) - end - end - end -end +require 'swagger_helper' + +RSpec.describe 'StudentQuizzes API', type: :request do + let!(:role) { create(:role, name: "Instructor") } # Creating a role with the name "Instructor" + let!(:instructor) { create(:user, role: role) } # Creating a user with the role created above + let!(:student) { create(:user, role:'Student') } + let!(:questionnaire) { create(:questionnaire, instructor_id: instructor.id) } + let(:questionnaire_id) { questionnaire.id } + let(:valid_attributes) do + { + questionnaire: { + name: 'Quiz 1', + instructor_id: instructor.id, + min_question_score: 1, + max_question_score: 5, + assignment_id: create(:assignment).id, + questions_attributes: [ + { + txt: 'What is Ruby?', + question_type: 'text', + break_before: true, + correct_answer: 'Programming Language', + score_value: 1, + answers_attributes: [ + { answer_text: 'Programming Language', correct: true }, + { answer_text: 'A Gem', correct: false } + ] + } + ] + } + } + end + let(:invalid_attributes) do + { questionnaire: { name: '' } } + end + + 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 + + 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 + + describe 'GET /api/v1/student_quizzes/{quiz_id}/get_score' do + it 'gets score for a given quiz' do + get "/api/v1/student_quizzes/#{questionnaire_id}/get_score" + expect(response).to have_http_status(:ok) + end + end + + describe 'POST /api/v1/student_quizzes/assign' do + let(:assign_attributes) do + { participant_id: student.id, questionnaire_id: questionnaire_id } + end + + 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 + + describe 'POST /api/v1/student_quizzes/submit_answers' do + let(:submit_attributes) do + { + questionnaire_id: questionnaire_id, + answers: [ + { question_id: create(:question, questionnaire: questionnaire).id, answer_value: 'Programming Language' } + ] + } + end + + 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) + end + end + + + describe 'PUT /api/v1/student_quizzes/{id}' do + let(:valid_attributes_update) do + { + questionnaire: { + name: 'Updated Quiz Name', + } + } + end + + 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 + + context 'when the record does not exist' do + before { put "/api/v1/student_quizzes/#{questionnaire_id + 100}", params: valid_attributes_update } # Assuming an ID that does not exist + + it 'returns status code 404' do + expect(response).to have_http_status(:not_found) + end + end + + 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 + end + + describe 'DELETE /api/v1/student_quizzes/{id}' do + let!(:quiz_to_delete) { create(:questionnaire, instructor_id: instructor.id) } + + context 'when the record exists' do + it 'deletes the record' do + expect { + delete "/api/v1/student_quizzes/#{quiz_to_delete.id}" + }.to change(Questionnaire, :count).by(-1) + expect(response).to have_http_status(:no_content) + end + end + + context 'when the record does not exist' do + before { delete "/api/v1/student_quizzes/#{quiz_to_delete.id + 100}" } # Assuming an ID that does not exist + + it 'returns status code 404' do + expect(response).to have_http_status(:not_found) + end + end + end +end From c0f263ea8cf54212be794eb808d4fafb3ed573a7 Mon Sep 17 00:00:00 2001 From: neerua08 Date: Wed, 11 Dec 2024 00:15:13 -0500 Subject: [PATCH 19/19] Fixed name change in test --- old.rb | 198 --------------------------------------------------------- 1 file changed, 198 deletions(-) delete mode 100644 old.rb diff --git a/old.rb b/old.rb deleted file mode 100644 index b9b61af31..000000000 --- a/old.rb +++ /dev/null @@ -1,198 +0,0 @@ -class Api::V1::StudentQuizzesController < ApplicationController - before_action :check_instructor_role, except: [:submit_answers] - before_action :set_student_quiz, only: [:show, :update, :destroy] - - rescue_from ActiveRecord::RecordInvalid do |exception| - render_error(exception.message) - end - - #GET /student_quizzes - def index - quizzes = Questionnaire.all - render_success(quizzes) - end - - #GET /student_quizzes/:id - def show - render_success(@student_quiz) - end - - #GET /student_quizzes/:id/calculate_score - def calculate_score - response_map = ResponseMap.find_by(id: params[:id]) - if response_map - render_success({ score: response_map.score }) - else - render_error('Attempt not found or you do not have permission to view this score.', :not_found) - end - end - - #POST /student_quizzes - def create - questionnaire = ActiveRecord::Base.transaction do - questionnaire = create_questionnaire(questionnaire_params.except(:questions_attributes)) - create_questions_and_answers(questionnaire, questionnaire_params[:questions_attributes]) - questionnaire - end - render_success(questionnaire, :created) - rescue StandardError => e - render_error(e.message, :unprocessable_entity) - end - - #POST /student_quizzes/assign - def assign_quiz_to_student - participant = find_resource_by_id(Participant, params[:participant_id]) - questionnaire = find_resource_by_id(Questionnaire, params[:questionnaire_id]) - return unless participant && questionnaire - - if quiz_already_assigned?(participant, questionnaire) - render_error("This student is already assigned to the quiz.", :unprocessable_entity) - return - end - - response_map = build_response_map(participant.user_id, questionnaire) - if response_map.save - render_success(response_map, :created) - else - render_error(response_map.errors.full_messages.to_sentence, :unprocessable_entity) - end - end - - #POST /student_quizzes/submit_answers - def submit_answers - ActiveRecord::Base.transaction do - response_map = find_response_map_for_current_user - unless response_map - render_error("You are not assigned to take this quiz.", :forbidden) - return - end - - total_score = process_answers(params[:answers], response_map) - response_map.update!(score: total_score) - render_success({ total_score: total_score }) - end - rescue ActiveRecord::RecordInvalid => e - render_error(e.message, :unprocessable_entity) - end - - #PUT /student_quizzes/:id - def update - if @student_quiz.update(questionnaire_params) - render_success(@student_quiz) - else - render_error(@student_quiz.errors.full_messages.to_sentence, :unprocessable_entity) - end - end - - #DELETE /student_quizzes/:id - def destroy - @student_quiz.destroy - head :no_content - rescue ActiveRecord::RecordNotFound - render_error('Record does not exist', :not_found) - end - - private - - #To get quiz from db - def set_student_quiz - @student_quiz = find_resource_by_id(Questionnaire, params[:id]) - end - - # Find the response map for the current user's attempt to submit quiz answers - def find_response_map_for_current_user - ResponseMap.find_by( - reviewee_id: current_user.id, - reviewed_object_id: params[:questionnaire_id] - ) - end - - # Process and calculate the total score for submitted answers - def process_answers(answers, response_map) - answers.sum do |answer| - question = Question.find(answer[:question_id]) - submitted_answer = answer[:answer_value] - - response = find_or_initialize_response(response_map.id, question.id) - response.submitted_answer = submitted_answer - response.save! - - question.correct_answer == submitted_answer ? question.score_value : 0 - end - end - - # Find or initialize a response for a specific question within an attempt - def find_or_initialize_response(response_map_id, question_id) - Response.find_or_initialize_by( - response_map_id: response_map_id, - question_id: question_id - ) - end - - # Find a specific resource by ID, handling the case where it's not found - def find_resource_by_id(model, id) - model.find(id) - rescue ActiveRecord::RecordNotFound - render_error("#{model.name} not found", :not_found) - nil - end - - # Check if a quiz has already been assigned to a participant - def quiz_already_assigned?(participant, questionnaire) - ResponseMap.exists?( - reviewee_id: participant.user_id, - reviewed_object_id: questionnaire.id - ) - end - - # Build a new ResponseMap instance for assigning a quiz to a student - def build_response_map(student_id, questionnaire) - instructor_id = questionnaire.assignment.instructor_id - ResponseMap.new( - reviewee_id: student_id, - reviewer_id: instructor_id, - reviewed_object_id: questionnaire.id - ) - end - - # Create a new questionnaire along with its questions and answers - def create_questionnaire(params) - Questionnaire.create!(params) - end - - # Create questions and their respective answers for a questionnaire - def create_questions_and_answers(questionnaire, questions_attributes) - questions_attributes.each do |question_attr| - question = questionnaire.questions.create!(question_attr.except(:answers_attributes)) - question_attr[:answers_attributes]&.each do |answer_attr| - question.answers.create!(answer_attr) - end - end - end - - # Permit and require the necessary parameters for creating/updating a questionnaire - def questionnaire_params - params.require(:questionnaire).permit( - :name, :instructor_id, :min_question_score, :max_question_score, :assignment_id, - questions_attributes: [:id, :txt, :question_type, :break_before, :correct_answer, :score_value, - { answers_attributes: %i[id answer_text correct] }] - ) - end - - # Render a success response with optional custom status code - def render_success(data, status = :ok) - render json: data, status: status - end - - # Render an error response with message and status code - def render_error(message, status = :unprocessable_entity) - render json: { error: message }, status: status - end - - # Ensure only instructors can perform certain actions - def check_instructor_role - unless current_user.role_id == 2 - render_error('Only instructors are allowed to perform this action', :forbidden) - end - end -end \ No newline at end of file