How to validate an attribute with "validates :attribute" referring to the value of a parent object attribute?
Asked Answered
J

1

5

In this exercise from O'Reilly's "Head first Rails" (ed. 2009) there are 2 related objects.

The "Flight" object [I used annotate gem to show every attribute]:

# == Schema Information
#
# Table name: flights
#
#  id                :integer         not null, primary key
#  departure         :datetime
#  arrival           :datetime
#  destination       :string(255)
#  baggage_allowance :decimal(, )
#  capacity          :integer
#  created_at        :datetime
#  updated_at        :datetime
#

class Flight < ActiveRecord::Base
  has_many :seats
end

Ant the "Seat" object:

# == Schema Information
#
# Table name: seats
#
#  id         :integer         not null, primary key
#  flight_id  :integer
#  name       :string(255)
#  baggage    :decimal(, )
#  created_at :datetime
#  updated_at :datetime
#

class Seat < ActiveRecord::Base
  belongs_to :flight
end

As you may guess the seat.baggage value should always be less than or equal to seat.flight.baggage_allowance.

So I wrote this validator, which works well:

class Seat < ActiveRecord::Base

  belongs_to :flight

  def validate
      if baggage > flight.baggage_allowance
      errors.add_to_base("Your have to much baggage for this flight!")
    end 
  end  
end

Then I tried to refactor it with this prettier one:

      validates :baggage, :numericality => { :less_than_or_equal_to => flight.baggage_allowance }, :presence => true 

But it causes a NameError in SeatsController:

undefined local variable or method `flight' for #<Class:0x68ac3d8>"

Than I also tried with "self.flight.baggage_allowance":

validates :baggage, :numericality => { :less_than_or_equal_to => self.flight.baggage_allowance }, :presence => true

But it throws a NoMethodError Exception:

undefined method `flight' for #<Class:0x67e9b40>

Is there a way to make the prettier validator work?

Which is the best practice to do this kind of validation?

Thank you.

---EDIT---

As Maurício Linhares kindly suggested hereafter, the problem is solvable defining a :bagging_allowance symbol. I'd like to understand better where custom validation is really needed. What about this one, is it possible to convert even this to a "validates" method or not? and why?

class Seat < ActiveRecord::Base
.
.
.
def validate
  if flight.capacity <= flight.seats.size
    errors.add_to_base("The flight is fully booked, no more seats available!")
  end
end

Thank you again.

Joinder answered 29/7, 2011 at 15:11 Comment(2)
Try an anonymous function in your validator: :less_than_or_equal_to => Proc.new { |flight| flight.baggage_allowance }Metacarpus
I wrote: validates :baggage, :numericality => { :less_than_or_equal_to => Proc.new { |flight| flight.baggage_allowance }}, :presence => true It throws: NoMethodError in SeatsController#create undefined method 'baggage_allowance' for #<Seat:0x6a8be38> Thank you anyway!Joinder
P
9

You can do it like this:

class Seat < ActiveRecord::Base

  validates :baggage, :numericality => { :less_than_or_equal_to => :baggage_allowance }, :presence => true 

  belongs_to :flight

  def baggage_allowance
    flight.baggage_allowance
  end  
end

EDIT

You can't do this:

class Seat < ActiveRecord::Base
        validates :baggage, :numericality => { :less_than_or_equal_to => flight.baggage_allowance }, :presence => true 
end

Because the method validates is being called at the class level, so there is no flight variable available, as it is an instance variable. When you configure it with :baggage_allowance you tell Rails to call the :baggage_allowance method on an instance of Seat to be able to access the value.

Pedalfer answered 29/7, 2011 at 17:50 Comment(3)
Thank you, it works! So it seems that parent model attributes aren't initialized when "validates" is processed. Is there a specific reason? Do you know where I can learn more about it please?Joinder
Your code works and solved my problem! I'd only like to know more about "validates" method to understand when custom validation isn't avoidable (to the detriment of code clearness). In other words, I'd like to know WHY is not possible to call a parent method like "flight.baggage_allowance" in validates. I edited the first post to better explain my concern. Thank you again.Joinder
There you go, added to the answer.Toothy

© 2022 - 2024 — McMap. All rights reserved.