Search code examples
ruby-on-railsvalidationnested-attributes

Validation that checks if any nested attributes contain a value


I'm trying to write a validation that checks if one of the nested attributes that belong to a model contains a certain value.

In this case I have a Questions model that contains many Answers. I need a validation that checks to see if at least one of the questions has a correct answer marked.

This is an app for creating tests. The question has several answers but not all of them are the correct one.

Here's my Question model:

class Question < ApplicationRecord
    belongs_to :examination
    has_many :answers, dependent: :destroy
    has_many :responses

    accepts_nested_attributes_for :answers, allow_destroy: true, :reject_if => :all_blank

    validates_presence_of :body
    validates_presence_of :question_type

    validate :has_correct_ans?

     private

     def has_correct_ans?
      errors.add(:correct, "You must select at least one correct answer") unless
      self.answers.exists?(correct: true)
     end
end

Here's the Answer model

class Answer < ApplicationRecord
    belongs_to :question
    has_many :responses, dependent: :destroy

end

I attempted to write a method called "has_correct_ans?" to check if any of the answers contain the correct attribute. This fails every time though. I assume that's because prior to saving the data doesn't exist in the database. From testing in the console that command works fine on existing data.
i.e. Question.find.answers.exists?(correct: true)

will return true for questions in which one of the answers has the correct attribute.

Id really like this to work as a validation. I just don't know how to access the nested attribute prior to save.

This is what the params look like:

Parameters: {"utf8"=>"✓", "authenticity_token"=>"dOf8H1Wqark3TZAGgX6kaY5Yt4kYKm1FNbCnNi4BlVTTQV9PijlkA1bNS8Qi8DwLLxV6FkWzNbmiT6X+7Vr6Xg==", "question"=>{"body"=>"gfdgdfs", "question_type"=>"Multiple Choice", "points"=>"1", "answers_attributes"=>{"0"=>{"correct"=>"true", "body"=>"dggf", "_destroy"=>"0"}}}, "commit"=>"Submit", "examination_id"=>"12"}

I also tried to do this in the controller using the params. Here's what my create function looks like:

class QuestionsController < ApplicationController

  def create
    @exam = Examination.find(params[:examination_id])
    @question = @exam.questions.build(question_params)
    ans_params = params[:question][:answers_attributes]
    @correct_ans = false
    ans_params.each do |k, v|
      if @correct_ans == false
       @correct_ans =  v.has_key?(:correct)
      end
    end

    if @exam.questions.count > 0
      @question.position = @exam.questions.count + 1 
    else 
      @question.position = 1
    end
    if @correct_ans == true && @question.save
      redirect_to @exam, notice: "question created successfully"
    elsif @question.save
      flash[:error] = "You need a correct answer"
      render :new
    else
      render :new
    end
  end

That doesn't actually work either. It still saves even though there is no correct answer. I don't want to do it in the controller anyway. It would work much better as a validation.

I'm sure I'm missing something obvious here. Can anyone help me out?


Solution

  • I got it!

    Thanks, Ashik. I had no idea that you can use methods like that, even though the data is not in the database.

    I tried your idea. It didn't return the right result but was really close.

    answers.map{ |x| x[:correct] == true }.size == 0?

    Returns an array of the correct attributes for all the Answers records. Like this:

    [false, false, true]
    

    I changed the method a little bit to this:

    answers.map{ |x| x[:correct]}.include? true
    

    Which returns true if any of the records contain a correct. I tried it in my validation method and it works perfectly.

    Here's the updated Question model.

    class Question < ApplicationRecord
        belongs_to :examination
        has_many :answers, dependent: :destroy
        has_many :responses
    
        accepts_nested_attributes_for :answers, allow_destroy: true, :reject_if => :all_blank
    
        validates_presence_of :body
        validates_presence_of :question_type
    
        validate :has_correct_ans?
    
         private
    
         def has_correct_ans?
          errors.add(:correct, "You must select at least one correct answer") unless
          answers.map{ |x| x[:correct]}.include? true
    
         end
    end