But what if we have a requirement to filter blog posts by substrings in the title? The named scope would be something of the form:
named_scope :with_title, lambda { |*args| {:conditions => ["title like ?", '%'+args[0]+'%']} }This makes our entire post class look like this:
class Post < ActiveRecord::BaseRecall from part 1 that we passed all our scopes into the controller in the form:
named_scope :published, :conditions => {:status => 'published'}
named_scope :draft, :conditions => {:status => 'draft'}
named_scope :archived, :conditions => {:status => 'archived'}
named_scope :published_or_draft, :conditions => ["status in ('published', 'archived')"]
named_scope :last_month, lambda { {:conditions => ["updated_at > ?", 1.month.ago]} }
named_scope :last_year, lambda { {:conditions => ["updated_at > ?", 1.year.ago]} }
named_scope :this_month, lambda { {:conditions => ["updated_at > ?", Time.now.beginning_of_month]}}
named_scope :sort_by_title, :order => "title DESC"
named_scope :sort_by_updated, :order => "updated_at DESC"
named_scope :with_title, lambda { |*args| {:conditions => ["title like ?", '%'+args[0]+'%']} }
def published?; status == "published" end
def draft?; status == "draft" end
def archived?; status == "archived" end
end
{:scopes => ["published", "sort_by_title"]}
Ideally, I would like to introduce parameterized named scopes to the controller in the form:
{:scopes => [ ["published"],If such a data structure were easily built in forms, then we could almost use the controller code from part 1 without change. Sadly, that is not quite possible.
["sort_by_title"],
["with_title", "foo"] ]}
As a workaround, I choose the convention of passing in named scopes with parameters as hashes.
<div id="filter-by-title-substring">This will give us parameters of the form:
<input type="text" name="scopes[][with_title]"/>
</div>
{:scopes => [ "published",Since we are already adding complexity, why not generalize a bit as well? The following code will handle the list-of-strings-and-hashes data structure, plus it will work for any class:
"sort_by_title",
{"with_title" => "foo"} ]}
class PostsController < ApplicationControllerThe scopes_for() private method handles the proxy scope injection that was done entirely in the index action from part 1.
def index
@posts = scopes_for(Post)
end
private
def scopes_for(klass)
scopes_from_params.inject(klass){|proxy, scope| proxy.send(*scope)}
end
def scopes_from_params
returning scopes = [] do
(params[:scopes] || []).reject(&:blank?).each do |scope|
case scope.class.to_s
when "String"
scopes << [scope] if klass.scopes.include?(scope.to_sym)
when "Hash", "HashWithIndifferentAccess"
msg, arg = scope.first # {:foo => 'bar'}.first => [:foo, "bar"]
scopes << [msg, arg] if klass.scopes.include?(msg.to_sym) && !arg.blank?
end
end
end
scopes.blank? ? [:all] : scopes
end
end
The actual scope calculation is now done in the scopes_from_params method. It still checks the supplied scopes to ensure that they are scopes and not arbitrary methods. It still returns the :all scope if no scopes are supplied. But it also handles our "Hash" case for invoking named scopes with arguments.
The last change is back in the scopes_for method, which now splats the scopes returned from scopes_from_params. If we access the action with the parameters:
{:scopes => [ "published",We will get the following back from scopes_from_params():
"sort_by_title",
{"with_title" => "foo"} ]}
[ ["published"],Injecting these into Post in scopes_for() will result in Post.send("published").send("sort_by_title").send("with_title", "foo"), which is equivalent to Post.published.sort_by_title.with_title("foo)—exactly what we want.
["sort_by_title"],
["with_title", "foo"] ]
We now have all the benefits from part 1 (simple views, simple controllers, no magic number coupling between view & controller to handly combo cases like showing published and drafted posts at the same time), plus the ability to use named scopes that take arguments.
No comments:
Post a Comment