27

I want to DRY up several models by moving shared scopes into a module, something like:

module CommonScopes
  extend ActiveSupport::Concern

  module ClassMethods
    scope :ordered_for_display, order("#{self.to_s.tableize}.rank asc")
  end
end

I also want to create shared specs that test the module. Unfortunately when I try to include the shared scope in my model I get:

undefined method `order' for CommonScopes::ClassMethods:Module

Any ideas? Thanks!

Rimian
  • 36,864
  • 16
  • 117
  • 117
Allyl Isocyanate
  • 13,306
  • 17
  • 79
  • 130

3 Answers3

67

As in rails 4 scope syntax you can simply use a lambda to delay the execution of the code (works in rails 3 too):

module CommonScopes
  extend ActiveSupport::Concern

  included do
    scope :ordered_for_display, -> { order("#{self.to_s.tableize}.rank asc") }
  end
end
mdemolin
  • 2,514
  • 1
  • 20
  • 23
17

You can use instance_eval

module CommonScopes
  extend ActiveSupport::Concern

  def self.included(klass)
    klass.instance_eval do
      scope :ordered_for_display, order("#{self.to_s.tableize}.rank asc")
    end
  end
end
Gazler
  • 83,029
  • 18
  • 279
  • 245
  • Don't you think lambdas should be preferred now? – mdemolin Nov 26 '13 at 14:04
  • @mdemolin I believe calling `scope` without a lambda is deprecated in rails 4. – Gazler Nov 26 '13 at 15:09
  • 1
    exactly what I was saying :) (but I think it is just the preferred syntax for now, and not deprecated yet) – mdemolin Nov 26 '13 at 15:14
  • 2
    This is also sort of wrong in that it nullifies the point of extending ActiveSupport::Concern. The scope should either go in a block like `included do; ... end` since that is part of ActiveRecord::Concern, or we should not bother extending it, since the above `self.included` syntax is built into every Ruby module, and has nothing to do with ActiveSupport::Concern. Ideally we pick one way or the other, since ActiveSupport::Concern takes care of that boilerplate code that we're reimplementing in this answer's example. This is for both Rails 3 and 4, as in mdemolin's answer – John Puccino Nov 30 '17 at 13:46
2

Because your scope method is called immediately when your module is parsed by Ruby and it's not accessible from your CommonScopes module..

But you can replace your scope call by a class method:

module CommonScopes
  extend ActiveSupport::Concern

  module ClassMethods
    def ordered_for_display
      order("#{self.to_s.tableize}.rank asc")
     end
  end
end
Richard.P
  • 802
  • 7
  • 6