Merge methods to one hash, through method chaining - ruby-on-rails

I have a class which is generating queries for a db:
They should concatenated, so I do not repeat my code for a query (DRY).
I thought I could do something like this:
ruby
class Blah < ActiveRecord::Base
def no_scope
{scope: false}
end
def for_user(user_id)
{user_id: user_id}
end
end
Now my query
Blah.no_scope.for_user(1)
RESULT SHOULD BE A HASH:
{user_id: 1, scope: false}

Did you try to achieve this?
class Blah < ActiveRecord::Base
scope :no_scope, -> { where(scope: false) }
scope :for_user, -> (id) { where(user_id: id) }
end
Blah.no_scope.for_user(1) # where(scope: false, user_id: 1)

You just need to create scopes.
class Blah < ActiveRecord::Base
scope :no_scope, -> {all}
scope :for_user, -> (user_id) {where(user_id: user_id)}
end
Blah.no_scope.for_user(1) # where(user_id: 1)
See also scopes

To give the result as a real Hash instead of a Activerecord relation, you do it like this
class Blah < ActiveRecord::Base
scope :no_scope, ->{where(scope: false)}
scope :for_user, ->(id){where(user_id: id)} # no space in ->(id)
end
# give results as an array of hashes
Blah.no_scope.for_user(1).map { |e| {scope: e.scope, user_id: e.user_id} }
gives
[{"scope"=>false, "user_id"=>1}]

Related

How to unscope multiple models in rails?

I am trying to unscope multiple model as below
User Model which has acts_as_paranoid
class User
acts_as_paranoid
has_one :category
has_one :brand
has_one :item
INDEXED_FIELDS = {
only: [:name],
include: {
category: { only: [:name] },
item: { only:[:name] },
brand: { only: [:name]},
}
}
def custom_json
Category.unscoped do
Item.unscoped do
Brand.unscoped do
self.as_json(INDEXED_FIELDS)
end
end
end
end
end
User model has following association which also has acts_as_paranoid
Sample Category model, Brand and Item model have same code
class Category
acts_as_paranoid
belongs_to :user
end
Can I do this dynamically with 'N' number of models, like iterating over array as below
def custom_json
[Category, Item, Brand].each do
# do unscoping
end
end
Association looks like
I think the approach you may have is to unscope the class manually, by setting default_scopes to [], and then putting it back.
classes_to_unscope = [Category, Item, Brand]
# remove default_scopes, saving them in previous_scopes
previous_scopes = classes_to_unscope.map do |klazz|
scopes = klazz.default_scopes
klazz.default_scopes = []
scopes
end
self.as_json(INDEXED_FIELDS)
# put default_scopes back
classes_to_unscope.each_with_index do |klazz, i|
klazz.default_scopes = previous_scopes[i]
end
As extra method:
def unscope_all(*models, &block)
# the order does not matter, but preserve it
blocks = [block] + models.reverse.map do |model|
proc do |inner_block|
model.unscoped { inner_block.call }
end
end
blocks.inject do |inner, outer|
proc { outer.call(inner) }
end.call
end
Then you would use it:
unscope_all(Category, Item, Brand) do
# do unscoping
end
unscoped pitfall: when leaving the block you loose the "unscopability", so make sure you don't return a relation (it won't be unscoped). Instead you have to resolve it in the block (e.g. by returning an array where(...).to_a.

How to convert a method into a scope.

Hello I am trying to convert the method self.liked_by(user) into a scope. I am not entirely sure what my instructor is asking for so any interpretations on the question are greatly appreciated.
this is the method in question that I am supposed to turn into a scope.
def self.liked_by(user)
joins(:likes).where(likes: { user_id: user.id })
end
this is where the method appears in the model
class Bookmark < ActiveRecord::Base
belongs_to :user
belongs_to :topic
has_many :likes, dependent: :destroy
before_validation :httpset
validates :url, format: { with: /\Ahttp:\/\/.*(com|org|net|gov)/i,
message: "only allows valid URLs." }
def self.liked_by(user)
joins(:likes).where(likes: { user_id: user.id })
end
def httpset
if self.url =~ /\Ahttp:\/\/|\Ahttps:\/\//i
else
if self.url.present?
self.url = "http://"+ self.url
else
self.url = nil
end
end
end
end
And this is where the method is called in the controller
class UsersController < ApplicationController
def show
user = User.find(params[:id])
#bookmarks = user.bookmarks
#liked_bookmarks = Bookmark.liked_by(user)
end
end
Thanks for looking at my problem and have a good day.
#liked_bookmarks = Bookmark.liked_by(user)
In this line, in the same way you send the user parameter to a method, the same way you can send it to a scope.
class Bookmark < ActiveRecord::Base
---------
---------
scope :liked_by, ->(user) { joins(:likes).where(likes: { user_id: user.id }) }
---------
---------
end
the parameter you sent from the scope call can be accessed using the (user{or any name) in the scope
reference of scopes
As Owen suggested, read the docs to understand what scopes are. It is just another syntax to define your model's class methods (just like the one you already have).
scope :liked_by, ->(user) { joins(:likes).where(likes: { user_id: user.id }) }

Rails scope filter/search from association

I have a one-to-one association between PublicKey and PublicKeyRole. The PublicKey model has a column, role_id, and the PublicKeyRole model has role column which is a string, such as 'super'.
I want to be able to search by role string via a url query param, like; https://api.domain.com/public_keys?role=admin. I've tried this on the PublicKey model but I'm not sure where to pass the query in to:
scope :filter_by_role, lambda { |query|
joins(:public_key_role).merge(PublicKeyRole.role)
}
Here are my models:
class PublicKey < ActiveRecord::Base
belongs_to :user
belongs_to :public_key_role, foreign_key: :role_id
def self.search(params = {})
public_keys = params[:public_key_ids].present? ? PublicKey.where(id: params[:public_key_ids]) : PublicKey.all
public_keys = public_keys.filter_by_role(params[:role]) if params[:role]
public_keys
end
end
class PublicKeyRole < ActiveRecord::Base
has_one :public_key
end
Also, here's my test:
describe '.filter_by_role' do
before(:each) do
#public_key1 = FactoryGirl.create :public_key, { role_id: 1 }
#public_key2 = FactoryGirl.create :public_key, { role_id: 2 }
#public_key3 = FactoryGirl.create :public_key, { role_id: 1 }
end
context "when a 'super' role is sent" do
it 'returns the 2 matching public keys results' do
expect(PublicKey.filter_by_role('super').size).to eq(2)
end
it 'returns the matching public keys' do
expect(PublicKey.filter_by_role('super').sort).to match_array([#public_key1, #public_key3])
end
end
end
Update
I was missing the following in my spec, #lcguida's answer works.
FactoryGirl.create :public_key_role, { id: 1, role: 'super' }
From the docs you have some examples. You can search in the relation passing the query to the PublicKeyRole relation:
scope :filter_by_role, lambda { |query|
joins(:public_key_role).merge(PublicKeyRole.where(role: query))
}
If you want a string search (as LIKE):
scope :filter_by_role, lambda { |query|
joins(:public_key_role).merge(PublicKeyRole.where(PublicKeyRole.arel_table[:role].matches(query)))
}

Trouble translating SQL query to Arel

I have a search page that narrows down the list of a specific class, and I want an OR condition that can grab two different conditions and add the together, for example, I have classes
model/party.rb
class Party < ActiveRecord::Base
has_many :invitations
end
mode/invitation.rb
class Invitation < ActiveRecord::Base
belongs_to :party
end
invitation has a status attribute, which will be "decline", "accept", or "unanswered"
What I want to do is grab all the parties that do not have any invitations, or any that have all of the invitations "unanswered".
I currently do
scope :not_confirmed, lambda { find_by_sql( "SELECT * FROM `parties` INNER JOIN `invitations` ON `invitations`.`party_id` = `parties`.`id` WHERE (invitations.status = 'unanswered') OR (parties.id NOT IN (SELECT DISTINCT(party_id) FROM invitations))" ) }
which works, but since it does not lazy load I can't add them in a facet like query.
I did something like
no_invitations.or(no_one_has_answered)
but it did not work.
I especially do not get the concept of using OR on AREL, could someone please help out?
edited:
For a very ugly yet functional work around until I get this down, here is what I have done
party.rb
scope :not_confirmed, lambda { joins(:invitations).where( "invitations.status NOT IN (?)", ["accepted", "declined" ] ) }
scope :with_no_invitations, lambda { includes(:invitaions).where( :invitations => { :party_id => nil } ) }
search_controller.rb
#parties = Party.all_the_shared_queries
#parties = ( #parties.not_confirmed + #parties.with_no_invitations).uniq
The query:
scope :not_confirmed, lambda { find_by_sql( "SELECT * FROM `parties` INNER JOIN `invitations` ON `invitations`.`party_id` = `parties`.`id` WHERE (invitations.status = 'unanswered') OR (parties.id NOT IN (SELECT DISTINCT(party_id) FROM invitations))" ) }
can be converted to arel with some transformation using boolean algebra too. But since it is only theoretical conversion, you have to verify it manually. So:
class Invitation < ActiveRecord::Base
belongs_to :party
scope :non_answered, -> { where(arel_table[:status].not_eq('unanswered')) }
end
class Party < ActiveRecord::Base
has_many :invitations
scope :not_confirmed, -> { not.join(:invitaions).merge(Invitation.non_answered)) }
end
Please test it and comment here.
Firstly, from the question tags, I have assumed that you are using Rails3 (had it been Rails4, there were more easy ways of doing things :))
For your requirement above (ie grab all the parties that do not have any invitations, or any that have all of the invitations "unanswered"), here is a way of doing it (using scope :unattended):
Party Model:
class Party < ActiveRecord::Base
has_many :invitations
scope :invitations_answered, -> { joins(:invitations).merge(Invitation.answered) }
scope :unattended, -> { where(arel_table[:id].not_in invitations_answered.pluck(:id)) }
end
Invitation Model:
class Invitation < ActiveRecord::Base
belongs_to :party
scope :answered, -> { where(status: ["decline", "accept"])}
end
In Rails 4, you can use where.not and simplify it further like this:
Party Model:
class Party < ActiveRecord::Base
has_many :invitations
scope :invitations_answered, -> { joins(:invitations).merge(Invitation.answered) }
scope :unattended, -> { where.not(id: invitations_answered.pluck(:id)) }
end
Invitation Model:
class Invitation < ActiveRecord::Base
belongs_to :party
scope :answered, -> { where.not(status: 'unanswered') }
end

Passing arguments to scope_procedure in searchlogic

I'd like to use searchlogic's scope_procedure feature like so
class MyModelObject < ActiveRecord::Base
scope_procedure :my_scope_proc, lambda { |p1, p2| { :conditions => "p1 >= #{p1} AND p2 < #{p2}" }}
end
Then, I am doing the search:
scope = MyModelObject.search(:my_scope_proc => true)
scope.all
The above code obviously doesn't work because I didn't pass p1 and p2 parameters to my named scope.
I can't figure out how to pass parameters to the named scope.
A wild guess is (meaning, I didn't check!):
scope = MyModelObject.search(:my_scope_proc => [p1, p2])
scope.all
One suggestion for the scope_procedure:
class MyModelObject < ActiveRecord::Base
scope_procedure :my_scope_proc, lambda { |p1, p2| { :conditions => ["p1 >= ? AND p2 < ?", p1, p2] }}
end
This prevents SQL injection.
Or the searchlogic way:
class MyModelObject < ActiveRecord::Base
scope_procedure :my_scope_proc, lambda { |p1, p2| p1_gte(p1).p2_lt(p2) }
end
I was able to make it work with one parameter as follows:
class MyModelObject < ActiveRecord::Base
scope_procedure :my_scope_proc, lambda { |p| p1_gte(p[0]).p2_lt(p[1]) }
end
scope = MyModelObject.search(:my_scope_proc => [p1, p2])
scope.all

Resources