Rails model scope based on associated model - ruby-on-rails

I have two relevant models here: InventoryItem and Store.
class InventoryItem < ActiveRecord::Base
belongs_to :store
include Tire::Model::Search
include Tire::Model::Callbacks
def self.search(params)
tire.search(load: true, :per_page => 40) do
query { string params[:query], default_operator: "AND" } if params[:query].present?
end
end
end
class Store < ActiveRecord::Base
geocoded_by :address
after_validation :geocode, :if => lambda{ |obj| obj.address_changed? }
has_many :inventory_items
end
I want to search inventory_items from stores that are nearby the current user (User is also geocoded using geocoder gem). The approach I'm considering is to scope the model somehow, but I'm unsure of how to implement. Here's how I find nearby stores in my controllers typically:
#stores = Store.near([current_user.latitude, current_user.longitude], current_user.distance_preference, :order => :distance)
Is there a way to do a :local scope on my InventoryItem model somehow based on whether its associated store is nearby the current user? Alternatively, is there a way to accomplish my goal with elasticsearch? Thanks in advance!

There are a few ways you could accomplish this. I think the easiest way to start would be something like this:
class Store < ActiveRecord::Base
scope :for_user, -> {|user| near([user.latitude, user.longitude], user.distance_preference, :order => :distance)}
end
Which would allow you to call
Store.for_user(whatever_user)
On the Inventory, I would also try to find by stores:
class InventoryItem < ActiveRecord::Base
scope :for_stores, -> {|stores| where(store_id: stores.map(&:id))}
end
I am not familiar with your search engine there, but if it does active record scope nesting, you should be able to do something like:
InventoryItem.for_stores(Store.for_user(whatever_user)).search(...)

Related

Order by field in related model with ActiveRecord condition

I am trying to order by a field in a related model in Rails. All of the solutions I have researched have not addressed if the related model is filtered by another parameter?
Item model
class Item < ActiveRecord::Base
has_many :priorities
Related Model:
class Priority < ActiveRecord::Base
belongs_to :item
validates :item_id, presence: true
validates :company_id, presence: true
validates :position, presence: true
end
I am retrieving Items using a where clause:
#items = Item.where('company_id = ? and approved = ?', #company.id, true).all
I need to order by the 'Position' column in the related table. The trouble has been that in the Priority model, an item could be listed for multiple companies. So the positions are dependent on which company_id they have. When I display the items, it is for one company, ordered by position within the company. What is the proper way to accomplish this? Any help is appreciated.
PS - I am aware of acts_as_list however found it did not quite suit my setup here, so I am manually handling saving the sorting while still using jquery ui sortable.
You could use the includes method to include the build association then order by it. You just make sure you disambiguate the field you are ordering on and there are some things you should read up on here on eager loading. So it could be something like:
#items = Item.includes(:priorities).where('company_id = ? and approved = ?', #company.id, true).order("priorities.position ASC")
class Item < ActiveRecord::Base
has_many :priorities
belongs_to :company
def self.approved
where(approved: true)
end
end
class Priority < ActiveRecord::Base
belongs_to :item
end
class Company < ActiveRecord::Base
has_many :items
end
#company = Company.find(params[:company_id])
#items = #company.items.joins(:priorities).approved.order(priorities: :position)
If I've understood your question, that's how I'd do it. It doesn't really need much explanation but lemme know if you're not sure.
If you wanted to push more of it into the model, if it's a common requirement, you could scope the order:
class Item < ActiveRecord::Base
has_many :priorities
belongs_to :company
def self.approved
where(approved: true)
end
def self.order_by_priority_position
joins(:priorities).order(priorities: :position)
end
end
and just use: #company.items.approved.order_by_priority_position

How do you concatenate two active record results to return a new result that can further be filtered?

Imagine the scenario...
#models/user.rb
class User < ActiveRecord::Base
has_many :accounts, :conditions => { :active => 1 }
end
#models/account.rb
class Account < ActiveRecord::Base
belongs_to :user
def public_accounts
Account.all :conditions => { public => true }
end
end
Now imagine I want to concatenate User(:id).accounts with Account.public_accounts to show a list of all accounts available to a user.
So you'd think I'd be able to update the User model to look like this.
#models/user.rb
class User < ActiveRecord::Base
has_many :accounts, :conditions => { :active => 1 }
def all_accounts
self.accounts + Account.public
end
end
However, now I won't be able to use the all() method since it's no longer of that type of object.
In the controller I'd like to do this...
#controllers/accounts_controller.rb
def search_all
User.find(params[:user_id]).all_accounts.all(
:offset => params[:offset],
:limit => params[:limit]
)
end
Thoughts?
Update #1:
Scope's won't work for my scenario. I simplified my scenario to try and get my point across. As stated I need a way to combine two active record results and retain the ability to further filter them in my controller.
So the question is, "Why?" The reason is, I am trying to combine two sets of records to form a complete collection and one of the collections is not associated with the user at all.
I have refactored the above scenario to try and show a more precise example without getting overly complicated.
This might be a good scenario to use scopes.
You can define active and inactive scopes in the Account model and then use the following:
User.accounts
User.accounts.active
User.accounts.inactive
You can even chain scopes together, so you could do something like:
User.accounts.active.paid_up
jklina's answer is correct, it's best to use scopes in this situation. Scopes provide a sugary syntax and are more readable. I'll elaborate on the setup:
class User < AR::Base
has_many :accounts
end
class Account < AR::Base
belongs_to :user
scope :active, where(:active => true)
scope :inactive, where(:active => false)
end
You would then access the account scopes as jklina showed: User.find(1).accounts.active, etc.
Accessing all of a user's accounts like: User.find(1).accounts.
UPDATE:
I fixed some mistakes and added more below.
Based on the updates to your question, I think you need to make the public method a method on the class:
class Accounts < AR::Base
...
# This is essentially a scope anyways
def self.public
where(:public => true)
end
end
class User < AR::Base
...
# This should return all the users accounts
# and any public accounts
def all_accounts
Account.where("user_id = ? OR public is true", self.id)
end
end
Lets look at the return values in the chain:
User.find(params[:user_id]) # returns an instance of User class
User.find(params[:user_id]).all_accounts # returns an array
The Array class doesn't have an instance method called all that's why you are seeing this error. This is not a bug.
Why don't you try this:
class User
has_many :accounts, :conditions => { :active => 1 }
has_many :all_accounts :conditions => ["(active = ? OR public = ?)",
true, true]
end
Now you can:
User.find(params[:user_id]).all_accounts.all(:limit => 10, :offset => 2)
You're try to access two distinct tables and apply LIMIT/OFFSET to them as a combined union. That aint gonna happen unless you logically combine them at the SQL layer, not at the ActiveRecord layer.
Sounds like writing out the SQL, maybe using a UNION and then using find_by_sql might be your best best.

Rails: How can I eager load associations with sorting through instance an method?

class Newsroom < ActiveRecord::Base
has_many :blog_posts
has_many :quote_posts
end
class BlogPost < ActiveRecord::Base
belongs_to :newsroom
end
class QuotePost < ActiveRecord::Base
belongs_to :newsroom
end
I would like to have an instance method, such that I could do #newsroom.posts to get a collection of blog_posts and quote_posts sorted by created_at.
def posts
#posts ||= #load and sort blog_posts, quote_posts, etc
end
What is the best and most efficient way to accomplish this? I have looked into using default_scope, something like:
default_scope :include => [:blog_posts, :quote_posts]
def posts
#posts ||= [blog_posts + quote_posts].flatten.sort{|x,y| x.created_at <=> y.created_at}
end
But I would rather keep the sorting at the database level, if possible. Any suggestions on how to accomplish this? Thanks.
Try something like this:
#app/models/newsroom.rb
scope :ordered_posts, lambda {
includes(:blog_posts,:quote_posts) & BlogPost.order("created_at asc") & QuotePost.order("created_at asc")
}
ARel should be able to handle the ordering of included Quote and Blog Posts. You could clean that up slightly by having scopes in both the BlogPost and QuotePost model that order by created_at and then use those scopes in the Newsroom#ordered_posts method.
I ended up using a polymorphic post model. This seems to give me what I want with the insignificant downside of having an extra model/table. I used delegate to hand off specific attribute getter methods to the correct model.
class Newsroom < ActiveRecord::Base
has_many :posts
end
class Post < ActiveRecord::Base
belong_to :blog_post, :polymorphic => true
delegate :title, :author, :etc, :to => :postable
end
class BlogPost < ActiveRecord::Base
has_one :post, :as => :postable
end

Model Relationship Problem

I am trying to calculate the average (mean) rating for all entries within a category based on the following model associations ...
class Entry < ActiveRecord::Base
acts_as_rateable
belongs_to :category
...
end
class Category < ActiveRecord::Base
has_many :entry
...
end
class Rating < ActiveRecord::Base
belongs_to :rateable, :polymorphic => true
...
end
The rating model is handled by the acts as rateable plugin, so the rateable model looks like this ...
module Rateable #:nodoc:
...
module ClassMethods
def acts_as_rateable
has_many :ratings, :as => :rateable, :dependent => :destroy
...
end
end
...
end
How can I perform the average calculation? Can this be accomplished through the rails model associations or do I have to resort to a SQL query?
The average method is probably what you're looking for. Here's how to use it in your situation:
#category.entries.average('ratings.rating', :joins => :ratings)
Could you use a named_scope or custom method on the model. Either way it would still require some SQL since, if I understand the question, your are calculating a value.
In a traditional database application this would be a view on the data tables.
So in this context you might do something like... (note not tested or sure it is 100% complete)
class Category
has_many :entry do
def avg_rating()
#entries = find :all
#entres.each do |en|
#value += en.rating
end
return #value / entries.count
end
end
Edit - Check out EmFi's revised answer.
I make no promises but try this
class Category
def average_rating
Rating.average :rating,
:conditions => [ "type = ? AND entries.category_id = ?", "Entry", id ],
:join => "JOIN entries ON rateable_id = entries.id"
end
end

Rails: order using a has_many/belongs_to relationship

I was wondering if it was possible to use the find method to order the results based on a class's has_many relationship with another class. e.g.
# has the columns id, name
class Dog < ActiveRecord::Base
has_many :dog_tags
end
# has the columns id, color, dog_id
class DogTags < ActiveRecord::Base
belongs_to :dog
end
and I would like to do something like this:
#result = DogTag.find(:all, :order => dog.name)
thank you.
In Rails 4 it should be done this way:
#result = DogTag.joins(:dog).order('dogs.name')
or with scope:
class DogTags < ActiveRecord::Base
belongs_to :dog
scope :ordered_by_dog_name, -> { joins(:dog).order('dogs.name') }
end
#result = DogTags.ordered_by_dog_name
The second is easier to mock in tests as controller doesn't have to know about model details.
You need to join the related table to the request.
#result = DogTag.find(:all, :joins => :dog, :order => 'dogs.name')
Note that dogs is plural in the :order statement.

Resources