Reputation: 813
I'm trying to add some custom methods to ActiveRecord. I want to add a *_after
and *_before
scopes for every date
field of a model so I can do something like this:
User.created_at_after(DateTime.now - 3.days).created_at_before(DateTime.now)
I've followed the solution explained here Rails extending ActiveRecord::Base but when I execute the rails console and try to call the methods I get an undefined method
error.
Here's my code:
# config/initializers/active_record_date_extension.rb
require "active_record_date_extension"
# lib/active_record_date_extension.rb
module ActiveRecordDateExtension
extend ActiveSupport::Concern
included do |base|
base.columns_hash.each do |column_name,column|
if ["datetime","date"].include? column.type
base.define_method("#{column_name}_after") do |date|
where("#{column_name} > ?", date)
end
base.define_method("#{column_name}_before") do |date|
where("#{column_name} < ?", date)
end
end
end
end
end
Rails.application.eager_load!
ActiveRecord::Base.descendants.each do |model|
model.send(:include, ActiveRecordDateExtension)
end
What am I doing wrong?
Upvotes: 9
Views: 1346
Reputation: 813
Thanks to the previous answer I realised part of the problems. Here are all the problems and the solution that I came to after some research:
column.type
is a symbol and I was comparing it with a String.base.define_method
is a private methodsingleton_class
, not in the base
class nor the class
.Rails.application.eager_load!
will cause eager load even when it is not required. This wasn't affecting the functionality but in first place the eager load should not be responsibility of this "extension" and in second place it depends in Rails, making the "extension" only Rails compatible.Taking into account these problems I decided to implement it using the method_missing
functionality of ruby and I wrote this gem (https://github.com/simon0191/date_supercharger). Here is the relevant part for this question:
module DateSupercharger
extend ActiveSupport::Concern
included do
def self.method_missing(method_sym, *arguments, &block)
return super unless descends_from_active_record?
matcher = Matcher.new(self,method_sym)
# Inside matcher
# method_sym.to_s =~ /^(.+)_(before|after)$/
if matcher.match?
method_definer = MethodDefiner.new(self) # self will be klass inside Matcher
method_definer.define(attribute: matcher.attribute, suffix: matcher.suffix)
# Inside MethodDefiner
# new_method = "#{attribute}_#{suffix}"
# operators = { after: ">", before: "<" }
# klass.singleton_class.class_eval do
# define_method(new_method) do |date|
# where("#{attribute} #{operators[suffix]} ?", date)
# end
# end
send(method_sym, *arguments)
else
super
end
end
def self.respond_to?(method_sym, include_private = false)
return super unless descends_from_active_record?
if Matcher.new(self,method_sym).match?
true
else
super
end
end
end
end
ActiveRecord::Base.send :include, DateSupercharger
Upvotes: 2
Reputation: 2378
Using Rails 4.1.9 and Ruby 2.2.1, I noticed a few issues with the code above.
column.type
with strings, and Rails returns symbols for that attribute.base.define_method
is trying to call a private method, you can get around that with send
This is the tweaked code
module ActiveRecordDateExtension
extend ActiveSupport::Concern
included do |base|
base.columns_hash.each do |column_name,column|
if [:datetime, :date].include? column.type
base.class.send(:define_method, "#{column_name}_after") do |date|
where("#{column_name} > ?", date)
end
base.class.send(:define_method, "#{column_name}_before") do |date|
where("#{column_name} < ?", date)
end
end
end
end
end
Upvotes: 4