How to make a Rails 3 Dynamic Scope Conditional? - ruby-on-rails-3

I'd like to make a dynamic named scope in rails 3 conditional on the arguments passed in. For example:
class Message < AR::Base
scope :by_users, lambda {|user_ids| where(:user_id => user_ids) }
end
Message.by_users(user_ids)
However, I'd like to be able to call this scope even with an empty array of user_ids, and in that case not apply the where. The reason I want to do this inside the scope is I'm going to be chaining several of these together.
How do I make this work?

To answer your question you can do:
scope :by_users, lambda {|user_ids|
where(:user_id => user_ids) unless user_ids.empty?
}
However
I normally just use scope for simple operations(for readability and maintainability), anything after that and I just use class methods, so something like:
class Message < ActiveRecord::Base
def self.by_users(users_id)
if user_ids.empty?
scoped
else
where(:user_id => users_id)
end
end
end
This will work in Rails 3 because where actually returns an ActiveRecord::Relation, in which you can chain more queries.
I'm also using #scoped, which will return an anonymous scope, which will allow you to chain queries.
In the end, it is up to you. I'm just giving you options.

Related

Create scopes using instance methods best way

I was wondering what's the best way to make an scope using an instance method to filter records. This is my model:
class Promotion < ActiveRecord::Base
scope :availables, lambda{ all.select{ |obj| obj.is_available? } }
def is_available?
Date.today <= Date.strptime(valid_thru, '%m/%d/%Y')
...more validations here
end
end
The problem here is this scope returns an array instead of an ActiveRecord::Relation and I'm not able to chain other scopes.
Any suggestion guys?
There is no way to accomplish what you want. If you want to apply logic on ruby objects you can no longer return an ActiveRecord::Relation.
The only way to achieve something like this is have the logic on a database level. In that case you could use a class method to achieve what you want like so:
def availables
where('valid_thru => ?', Date.today)
end

Rails ActiveRecord query

My question is twofold... Primarily, I am trying to figure out how to ask > or < when filtering this query. You can see at the end I have .where(:created_at > 2.months.ago) and that is improper syntax, but I'm not sure the correct way to call something similar.
Secondly, this is a bit of a long string and is going to get longer as the are more conditions I have to factor in. Is there a cleaner way of building this, or is a long string of conditions like this pretty standard?
class PhotosController < ApplicationController
def showcase
#photos = Photo.order(params[:sort] || 'random()').search(params[:search]).paginate(:per_page => 12, :page => params[:page]).where(:created_at > 2.months.ago)
end
Thanks.
Unfortunately you've hit a sore point in the ActiveRecord querying api. There is no standard, out of the box way to do this. You can do date ranges very easily, but < and > have no easy path. However Arel, the underlying SQL engine, can do this very easily. You could write a simple scope to handle it thusly:
scope :created_after, lambda {|date| where arel_table[:created_at].gt(date) }
And you could refactor this easily to take a column, or gt versus lt, etc.
Other people have solved this problem already, however, and you could take advantage of their work. One example is MetaWhere, which adds a bunch of syntactic sugar to your queries. For example, using it you might write:
Article.where(:title.matches => 'Hello%', :created_at.gt => 3.days.ago)
On #2, scopes do tend to get long. You might look into the gem has_scope, which helps to alleviate this by defining scopes on the controller in an analogous way to how they are defined on the model. An example from the site:
# The model
# Note it's using old Rails 2 named_scope, but Rails 3 scope works just as well.
class Graduation < ActiveRecord::Base
named_scope :featured, :conditions => { :featured => true }
named_scope :by_degree, proc {|degree| { :conditions => { :degree => degree } } }
end
# The controller
class GraduationsController < ApplicationController
has_scope :featured, :type => :boolean
has_scope :by_degree
def index
#graduations = apply_scopes(Graduation).all
end
end
You can do where(["created_at > ?", 2.months.ago]) for your first question.
For your second question there are several solutions :
You can use scopes to embed the conditions in them and then combine them.
You can break the line in multiple lines.
You can keep it like this if you have a large screen and you don't work with any other people.

rails 3 - 'or' scopes

We are updating a rails 2 app. We have happily been using fake_arel which provides a very nice 'or' scope.
Now, with rails 3 I can't find a way to replicate this.
We have code like this:
scope.or(Event.horse, Event.dog, Event.trap).today
The model looks like this:
class Event < ActiveRecord::Base
named_scope :horse, lambda { {:conditions => ["sport_category_id in (?)", SportCategory.find_horse_ids] }}
named_scope :dog, lambda { {:conditions => ["sport_category_id in (?)", SportCategory.find_dog_ids] }}
named_scope :trap, lambda { {:conditions => ["sport_category_id in (?)", SportCategory.find_trap_ids] }}
end
These scopes need to be separate and are used all over the place. This model actually has dozens of scopes on it that are used in combination, so rewriting it all is the last thing we want to do.
It seems strange that you can't 'or' scopes together.
Can someone propose a way to do this as nicely in Rails 3? Even using arel I don't see how to.
We are using meta_where in a different project, but it doesn't offer any such thing either.
Well, the way to do that is, in your model (adapt it to your needs!) :
where(:event => ['horse', 'dog', 'trap'])
An array will produce a IN statement, which is what you want there. Furthermore, you can use rails 3 scopes to achieve that :
scope :my_scope, where(:event => ['horse', 'dog', 'trap'])
Then you can use it this way :
mymodel.my_scope # and possibility to chain, like :
mymodel.my_scope.where(:public => true)
I ripped out the 'or' function from fake_arel and got it working with rails 3.0x (not sure if it will work with 3.1 as we don't use that here)
I case anyone is interested I have put it in a gist:
I was unable to get the code from the Github-gist by Phil working, but it inspired me to come up with the following, which I think is a simpler, solution. It uses a class-method that returns an ActiveRecord::Relation class nonetheless.
def self.or alt_scope
either = scoped.where_clauses.join(' AND ')
alternative = alt_scope.where_clauses.join(' AND ')
Locatie.unscoped.where("(#{either}) OR (#{alternative})").
joins(scoped.joins_values).joins(alt_scope.joins_values).
group(scoped.group_values).group(alt_scope.group_values).
having(scoped.having_values).having(alt_scope.having_values).
includes(scoped.includes_values).includes(alt_scope.includes_values).
order(scoped.order_values).order(alt_scope.order_values).
select(scoped.select_values).select(alt_scope.select_values)
end
Just add this to your class. You'll then get the ability to create a multiple OR query as follows:
Event.horse.or(Event.dog).or(Event.trap)
Which you can consequently 'store' in a scope:
scope :horse_dog_or_trap, horse.or(Event.dog).or(Event.trap)
and also extend the scope even further, such as:
Event.horse.or(Event.dog).or(Event.trap).today

named_scope and .first?

I can return a collection of objects, with only one (:limit => 1) but is there a way to return the .first() object only, like not within a collection?
named_scope :profile, :conditions => {:association => 'owner', :resource_type => 'Profile'}, :limit => 1 # => collection of 1 profile but I want the profile only NOT in a collection or array
the workaround is simply to apply .first() to the results, but I'd just like to clean up the code and make it less error prone.
You'll probably need to create a class method instead:
def self.profile
where(:association => 'owner', :resource_type => 'Profile').first
end
Note that with Rails 3 you should be using the where(...) syntax, and that when doing .first, you don't need to specify the limit.
First off, if you're using Rails 3 you should be using scope instead of named_scope. Same thing, different, err, name (named_scope will still work, but it is deprecated). Now that that is out of the way…
A scope (or named scope) takes two arguments (a symbol and either a lambda or a hash) and defines a class method on that model that returns an ActiveRecord::Relation, which is why you're able to chain methods on it.
first, like find or all, returns an actual result from the database. For this reason it won't work in a scope.
All that said, you can define your own class method on your model that gives the behavior you're wanting (as 2 people already answered while I was typing this). This is actually recommended over using scopes by many well-respected devs in the Rails community. Since using the scope class macro just defines class methods itself anyways, there isn't really a downside to this, and it has the benefit of flexibility (like in your case here).
Define a class method to do this:
def profile
where(:association => "owner", :resource_type => 'Profile').first
end
The first already does an implicit limit 1 on the query, AND will order it by the primary key of the table so you'll always get the first.

Rails 3: how to write DRYer scopes

I'm finding myself writing very similar code in two places, once to define a (virtual) boolean attribute on a model, and once to define a scope to find records that match that condition. In essence,
scope :something, where(some_complex_conditions)
def something?
some_complex_conditions
end
A simple example: I'm modelling a club membership; a Member pays a Fee, which is valid only in a certain year.
class Member < ActiveRecord::Base
has_many :payments
has_many :fees, :through => :payments
scope :current, joins(:fees).merge(Fee.current)
def current?
fees.current.exists?
end
end
class Fee < ActiveRecord::Base
has_many :payments
has_many :members, :through => :payments
scope :current, where(:year => Time.now.year)
def current?
year == Time.now.year
end
end
Is there a DRYer way to write a scopes that make use of virtual attributes (or, alternatively, to determine whether a model is matched by the conditions of a scope)?
I'm pretty new to Rails so please do point out if I'm doing something stupid!
This in not an answer to the question, but your code has a bug (in case you use something similar in production): Time.now.year will return the year the server was started. You want to run this scope in a lambda to have it behave as expected.
scope :current, lambda { where(:year => Time.now.year) }
No, there's no better way to do what you're trying to do (other than to take note of Geraud's comment). In your scope you're defining a class-level filter which will generate SQL to be used in restricting the results your finders return, in the attribute you're defining an instance-level test to be run on a specific instance of this class.
Yes, the code is similar, but it's performing different functions in different contexts.
Yes, you can use one or more parameters with a lambda in your scopes. Suppose that you have a set of items, and you want to get back those that are either 'Boot' or 'Helmet' :
scope :item_type, lambda { |item_type|
where("game_items.item_type = ?", item_type )
}
You can now do game_item.item_type('Boot') to get only the boots or game_item.item_type('Helmet') to get only the helmets. The same applies in your case. You can just have a parameter in your scope, in order to check one or more conditions on the same scope, in a DRYer way.