How to combine different scopes in Rails 3? - ruby-on-rails

I have the following named scopes in my rails app:
scope :published, :conditions => {:status => 'published'}
scope :coming_soon, :conditions => {:status => 'coming_soon'}
scope :in_development, :conditions => {:status => 'in_development'}
scope :cancelled, :conditions => {:status => 'cancelld'}
I'm having trouble writing one that combines "published" and "combing soon." Here's what I've tried.
scope :public, :conditions => {"status == published || status == coming_soon"}
Any ideas?

Rails 2: named_scope :public, :status => ['published', 'coming_soon']
Rails 3: scope :public, where(:status => ['published', 'coming_soon'])
Rails will see the array and use the IN operator in the sql.
An a note: The other approach (chain existing scopes) of Article.published.coming_soon would NOT work because an Article can't be both of those things at the time time or be a subset of each other.
Another note: Careful when you want something dependent on a variable parameter. For example say you wanted "future appointment" for a scheduling system, you might write
[This is invalid]
Rails 2: named_scope :upcoming_appts, :conditions => (['appt_dt > ?', Time.now])
Rails 3: scope :upcoming_appts, where(['appt_dt > ?', Time.now])
However there's a problem: The Time.now will get evaluated the first time the class is evaluated not when the scope itself is evaluated.
To overcome this you use a lambda (silly name but basically means anonymous function - or to put it even simpler 'a function that doesn't actually have a name') as follows:
[This is valid]
Rails 2: named_scope :upcoming_appts, lambda {:conditions => (['appt_dt > ?', Time.now])}
Rails 3: scope :upcoming_appts, lambda {where(['appt_dt >= ?', Time.now])}
This scope will now get evaluated at execution time each time - it is used - so Time.now will be the actual current date-time.

Related

Updating named_scope :all for new ActiveRecord format

I have been upgrading named scopes to scopes following this guide http://m.onkey.org/active-record-query-interface. The one thing I have run into in the code I am working with that I have not seen in any examples is how do deal with
named_scope :all, <ect>
The closest example I have seen is in the m.onkey.org guide which is
named_scope :red, :conditions => { :colour => 'red' }
to
scope :red, :conditions => { :colour => 'red' }
So would the same idea apply, making my new code
scope :all, <ect>
or am I missing something?
The scope .all is reserved since ActiveRecord allows you to retrieve all the entries with the same method:
User.all #=> returns ALL the users existing in the DB
You should rename your scope to a different name.
Also, you don't have to precise the conditions keyword. Here is part of codes I'm using in my projects:
scope :ordered, order(:name)
scope :with_color, lambda{ |color| where( color: color.try(:to_s) ) }
# this lambda scope is usable by:
# Model.with_color(:red)

Rails Quiz (find bugs in scope definition)

The following scope definition contains two bugs, which causes it not to work as expected. Can you find them?
named_scope :articles_to_display,
:conditions => ["articles.publish_at < (?)", Time.now]
(the column publish_at contains time/date, when the article should be published). The bugs are fundamental ones, not just a typos.
I will either accept the first correct answer or post the solution in few days.
The first problems is that Time.now is evaluated at the class level (when the file is read by Ruby) and not evaluated when the scope is used (which is what you most likely expect). In that case you need to wrap the conditions generation in a lambda/proc.
named_scope :articles_to_display, lambda {
:conditions => ["articles.publish_at < (?)", Time.now]
}
The second issue is likely that you're want to use Time.zone.now instead of Time.now to respect the localized time of the current request rather than the system time on the server.
The following is what you want to end up with:
named_scope :articles_to_display, lambda {
:conditions => ["articles.publish_at < (?)", Time.zone.now]
}
named_scope :articles_to_display, :conditions => ["articles.publish_at < (?)", DateTime.now]
Or try proc
named_scope :articles_to_display, proc{ :conditions => ["articles.publish_at < (?)", DateTime.now]}
Here was a picture of a superman, that was deleted :D
For additional reference, the accepted answer, converted to rails 3, is:
scope :articles_to_display, lambda {
where("articles.publish_at < (?)", Time.zone.now)
}
named_scope :articles_to_display,
:conditions => ["articles.publish_at IS NOT NULL AND articles.publish_at <= (?)", Time.now]

Rails 2.3.x - how does this ActiveRecord scope work?

There's a named_scope in a project I'm working on that looks like the following:
# default product scope only lists available and non-deleted products
::Product.named_scope :active, lambda { |*args|
Product.not_deleted.available(args.first).scope(:find)
}
The initial named_scope makes sense. The confusing part here is how .scope(:find) works. This is clearly calling another named scope (not_deleted), and applying .scope(:find) afterwards. What/how does .scope(:find) work here?
A quick answer
Product.not_deleted.available(args.first)
is a named-scope itself, formed by combining both named scopes.
scope(:find) gets the conditions for a named-scope (or combination of scopes), which you can in turn use to create a new named-scope.
So by example:
named_scope :active, :conditions => 'active = true'
named_scope :not_deleted, :conditions => 'deleted = false'
then you write
named_scope :active_and_not_deleted, :conditions => 'active = true and deleted = false'
or, you could write
named_scope :active_and_not_deleted, lambda { self.active.not_deleted.scope(:find) }
which is identical. I hope that makes it clear.
Note that this has become simpler (cleaner) in rails 3, you would just write
scope :active_and_not_deleted, active.not_deleted
Scope is a method on ActiveRecord::Base that returns the current scope for the method passed in (what would actually be used to build the query if you were to run it at this moment).
# Retrieve the scope for the given method and optional key.
def scope(method, key = nil) #:nodoc:
if current_scoped_methods && (scope = current_scoped_methods[method])
key ? scope[key] : scope
end
end
So in your example, the lambda returns the scope for a Product.find call after merging all the other named scopes.
I have a named_scope:
named_scope :active, {:conditions => {:active => true}}
In my console output, Object.active.scope(:find) returns:
{:conditions => {:active => true}}

Problem with Thinking Sphinx and scopes

For several hours now I am unsuccessfully trying to get sphinx scopes work.
I want to scope tags of ActsAsTaggableOn. In my model (that is taggable) I tried the following scopes:
# This normal scope works
scope :tagged, lambda {
joins(:taggings => :tag).
where("tags.name = 'consequatur'")
}
# fails! (can't convert ActiveRecord::Relation into Hash)
sphinx_scope :tagged do
joins(:taggings => :tag).
where("tags.name = 'consequatur'")
end
Another try with the old conditions:
# works with normal scope (returns one record)
scope :tagged, :joins => :taggings, :conditions => {"taggings.tag_id" => 74}
# fails! (returns nothing)
sphinx_scope(:tagged) do
{:joins => :taggings, :conditions => {"taggings.tag_id" => 74}}
end
How can I make those scopes work? Is there another way to archive that task? I want to only search those models that are tagged with a specific tag.

Conditions with Bind Variables and Optional Parameters

Let's say I have a form where users can search for people whose name starts with a particular name string, for example, "Mi" would find "Mike" and "Miguel". I would probably create a find statement like so:
find(:all, :conditions => ['name LIKE ?', "#{name}%"])
Let's say the form also has two optional fields, hair_color and eye_color that can be used to further filter the results. Ignoring the name portion of the query, a find statement for people that can take in an arbitrary number of optional parameters might look like this:
find(:all, :conditions => { params[:person] })
Which for my two optional parameters would behave as the equivalent of this:
find(:all, :conditions => { :hair_color => hair_color, :eye_color => eye_color })
What I can't figure out is how to merge these two kinds of queries where the required field, "name" is applied to the "like" condition above, and the optional hair_color and eye_color parameters (and perhaps others) can be added to further filter the results.
I certainly can build up a query string to do this, but I feel there must be a "rails way" that is more elegant. How can I merge mandatory bind parameters with optional parameters?
This is the perfect use of a named scope.
create a named scope in the model:
named_scope :with_name_like, lambda {|name|
{:conditions => ['name LIKE ?', "#{name}%"]}
}
At this point you can call
Model.with_name_like("Mi").find(:all, :conditions => params[:person])
And Rails will merge the queries for you.
Edit: Code for Waseem:
If the name is optional you could either omit the named scope from your method chain with an if condition:
unless name.blank?
Model.with_name_like("Mi").find(:all, :conditions => params[:person])
else
Model.find(:all, :conditions => params[:person])
end
Or you could redefine the named scope to do the same thing.
named_scope :with_name_like, lambda {|name|
if name.blank?
{}
else
{:conditions => ['name LIKE ?', "#{name}%"]}
end
}
Update
Here is the Rails 3 version of the last code snippet:
scope :with_name_like, lambda {|name|
if not name.blank?
where('name LIKE ?', "#{name}%")
end
}
To comply also with Waseem request, but allowing nil instead blank? (which is udeful in case you want to use "#things = Thing.named_like(params[:name])" directly)
named_scope :named_like, lambda do |*args|
if (name=args.first)
{:conditions => ["name like ?",name]}
else
{}
end
end
# or oneliner version:
named_scope :named_like, lambda{|*args| (name=args.first ? {:conditions => ["name like ?",name]} : {}) } }
I hope it helps

Resources