Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails 3.2 to 4.0 Upgrade: Undefined method to_datetime for false:FalseClass

I'm upgrading a Rails application I've inherited from 3.2 to 4.0.1. I followed and finished the edge guide here:

http://edgeguides.rubyonrails.org/upgrading_ruby_on_rails.html#upgrading-from-rails-3-2-to-rails-4-0

I've gotten everything fixed except for a single error that I can't seem to find the root cause of. When I attempt to save a User model object, I'm met with the following error:

[1] pry(main)> User.create(name: "test user", email: "[email protected]", password: "testPassword123", password_confirmation: "testPassword123")                                                                                                                               

(0.6ms)  BEGIN
(0.9ms)  ROLLBACK
NoMethodError: undefined method `to_datetime' for false:FalseClass
from /home/cmhobbs/src/serve2perform/.gem/ruby/2.3.0/gems/activesupport-4.0.1/lib/active_support/core_ext/date_time/calculations.rb:161:in `<=>'

activesupport 4.0.1 and rals 4.0.1 are installed. I use chgems and I purged my .gem/ directory and Gemfile.lock before bundling again.

Here is a Gist of the User model.

And here is all of the backtrace output I could get from pry.

Here is a link to the User table schema.

like image 263
cmhobbs Avatar asked Apr 23 '16 01:04

cmhobbs


2 Answers

Once you've found the offending callback to be this one:

  before_create :activate_license

  def activate_license
    self.active_license = true
    self.licensed_date = Time.now
  end

things begin to be clearer. The activate_licence is a before callback. Before callbacks can halt the whole callbacks chain by returning false (or raising an exception).

If we look carefully in the debug output that you provided by manually adding some puts lines into the Rails callbacks code, we can indeed find the comparison of this callback result with false (here - I removed some unimportant parts of the code):

result = activate_license
halted = (result == false)
if halted
  halted_callback_hook(":activate_license")
end 

Because the support for halting before callbacks by returning false (i.e. the Rails code shown above) practically has not changed from Rails 3.2 to Rails 4.0.1, the issue must lie in the comparison itself.

The callback returns a DateTime object (it's the last assignment in the method which is also returned). And, indeed, the comparison of DateTimes changed significantly between the two Rails versions (also note that the == operator is normally evaluated using the <=> operator):

  • in Rails 3.2 it was this:

    def <=>(other)
      if other.kind_of?(Infinity)
        super
      elsif other.respond_to? :to_datetime
       super other.to_datetime
      else
        nil
      end
    end
    

    notice especially the respond_to? check if the other object is also a date or time object while otherwise returning nil.

  • whereas in Rails 4.0.1 this changed to the bare code below:

    def <=>(other)
      super other.to_datetime
    end
    

    → all sanity checks are gone!

Now, everything is clear: the result of the callback (a DateTime object) is compared using the <=> operator with false and under Rails 4.0, the comparison tries to convert the false object to DateTime without any sanity checks, which of course fails and throws the exception.

To fix this issue, simply ensure that your callback returns something that Rails can compare with false without any problems, e.g. true, as your callback is never supposed to halt the chain:

  def activate_license
    self.active_license = true
    self.licensed_date = Time.now
    true
  end

Now everything should work as expected again.

like image 167
Matouš Borák Avatar answered Nov 08 '22 02:11

Matouš Borák


You can bind even in core classes, please do something like this and check what the other is, from where it came from.

/home/cmhobbs/src/serve2perform/.gem/ruby/2.3.0/gems/activesupport-4.0.1/lib/active_support/core_ext/date_time/calculations.rb

def <=>(other)
  binding.pry
  if other.kind_of?(Infinity)
    super
  elsif other.respond_to? :to_datetime
    super other.to_datetime rescue nil
  else
    nil
  end
end
like image 42
dodo121 Avatar answered Nov 08 '22 02:11

dodo121