Reputation: 35350
class Users < ActiveRecord::Base
has_many :meetings, :through => :meeting_participations
has_many :meeting_participations
end
class Meetings < ActiveRecord::Base
has_many :users, :through => :meeting_participations
has_many :meeting_participations
end
class MeetingParticipations < ActiveRecord::Base
belongs_to :user
belongs_to :meeting
scope :hidden, where(:hidden => true)
scope :visible, where(:hidden => false)
end
hidden
is an extra boolean column within the m2m association table. Given some Users
instance current_user
, I want to do
current_user.meetings.visible
which will retrieve a collection of Meetings
for which the user is a participant where the hidden
column is false
. The closest I have gotten is adding the following scope to the Meetings
class
scope :visible, joins(:meeting_participations) & MeetingParticipation.visible
The scope
does filter the Meetings
against the MeetingParticipations
table, however there is no join/condition against the MeetingParticipations
table related to current_user
.
The issue with this is, if current_user
and another_user
are both participants for some Meetings
instance, a Meetings
record in the result set will be returned for each participant that has hidden
set to false
. If current_user
has true
set for hidden
for all Meetings
, if another_user
is a participant in any of those same Meetings with hidden
set to false
, those Meetings
will appear in the Meetings.visible
result set.
Is it possible to have a scope as I've mentioned above which will properly join on the User
instance? If not, can someone recommend a solution to this?
Upvotes: 87
Views: 56005
Reputation: 951
I know this question was answered a while back but I just encountered a similar issue and was looking around for the best way to handle this. The accepted solution is very simple but I think it would be cleaner by moving the scope of the association from Users
to Meeting
as should below
class Users < ActiveRecord::Base
has_many :meetings, :through => :meeting_participations
has_many :meeting_participations
end
class Meetings < ActiveRecord::Base
has_many :users, :through => :meeting_participations
has_many :meeting_participations
scope :hidden, -> { where('meeting_participations.hidden = ?', true) }
scope :visible, -> { where('meeting_participations.hidden = ?', false) }
end
class MeetingParticipations < ActiveRecord::Base
belongs_to :user
belongs_to :meeting
scope :hidden, where(:hidden => true)
scope :visible, where(:hidden => false)
end
With this, you are able to call current_user.meetings.hidden
By design, the meeting now dictates what makes it hidden/visible.
Upvotes: 2
Reputation: 25054
The clean, associations way to do it is:
has_many :visible_meetings, -> { merge(MeetingParticipations.visible) },
:source => :meeting, :through => :meeting_participations
To put it in more generic terms: if you have a chained has_many
association you can scope the intermediate (through
) association via merging the scope. Probably requires Rails 4+.
Otherwise this would have to be done via creating a (probably unwanted) intermediate scoped association as seen in @Paul Pettengill's answer.
Upvotes: 19
Reputation: 1643
This is my solution for your problem:
class User < ActiveRecord::Base
has_many :meeting_participations
has_many :meetings, :through => :meeting_participations do
def visible
where("meeting_participations.visible = ?", true)
end
end
end
@user.meetings.visible
Upvotes: 121
Reputation: 1383
You could also do:
current_user.meeting_participations.visible.map(&:meeting)
Upvotes: -3
Reputation: 12663
Here's a one liner:
Meeting.joins(:meeting_participations).where(meeting_participation: { hidden: false, user_id: current_user.id })
This is great because you can make a scope out of it, a function out of it, or simply call it anywhere. You can also add any more restrictions you want to the hash.
Upvotes: 4
Reputation: 4855
In Rails 4, you can specify the scope originally defined in the child object in the association itself. Short: you don't have to know the internals of the MeetingParticipation model within the User model.
class User < ActiveRecord::Base
has_many :meeting_participations
has_many :meetings, :through => :meeting_participations
has_many :visible_participations, -> { visible }, :class_name => 'MeetingParticipation'
has_many :visible_meetings, :source => :meeting, :through => :visible_participations
end
class Meeting < ActiveRecord::Base
has_many :meeting_participations
has_many :users, :through => :meeting_participations
end
class MeetingParticipation < ActiveRecord::Base
belongs_to :user
belongs_to :meeting
scope :hidden, -> { where(:hidden => true) }
scope :visible, -> { where(:hidden => false) }
end
This would allow you to do: user1.visible_meetings
and user2.visible_meetings
with different result sets
Upvotes: 75
Reputation: 25757
It would seem to me that it is not sensible to use a scope on Meeting for your purpose. A meeting itself has no visibility, but the participation has. So I would suggest an extension on the association within User:
class User < ActiveRecord::Base
has_many :meetings, :through => :meeting_participations do
def visible
ids = MeetingParticipation.
select(:meeting_id).
where(:user_id => proxy_owner.id, :visible => true).
map{|p| p.meeting_id}
proxy_target.where("id IN (?)", ids)
end
end
...
end
I hope, this helps.
Upvotes: -5