Rails 4: Model Logic - (Limited Associations) - ruby-on-rails

Is there a way to use a similar command to .where() in the Model?
In my instance I want to loop through items but I want to check for a database value first:
User Model:
has_many :purchases
has_many :purchasedtools, through: :purchases, source: :tool #only where "status: "Completed"!
Purchase Model:
belongs_to :user
belongs_to :tool
Tool Model:
belongs_to :user
has_many :purchases
Tools Controller:
def index
#boughttools = current_user.purchasedtools
end
Tool Index View:
- #boughttools.limit(4).each do |tool|
I expect something like: #boughttools = current_user.purchases.where(status: "Completed", user_id: current_user.id).The only difference is that I need purchasedtools instead of purchases because I want the actual Tools as source.Fortunately I can't just change this as is because the db values I check are on the purchase table and not on the tool table.
Thanks in advance for each answer! Please tell me if you need additional information.

As I suggested in my comment, you might be looking for a conditional association:
has_many :purchasedtools,
-> { where(status: 'Completed') }
through: :purchases,
source: :tool
Check out docs for more info on associations.

To add a scope to an association you simply pass a lambda or proc to the association macro:
class User
has_many :purchases
has_many :completed_purchases, ->{ where(status: 'Complete') },
class_name: 'Purchase'
has_many :purchased_tools
through: :completed_purchases
source: :tool
end
Added
A simpler way to do this as noticed by the OP would just be to specify that the condition applies to the join table:
has_many :purchased_tools, -> { where(purchases: {status: "Completed"} ) }
through: :completed_purchases
source: :tool
See Scopes for has_many

You can use joins
Purchase.joins(:tool).where(tools: {user_id: current_user.id}, purchases: {status: 'Completed'})

Related

Activerecord has_many :through through multiple models

I'm trying to access all comments from a given user with user.comments. The query is to go through two different models, which likely both return results. My relations are set up as follow:
class User < ActiveRecord::Base
has_many :organisers
has_many :participants
has_many :comments, through: :participants / :organisers (see explenation below)
end
class Organiser < ActiveRecord::Base
belongs_to :user
end
class Participant < ActiveRecord::Base
belongs_to :user
end
class Comment < ActiveRecord::Base
belongs_to :organiser
belongs_to :participant
end
A comment is validated to belong to either a participant, or an organiser.
I'm not sure how to go about this. I've tried
has_many :comments, through: :participants
has_many :comments, through: :organisers
and
has_many :comments, through: [:organisers, :participants]
But that last one isn't rails. Is there a proper way to do this? Thanks!
has_many :comments, ->(user) {
unscope(where: :user_id).
left_joins(:organizer, :participant).
where('organizers.user_id = ? OR participants.user_id = ?', user.id, user.id)
}
The unscope is to remove the comments.user_id = ? clause (which is added by default when you define a has_many relation).
The left_joins is called on Comment, so you need to pass in the relation names as defined on Comment, hence the singulars in this example.
I found a solution after many tries. You can use a scope with param in your last has_many sentence in the User model:
has_many :comments, -> (user) {where organiser: user.organisers}, through: :participants
The "user" param represet the User object whom is calling the comments method.
For anyone coming across this using polymorphic associations, the following worked for me inspired by magni- and Carlos Jimenez' answers:
has_many :comments, -> (user) {
unscope(where: :user_id).
where(commentable: [user.organizers, user.participants])
}
Since we couldn't use has_many, through here because comments come from both of organisers and participants. I just think there are 2 solutions here:
Solution #1 Define comments method:
class User < ActiveRecord::Base
def comments
Comment.joins([{organiser: :user}, {participant: :user}])
.where(users: {id: self.id})
end
end
So then your query to find comments is:
User.first.comments
Solution #2 Use scope in Comment
class Comment < ActiveRecord::Base
scope :from_user, -> (user) {
joins([{organiser: :user}, {participant: :user}]).where(users: {id: user.id})
}
end
So your query will be like:
user = User.first
comments = Comment.from_user(user)
Since we couldn't use has_many, through here because comments come from both of organizers and participants. I just think there are 2 solutions here:
Basically you can still change the foreign key to accept the self.id automatically with Rails here
User.first.comments
class User
has_many :comments, -> { joins([{ organiser: :user }, { participant: :user }]) }, through: :participants, foreign_key: "users.id"
end
I believe your associations would be confused, as user.comments wouldn't know whether it's going through Participant or Organiser, so the best option would be to declare two different joins (with different names):
http://guides.rubyonrails.org/association_basics.html#self-joins

Active record query using joins

I have three different models User, Order, Product:-
class User < ActiveRecord::Base
has_many :orders
has_many :products
end
class Product < ActiveRecord::Base
has_many :orders
end
class Order < ActiveRecord::Base
belongs_to :user
belongs_to :product
end
How to find all the products which an user has ordered using a one line active-record query?
Another options here is using has_many :through relation:
Add to User model
has_many :products, through: :orders
and now user.products does the trick
This is explained in the ActiveRecord documentation:
Product.joins(:orders).where(orders: { user_id: user.id })
I got it finally through this
Product.joins(:orders).where(order: { user_id: user.id })

How to sort associated models by column in join model (:through)

I have 3 models:
Event, Lineup, Artist
class Event
has_many :lineups
has_many :artists, -> { uniq }, through: :lineups
end
class Artist
has_many :events, through: :lineups
has_many :lineups
end
class Lineup
belongs_to :event
belongs_to :artist
end
Lineup has artist_order integer column.
What can I do to have Event.last.artists sorted by lineup's artist_order column by default?
You can use default_scope for Lineup model:
class Lineup
default_scope { order(:artist_order) } # or order('artist_order DESC')
end
This way whenever you call lineup collection, it is ordered by the artist_order column.
Other way is to specify the order directly on has_many relation:
has_many :lineups, -> { order(:artist_order) }
The latter might be the better option, because default_scope is sometimes considered to be a bad idea.
I think you can just add -> { order "artist_order ASC" } to the lineups association in Artist model
Artist:
has_many :events, through: :lineups
has_many :lineups, -> { order "artist_order ASC" }
Now Event.last.artists should give you the results sorted by lineup's artist_order
PS: Not Tested.
You can add Order in any association so just add order in front of lineups association.
has_many :lineups,-> { order(:artist_order) }
Check this http://apidock.com/rails/v4.2.1/ActiveRecord/Associations/ClassMethods/has_many for complete reference.

How can I set up conditional associations in Rails?

So I have two associated models Users and Magazines. They are associated by a has_many :through relationship via Subscriptions. So this is what it looks like:
class User < ActiveRecord::Base
has_many :subscriptions
has_many :magazines, :through => :subscriptions
end
class Subscription < ActiveRecord::Base
belongs_to :user
belongs_to :magazine
end
class Magazine < ActiveRecord::Base
has_many :subscriptions
has_many :users, :through => :subscriptions
end
Users have a boolean attribute called paid. I only want users where paid == true to be able to subscribe to any magazines. How can I set up a conditional association like this? Thanks in advance for the help!
For the associations, you can pass a lambda before the hash options. In this lambda you can specify conditions, ordering, etc.
class Magazine < ActiveRecord::Base
has_many :subscriptions
has_many :users, ->{ where(users: {paid: true}) }, through: :subscriptions
end
For users, you could do something like this:
class User < ActiveRecord::Base
has_many :subscriptions
has_many :magazines, through: :subscriptions
def magazines
self.paid ? super : Magazine.none
end
end
While it's neat, the above might behave unpredictably when you're joining tables e.g. User.joins(:magazines) might ignore the paid condition or crash.
A better alternative:
class User < ActiveRecord::Base
has_many :subscriptions
has_many :magazines,
->{ where("subscriptions.user_id IN (
SELECT id FROM users WHERE users.paid = 't')") },
through: :subscriptions
end
That where in the lambda might be replaceable by a joins like so:
joins(subscriptions: :user).where(users: {paid: true})
But I think that'll join subscriptions twice - once for the lambda and once for the association. I'm not certain. If so, and if that bothers you, then:
joins("INNER JOIN users ON (users.id = subscriptions.user_id)").
where(users: {paid: true})
or:
joins("INNER JOIN users ON (
(users.id = subscriptions.user_id) AND (users.paid = 't')
)")
Also, I think you mislabeled your Subscription model as Registration

rails polymorphic with includes based on type of class

Let's say we have these models
class Message
belongs_to :messageable, polymorphic: true
end
class Ticket
has_many :messages, as: :messageable
has_many :comments
end
class User
has_many :messages, as: :messageable
has_many :ratings
end
class Rating
belongs_to :user
end
class Comment
belongs_to :ticket
end
Now I want to load all messages (which have associated tickets or users), and eager load depending on the type of class, either comments for tickets and ratings for users
Of course Message.includes(:messageable).order("created_at desc") will only include the directly associated object, but the question would be how to include the different association types that derive from each model type (i.e. in this example, how to eager load comments for tickets and ratings for users)?
This is just a simple example, but what about even more complicated cases, where I'd like to include something else for the user, another association, and what if that association needs more includes?
The only way I can think of to do this is to duplicate the associations on each model with a common name:
class Ticket
has_many :messages, as: :messageable
has_many :comments
has_many :messageable_includes, class_name: "Comment"
end
class User
has_many :messages, as: :messageable
has_many :ratings
has_many :messageable_includes, class_name: "Rating"
end
Message.includes(:messageable => :messageable_includes) ...
I'm not sure I would use this strategy as a widespread solution, but if this is a complicated as your case gets, it may work for you.
I've used the following helper methods in my own project:
def polymorphic_association_includes(association, includes_association_name, includes_by_type)
includes_by_type.each_pair do |includes_association_type, includes|
polymorphic_association_includes_for_type(association, includes_association_name, includes_association_type, includes)
end
end
def polymorphic_association_includes_for_type(association, includes_association_name, includes_association_type, includes)
id_attr = "#{includes_association_name}_id"
type_attr = "#{includes_association_name}_type"
items = association.select {|item| item[type_attr] == includes_association_type.to_s }
item_ids = items.map {|item| item[id_attr] }
items_with_includes = includes_association_type.where(id: item_ids).includes(includes).index_by(&:id)
items.each do |parent|
parent.send("#{includes_association_name}=", items_with_includes[parent[id_attr]])
end
end
These would allow you to say:
messages = Message.all
polymorhpic_association_includes messages, :messageable, {
Ticket => :comments,
User => :ratings
}
Not a particularly fluent interface but it works in general.
Place the includes on a default scope for each model:
class Ticket
has_many :messages, as: :messageable
has_many :comments
default_scope -> { includes(:comments).order('id DESC') }
end
class User
has_many :messages, as: :messageable
has_many :ratings
default_scope -> { includes(:ratings).order('id DESC') }
end
Then whenever you call Message.all each polymorphic association will include it's own resources.
Also if you need to call the class without the scope just use unscoped or create a different scope:
class Ticket
has_many :messages, as: :messageable
has_many :comments
has_many :watchers
default_scope -> { includes(:comments).order('id DESC') }
scope :watched -> {includes(:watchers)}
end
Ticket.unscoped.all # without comments or watchers (or order)
Ticket.watched.all # includes watchers only

Resources