Rails 3 "scoped" value (scope vs class method) - ruby-on-rails

I would like to know, why scoped value is different in case of scope keyword and a class method
class A < ActiveRecord::Base
scope :first_scope, -> { where( "1=1" ) } # to be used by both
scope :my_scope, -> { p "S: #{ scoped.to_sql }"; where( "2=2" ) }
def my_scope_2
p "S: #{ scoped.to_sql }";
where( "2=2" )
end
end
And testing what is it going to print out:
A.first_scope.my_scope # "S: SELECT * FROM `A`"
A.first_scope.my_scope_2 # "S: SELECT * FROM `A` WHERE (1=1)
Although they produce the same relation object in the end: SELECT * FROM A WHERE (1=1) AND (2=2), the intermediate scoped object is NOT(?) correct for scope definition
Is that expected behaviour?
rails 3.2.21; ruby 2.1.5p273

Related

How to dynamically call scopes with 'OR' clause from an array

I have an array, and I'd like to call scopes with OR clause:
cars = ['bmw', 'audi', 'toyota']
class Car < AR
scope :from_bmw, -> { where(type: 'bmw') }
scope :from_audi, -> { where(type: 'audi') }
scope :from_toyota, -> { where(type: 'toyota') }
end
I'd like to achieve something like this:
Car.from_bmw.or(Car.from_audi).or(Car.from_toyota)
My cars array can change; in case: cars = ['toyota', 'audi'], my method should produce:
Car.from_toyota.or(Car.from_audi)
I have something like the following:
def generate(cars)
scopes = cars.map {|f| "from_#{f} "}
scopes.each do |s|
# HOW TO I ITERATE OVER HERE AND CALL EACH SCOPE???
end
end
I don't want to pass type as an argument to scope, there's a reason behind it.
def generate(cars)
return Car.none if cars.blank?
scopes = cars.map {|f| "from_#{f} "}
scope = Car.send(scopes.shift)
scopes.each do |s|
scope = scope.or(Car.send(s))
end
scope
end
Assuming the given array contains only valid type values, you could simply do that:
class Car
scope :by_type, -> (type) { where(type: type) }
end
types = ['bmw', 'audi', 'toyota']
Car.by_type(types) # => It'll generate a query using IN: SELECT * FROM cars WHERE type IN ('bmw', 'audi', 'toyota')
If you don't want to pass the array as an argument to scope for whatever reason, you could create a hash mapping the array values to valid by_type arguments.
VALID_CAR_TYPES = { volkswagen: ['vw', 'volkswagen'], bmw: ['bmw'], ... }
def sanitize_car_types(types)
types.map do |type|
VALID_CAR_TYPES.find { |k, v| v.include?(type) }.first
end.compact
end
Car.by_type(sanitize_car_types(types))

How to isolate a query inside a scope from pundit policy scope?

I'm using Rails 5 + Pundit gem and trying to fetch some chats with policy scope and model scope. Model scope has a query inside it and the problem is that policy scope applies to this inner query. The question is how to isolate the query from outer scope? Here's some code:
# model
scope :with_user, ->(user_id=nil) {
user_id ? where(chats: { id: User.find(user_id).chats.ids }) : all
}
# policy
class Scope < Scope
def resolve
if user.admin?
scope.all
else
scope.joins(:chat_users).where(chat_users: { user_id: user.id })
end
end
end
So I decided to output the inner sql query, which should get user chats' ids from the scope. I updated the model scope:
scope :with_user, ->(user_id=nil) {
puts User.find(user_id).chats.to_sql
where(chats: { id: User.unscoped.find(user_id).chats.ids } )
}
and here are results:
when I run ChatPolicy::Scope.new(User.first, Chat).resolve.with_user(358) I get:
SELECT "chats".* FROM "chats" INNER JOIN "chat_users"
"chat_users_chats" ON "chat_users_chats"."chat_id" = "chats"."id"
INNER JOIN "chat_users" ON "chats"."id" = "chat_users"."chat_id" WHERE
(chat_users.user_id = 350) AND "chat_users"."user_id" = 358
When I run Chat.with_user(358) I get:
SELECT "chats".* FROM "chats" INNER JOIN "chat_users" ON "chats"."id"
= "chat_users"."chat_id" WHERE "chat_users"."user_id" = 358
It generates the correct query if I run it without policy scope. Is there a workaround?
This is a Community Wiki answer replacing the answer having been edited into the original question as recommended by Meta.
This has been solved by the OP with a different unscoped model scope:
scope :with_user, ->(user_id=nil) {
user_id ? where(chats: { id: Chat.unscoped.joins(:chat_users).where(chat_users: { user_id: user_id }).ids } ) : all
}

Using another class scope in existing scope Activerecord

I want to use the scope of another class in the scope of the first class
so instead of
scope :active, -> {includes(:b).where(b: {column: 'ACTIVE'}).where(a: {column2: 'ACTIVE'})}
I want to be able to use a scope of b
scope :active, -> {includes(b.active).where(a: {column2: 'Active'})}
You can do this using merge:
scope :active, -> { includes(:b).merge(B.active)
.where(a: {column2: 'Active'}) }
Note: I used B to represent the model class for the b column or object.
Or, assuming you're in a's model already:
scope :active, -> { includes(:b).merge(B.active)
.where(column2: 'Active') }
Also, if you WANT eager loading then using includes is great. Otherwise, it's faster and less overhead to use joins, like this:
scope :active, -> { joins(:b).merge(B.active)
.where(column2: 'Active') }
I recommend to use scope on model, if it's admin specific, then can separate it to concern
http://api.rubyonrails.org/classes/ActiveSupport/Concern.html
module AdminUserScopes
extend ActiveSupport::Concern
included do
scope :admin_scope1, -> { includes(:b).where(b: {column: 'ACTIVE'}).where(a: {column2: 'ACTIVE'}) }
scope :admin_scope2, -> { admin_scope1.where(a: {column2: 'Active'}) }
end
end
# in your model
include AdminUserScopes
# in active_admin
scope :active, -> { admin_scope1 }
scope :active2, -> { admin_scope2 }
Upd:
If you want to use one condition to other model then can use merge
Dog.all.merge(User.males) # => select * from dogs where sex = 1;
If you want to use in association filtering, then:
Post.where(user: User.males) # => select * from posts where user_id in (select users.id from users where sex = 1)
In your case I guess you have A and B, and you want to get active A-records what connected to active B-records
# in A
scope :active, -> { where(column: 'ACTIVE') }
# in B
scope :active, -> { where(column2: 'ACTIVE', a: A.active) }
# in somewhere else
scope :active, -> { where(a: A.active) } # => have active A which have active B
p.s. it's much easier with more informative names, "A's" and "B's" are hard :)

Is it possible to have a scope with optional arguments?

Is it possible to write a scope with optional arguments so that i can call the scope with and without arguments?
Something like:
scope :with_optional_args, lambda { |arg|
where("table.name = ?", arg)
}
Model.with_optional_args('foo')
Model.with_optional_args
I can check in the lambda block if an arg is given (like described by Unixmonkey) but on calling the scope without an argument i got an ArgumentError: wrong number of arguments (0 for 1)
Ruby 1.9 extended blocks to have the same features as methods do (default values are among them):
scope :cheap, lambda{|max_price=20.0| where("price < ?", max_price)}
Call:
Model.cheap
Model.cheap(15)
Yes. Just use a * like you would in a method.
scope :print_args, lambda {|*args|
puts args
}
I used scope :name, ->(arg1, arg2 = value) { ... } a few weeks ago, it worked well, if my memory's correct. To use with ruby 1.9+
You can conditionally modify your scope based on a given argument.
scope :random, ->(num = nil){ num ? order('RANDOM()').limit(num) : order('RANDOM()') }
Usage:
Advertisement.random # => returns all records randomized
Advertisement.random(1) # => returns 1 random record
Or, you can provide a default value.
scope :random, ->(num = 1000){ order('RANDOM()').limit(num) }
Usage:
Product.random # => returns 1,000 random products
Product.random(5) # => returns 5 random products
NOTE: The syntax shown for RANDOM() is specific to Postgres. The syntax shown is Rails 4.
Just wanted to let you know that according to the guide, the recommended way for passing arguments to scopes is to use a class method, like this:
class Post < ActiveRecord::Base
def self.1_week_before(time)
where("created_at < ?", time)
end
end
This can give a cleaner approach.
Certainly.
scope :with_optional_args, Proc.new { |arg|
if arg.present?
where("table.name = ?", arg)
end
}
Use the *
scope :with_optional_args, -> { |*arg| where("table.name = ?", arg) }
You can use Object#then (or Object#yield_self, they are synonyms) for this. For instance:
scope :cancelled, -> (cancelled_at_range = nil) { joins(:subscriptions).merge(Subscription.cancelled).then {|relation| cancelled_at_range.present? ? relation.where(subscriptions: { ends_at: cancelled_at_range }) : relation } }

Multiple scope in rails 3.0

I am a beginner in Rails and i have a problem with scope.
I have my class with 2 scopes :
class Event < ActiveRecord::Base
belongs_to :continent
belongs_to :event_type
scope :continent, lambda { |continent|
return if continent.blank?
composed_scope = self.scoped
composed_scope = composed_scope.where('continent_id IN ( ? )', continent).all
return composed_scope
}
scope :event_type, lambda { |eventType|
return if eventType.blank?
composed_scope = self.scoped
composed_scope = composed_scope.where('event_type_id IN ( ? )', eventType).all
return composed_scope
}
end
And in my controller i want to use this 2 scopes at the same time. I did :
def filter
#event = Event.scoped
#event = #event.continent(params[:continents]) unless params[:continents].blank?
#event = #event.event_type(params[:event_type]) unless params[:event_type].blank?
respond_with(#event)
end
But i doesn't work, I have this error :
undefined method `event_type' for #<Array:0x7f11248cca80>
It's because the first scope return an array.
How can I do to make it work?
Thank you !
You should not append '.all' in your scopes:
It transforms a chainable ActiveRelation into an Array, by triggering the SQL query.
So simply remove it.
Bonus:
Some refactoring:
scope :continent, lambda { |continent|   
self.scoped.where('continent_id IN ( ? )', continent) unless continent.blank?
}
I don't think you need .scoped in your scopes.
def filter
#event = Event.scoped
#event = #event.continent(params[:continents]) unless params[:continents].blank?
#event = #event.event_type(params[:event_type]) unless params[:event_type].blank?
respond_with(#event)
end
on the code above you already have everything returning as 'scoped'.
Plus, your scopes wouldnt need an 'unless' on them, since they will only be called if your params arent blank. So your scopes could become something like this
scope :continent, lambda { |continent|
where('continent_id IN ( ? )', continent)
}
or, on a more Rails 3 way,
scope :continent, lambda { |continent_id|
where(:continent_id => continent_id)
}
which is much shorter :)

Resources