문제

Is it possbile to initialize an instance variable through a named scope? I want to remember my search criteria after the initial search. See the following:

class Foo < ActiveRecord::Base    
  attr_accessor :search

  named_scope :bar, lambda {|search|
    # Do something here to set the @search variable to search
    {
      :conditions => [
        "name LIKE :search OR description LIKE :search", 
        {:search => "%#{search}%"} ]
    } 
  }

  def match
    if name.match(@search)
      :name
    elsif description.match(@search)
      :description
    end
  end
end

Alternatively, I could create a class method that calls the named scope and then iterates through the results to set the instance variable on each, but I lose the named scope chainability in that case.

도움이 되었습니까?

해결책

named scopes are not instantiating any single record, until they are iterated. And they may be, as you correctly say, be chained with more scopes.

I see two possible solutions. One relies on the database to populate a fake column with the matching value. It should be simple using a special :select option to the query (or a special argument to .select() in rails 3)

The other is to rely on the controller. This is much easier to implement, but you have to ensure that the "match" in ruby is the same than the "LIKE" in the database. I'm speaking about collation, unicode normalized forms etc. Most probably there is at least one case in which they behave differently in both engines.

다른 팁

thanks alot for the latest post with inspiration of how to do it. This monkey patches still work with Rails 5.

I just encountered the situation, that the context wasnt forwarded, when chaining the scope with "where()" conditions.

To accomplish this, I added another patch to Association's CollectionProxy, which seem to work for my current situation.

Perhaps it is useful for you or somebody else:

module ActiveRecord
  module Associations
    class CollectionProxy
      alias_method :original_scope, :scope

      def scope
        return @scope if @scope

        original_scope.tap { |new_scope|
          new_scope.scope_context = scope_context
        }
      end
    end
  end
end

I've been frantically trying to accomplish this same problem for the past week and have finally managed it. Since I found this question amidst my troubles, I figured that I'd share the answer here just in case anyone else finds themselves faced with a similar problem.

As rewritten stated in his answer, you can't set an instance variable on your model from the scope as there is no model instance yet. The scope is just for building up the sql expression that will be evaluated when you try to iterate over the result of the scope. What does exist, however, is the ActiveRecord::Relation object. This is the thing that holds the details of your scope. By putting the variables you want to keep on that object, they'll be still accessible later.

So, how to get the variable on to the Relation object? Monkey patching comes to your rescue here:

/lib/core_ext/add_scope_context_to_activerecord_relation.rb:

module ActiveRecord
  class Relation
    attr_writer :scope_context

    def scope_context
      @scope_context ||= {}
    end
  end

  module SpawnMethods
    alias_method :old_merge, :merge

    def merge(r)
      merged = old_merge(r)
      merged.scope_context.deep_merge! r.scope_context
      merged
    end
  end
end

Now, in all of your scopes, you have a scope_context variable. The SpawnMethods#merge malarky is to make sure that the scope_context is kept along a chain of scopes (eg Foo.search(xxx).sort(xxx).paginate(xxx))

/app/concerns/searchable.rb:

require 'active_support/concern'

module Searchable
  extend ActiveSupport::Concern

  included do
    self.scope :search, Proc.new { |params|
      s = scoped

      s.scope_context[:searchable] = {}
      s.scope_context[:searchable][:params] = parse_params params

      s.scope_context[:searchable][:params].each do |param|
        s = s.where generate_where_expression param
      end
      s
    } do
      def search_val col_name
        param = scope_context[:searchable][:params].find do |param|
          param[:col_name] == field.to_s
        end
        param.nil? ? '' : param[:val]
      end
    end
  end

  module ClassMethods
    def parse_params params
      # parse and return the params
    end

    def generate_where_expression param
      "#{param[:col_name]} like '%#{param[:val]}%'"
    end
  end
end

Now, our controller, model and view will look like the following:

/app/controllers/foo_controller.rb:

class FooController < ApplicationController
  def index
    @foos = Foo.search(params[:search])
  end
end

/app/models/foo.rb:

class Foo < ActiveRecord::Base    
  include Searchable

  attr_accessor :name, :description
end

/app/views/foos/index.html.erb:

<p>You searched for:</p>
<table>
  <tr>
    <th>Name</th>
    <td><%= @foos.search_val :name %>
  </tr>
  <tr>
    <th>Description</th>
    <td><%= @foos.search_val :description %>
  </tr>
</table>
<p>And you got:</p>
<table>
  <% @foos.each do |foo| %>
    <tr> xxx </tr>
  <% end %>
</table>

Now, you may have noticed the core_ext and concerns directories alluded to above. Your application will need to be made aware of these:

/config/application.rb:

# Should be fairly obvious where to put this line
config.autoload_paths += Dir[Rails.root.join('app', 'concerns', '{**}')]

/config/initializers/load_extensions.rb:

Dir[File.join(Rails.root, "lib", "core_ext", "*.rb")].each {|l| require l }

Don't forget to restart your server and away you go (assuming I haven't forgotten to include anything).

Oh, and I've come up with this solution with Rails 3.2.13 and Ruby 1.9.3; no idea how it behaves with other versions.

라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top